merge: redact invite analytics URLs

This commit is contained in:
jjbrunton committed 2026-09-06 12:45:46 +01:00
commit 077f5433e1
3 files changed
+41 -2

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
import { readFile } from 'node:fs/promises'
import { describe, expect, it } from 'vitest'
const pageViewPath = new URL('./PostHogPageView.tsx', import.meta.url)
const providerPath = new URL('./providers.tsx', import.meta.url)
describe('PostHog invite privacy boundary', () => {
it('does not send a global page view for invite routes', async () => {
const source = await readFile(pageViewPath, 'utf8')
expect(source).toContain("pathname?.startsWith('/join/')")
expect(source.indexOf("pathname?.startsWith('/join/')")).toBeLessThan(source.indexOf("posthog.capture('$pageview'"))
})
it('disables automatic page-leave capture and redacts join URLs', async () => {
const source = await readFile(providerPath, 'utf8')
expect(source).toContain('capture_pageview: false')
expect(source).toContain('capture_pageleave: false')
expect(source).toContain('get_current_url: redactInviteUrl')
expect(source).toContain("url.pathname.startsWith('/join/')")
expect(source).toContain('/join/[invite]')
})
})
+4
View File
@@ -10,6 +10,10 @@ function PostHogPageViewInner() {
const posthog = usePostHog() const posthog = usePostHog()
useEffect(() => { useEffect(() => {
// Invite codes are bearer-like tokens. The join route emits its own
// aggregate-only event, so it must never produce a URL-bearing page view.
if (pathname?.startsWith('/join/')) return
if (pathname && posthog) { if (pathname && posthog) {
let url = window.origin + pathname let url = window.origin + pathname
if (searchParams.toString()) { if (searchParams.toString()) {
+13 -2
View File
@@ -4,14 +4,25 @@ import posthog from 'posthog-js'
import { PostHogProvider as PHProvider } from 'posthog-js/react' import { PostHogProvider as PHProvider } from 'posthog-js/react'
import { useEffect } from 'react' import { useEffect } from 'react'
function redactInviteUrl(defaultUrl: string): string {
const url = new URL(defaultUrl)
if (url.pathname.startsWith('/join/')) {
return `${url.origin}/join/[invite]`
}
return defaultUrl
}
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) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://eu.i.posthog.com', api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://eu.i.posthog.com',
person_profiles: 'identified_only', person_profiles: 'identified_only',
capture_pageview: false, // We'll capture pageviews manually for better control capture_pageview: false,
capture_pageleave: true, capture_pageleave: false,
get_current_url: redactInviteUrl,
}) })
} }
}, []) }, [])