mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
fix(web): scrub invite tokens before analytics
This commit is contained in:
1 parent
49a9cd270f
commit
dd1a47ecfa
2 files changed
+46
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
import { readFile } from 'node:fs/promises'
|
import { readFile } from 'node:fs/promises'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { redactInvitePaths } from './providers'
|
||||||
|
|
||||||
const pageViewPath = new URL('./PostHogPageView.tsx', import.meta.url)
|
const pageViewPath = new URL('./PostHogPageView.tsx', import.meta.url)
|
||||||
const providerPath = new URL('./providers.tsx', import.meta.url)
|
const providerPath = new URL('./providers.tsx', import.meta.url)
|
||||||
@@ -18,7 +19,28 @@ describe('PostHog invite privacy boundary', () => {
|
|||||||
expect(source).toContain('capture_pageview: false')
|
expect(source).toContain('capture_pageview: false')
|
||||||
expect(source).toContain('capture_pageleave: false')
|
expect(source).toContain('capture_pageleave: false')
|
||||||
expect(source).toContain('get_current_url: redactInviteUrl')
|
expect(source).toContain('get_current_url: redactInviteUrl')
|
||||||
|
expect(source).toContain('before_send: redactInvitePathsBeforeSend')
|
||||||
expect(source).toContain("url.pathname.startsWith('/join/')")
|
expect(source).toContain("url.pathname.startsWith('/join/')")
|
||||||
expect(source).toContain('/join/[invite]')
|
expect(source).toContain('/join/[invite]')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('removes an invite code from every serialized URL or path property', () => {
|
||||||
|
const inviteCode = 'A4K9BT2Q'
|
||||||
|
const event = {
|
||||||
|
event: 'join_page_viewed',
|
||||||
|
properties: {
|
||||||
|
$current_url: `https://sauci.app/join/${inviteCode}`,
|
||||||
|
$pathname: `/join/${inviteCode}`,
|
||||||
|
$referrer: `https://sauci.app/join/${inviteCode}?source=message`,
|
||||||
|
$session_entry_url: `https://sauci.app/join/${inviteCode}`,
|
||||||
|
nested: { previous_path: `/join/${inviteCode}` },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(redactInvitePaths(event))
|
||||||
|
|
||||||
|
expect(serialized).not.toContain(inviteCode)
|
||||||
|
expect(serialized).toContain('/join/[invite]')
|
||||||
|
expect(serialized).toContain('join_page_viewed')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
import posthog from 'posthog-js'
|
import posthog from 'posthog-js'
|
||||||
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
||||||
|
import type { CaptureResult } from 'posthog-js'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
const INVITE_PATH_PATTERN = /\/join\/[^/?#\s]+/g
|
||||||
|
|
||||||
function redactInviteUrl(defaultUrl: string): string {
|
function redactInviteUrl(defaultUrl: string): string {
|
||||||
const url = new URL(defaultUrl)
|
const url = new URL(defaultUrl)
|
||||||
|
|
||||||
@@ -14,6 +17,26 @@ function redactInviteUrl(defaultUrl: string): string {
|
|||||||
return defaultUrl
|
return defaultUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function redactInvitePaths<T>(value: T): T {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.replace(INVITE_PATH_PATTERN, '/join/[invite]') as T
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(redactInvitePaths) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === 'object' && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
|
||||||
|
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, redactInvitePaths(entry)])) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactInvitePathsBeforeSend(event: CaptureResult | null): CaptureResult | null {
|
||||||
|
return event ? redactInvitePaths(event) : null
|
||||||
|
}
|
||||||
|
|
||||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||||
@@ -23,6 +46,7 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|||||||
capture_pageview: false,
|
capture_pageview: false,
|
||||||
capture_pageleave: false,
|
capture_pageleave: false,
|
||||||
get_current_url: redactInviteUrl,
|
get_current_url: redactInviteUrl,
|
||||||
|
before_send: redactInvitePathsBeforeSend,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
Reference in new issue
Block a user