fix(web): scrub invite tokens before analytics

This commit is contained in:
jjbrunton committed 2026-09-06 12:49:32 +01:00
1 parent 49a9cd270f
commit dd1a47ecfa
2 files changed
+48 -2

No files matched your search

+22
View File
@@ -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')
})
}) })
+26 -2
View File
@@ -1,9 +1,12 @@
'use client' 'use client'
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,
}) })
} }
}, []) }, [])