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
+48
-2
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { redactInvitePaths } from './providers'
|
||||
|
||||
const pageViewPath = new URL('./PostHogPageView.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_pageleave: false')
|
||||
expect(source).toContain('get_current_url: redactInviteUrl')
|
||||
expect(source).toContain('before_send: redactInvitePathsBeforeSend')
|
||||
expect(source).toContain("url.pathname.startsWith('/join/')")
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import posthog from 'posthog-js'
|
||||
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
||||
import posthog from 'posthog-js'
|
||||
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
||||
import type { CaptureResult } from 'posthog-js'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const INVITE_PATH_PATTERN = /\/join\/[^/?#\s]+/g
|
||||
|
||||
function redactInviteUrl(defaultUrl: string): string {
|
||||
const url = new URL(defaultUrl)
|
||||
|
||||
@@ -14,6 +17,26 @@ function redactInviteUrl(defaultUrl: string): string {
|
||||
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 }) {
|
||||
useEffect(() => {
|
||||
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_pageleave: false,
|
||||
get_current_url: redactInviteUrl,
|
||||
before_send: redactInvitePathsBeforeSend,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
Reference in new issue
Block a user