mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
fix(acquisition): close invite privacy gaps
This commit is contained in:
1 parent
1c7084c033
commit
4a57f31615
15 files changed
+213
-70
No files matched your search
@@ -65,13 +65,21 @@ describe('/join/[code] JoinClient', () => {
|
||||
expect(source).not.toContain('glass-light')
|
||||
})
|
||||
|
||||
it('describes open-answer visibility truthfully', async () => {
|
||||
it('describes comparison and open-answer visibility truthfully', async () => {
|
||||
const source = await readFile(componentPath, 'utf8')
|
||||
|
||||
expect(source).toContain('For open-answer questions, each response can be visible after you have both answered.')
|
||||
expect(source).toContain('Vote-style questions reveal shared outcomes')
|
||||
expect(source).toContain('open text, audio, photo, and who-likely responses can be visible to you both after you have each answered.')
|
||||
expect(source).not.toContain('Neither of you sees the other’s individual answers.')
|
||||
})
|
||||
|
||||
it('marks the invite-code surface out of PostHog DOM capture and replay', async () => {
|
||||
const source = await readFile(componentPath, 'utf8')
|
||||
|
||||
expect(source).toContain('data-ph-no-capture')
|
||||
expect(source).toContain('data-ph-mask')
|
||||
})
|
||||
|
||||
it('shows a clear fallback message for a malformed invite code', async () => {
|
||||
const source = await readFile(componentPath, 'utf8')
|
||||
|
||||
|
||||
@@ -124,10 +124,10 @@ export function JoinClient({ code }: { code: string }) {
|
||||
const storeUnavailableTitle = isOnline ? undefined : 'Reconnect to open the app store.'
|
||||
const desktopCopy = platform === 'desktop'
|
||||
? 'Sauci is a phone app. Copy your code, then open this link on your phone or install from the store there.'
|
||||
: 'They have already answered questions about you two. Join them to discover what you have in common. For open-answer questions, each response can be visible after you have both answered.'
|
||||
: 'Join to compare answers together. Vote-style questions reveal shared outcomes; open text, audio, photo, and who-likely responses can be visible to you both after you have each answered.'
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-b from-background-light to-background">
|
||||
<main className="min-h-screen bg-gradient-to-b from-background-light to-background" data-ph-no-capture data-ph-mask>
|
||||
<header className="py-6 px-6 border-b border-white/5">
|
||||
<div className="max-w-4xl mx-auto flex items-center gap-4">
|
||||
<Link href="/" className="flex items-center gap-2 text-white/70 hover:text-white transition-colors">
|
||||
@@ -150,7 +150,7 @@ export function JoinClient({ code }: { code: string }) {
|
||||
<h1 className="text-3xl sm:text-4xl font-bold mb-3">You've been invited to compare answers</h1>
|
||||
<p className="text-white/70 mb-10">{desktopCopy}</p>
|
||||
|
||||
<section className="surface-solid p-6 sm:p-8 mb-6" aria-labelledby="invite-code-heading">
|
||||
<section className="surface-solid p-6 sm:p-8 mb-6" aria-labelledby="invite-code-heading" data-ph-no-capture data-ph-mask>
|
||||
<h2 id="invite-code-heading" className="text-white/70 text-sm font-medium mb-3">Your invite code</h2>
|
||||
<p
|
||||
className="text-4xl sm:text-5xl font-bold tracking-[0.3em] text-primary mb-6"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const providerPath = new URL('./providers.tsx', import.meta.url)
|
||||
|
||||
describe('PostHogProvider invite privacy', () => {
|
||||
it('disables DOM autocapture and session recording on invite routes', async () => {
|
||||
const source = await readFile(providerPath, 'utf8')
|
||||
|
||||
expect(source).toContain("pathname?.startsWith('/join/')")
|
||||
expect(source).toContain("window.location.pathname.startsWith('/join/')")
|
||||
expect(source).toContain('autocapture: !isInvitePage')
|
||||
expect(source).toContain('disable_session_recording: isInvitePage')
|
||||
expect(source).toContain('mask_all_text: isInvitePage')
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import posthog from 'posthog-js'
|
||||
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
||||
import type { CaptureResult } from 'posthog-js'
|
||||
import { useEffect } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
const INVITE_PATH_PATTERN = /\/join\/[^/?#\s]+/g
|
||||
|
||||
@@ -38,18 +39,41 @@ export function redactInvitePathsBeforeSend(event: CaptureResult | null): Captur
|
||||
}
|
||||
|
||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
const pathname = usePathname()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
const isInvitePage = window.location.pathname.startsWith('/join/')
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://eu.i.posthog.com',
|
||||
person_profiles: 'identified_only',
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
get_current_url: redactInviteUrl,
|
||||
before_send: redactInvitePathsBeforeSend,
|
||||
})
|
||||
// This must be part of initialization, not only the navigation effect
|
||||
// below, so an initially loaded invite page is protected before its DOM
|
||||
// can be considered for capture or recording.
|
||||
autocapture: !isInvitePage,
|
||||
disable_session_recording: isInvitePage,
|
||||
mask_all_text: isInvitePage,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return
|
||||
|
||||
// Invite codes are credentials. Do not let their page enter PostHog's DOM
|
||||
// autocapture or session-recording pipeline, even though event properties
|
||||
// and URLs are separately redacted below.
|
||||
const isInvitePage = pathname?.startsWith('/join/')
|
||||
posthog.set_config({
|
||||
autocapture: !isInvitePage,
|
||||
disable_session_recording: isInvitePage,
|
||||
mask_all_text: isInvitePage,
|
||||
})
|
||||
}, [pathname])
|
||||
|
||||
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
return <>{children}</>
|
||||
|
||||
Reference in new issue
Block a user