mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Telemetry feature. ## What is the current behavior? - Session replay is off, and nothing in the code keeps it off. - `packages/common/posthog-client.ts` sets no recording config at all. - So PostHog's project setting alone decides, for every app sharing that project. - Studio, www and docs share one project. - Studio shows customer data almost everywhere: SQL editor, table rows, connection strings, API keys. - posthog-js masks inputs by default. It does not mask rendered text. - [GROWTH-1055](https://linear.app/supabase/issue/GROWTH-1055) ## What is the new behavior? - `posthogClient.init()` takes a masking config, and disables recording when it gets none. - Studio passes one behind `NEXT_PUBLIC_POSTHOG_SESSION_REPLAY`. - Every other app passes nothing, so it never loads the recorder. - Studio masks all text and all inputs. - `data-ph-capture="true"` opts one element's text back in. Unused so far. - Canvas is blocked, because it records as images that text masking cannot reach. - Query strings and fragments are stripped from recorded URLs, where auth callbacks carry tokens. - Request and response bodies are never recorded. - Console logs are never recorded, since masking only reaches DOM text. - Masking is set in code, so PostHog's settings cannot loosen it. - Consent gating is unchanged. Nothing records before a user accepts. ## Additional context - Recording needs three things: this env var, the PostHog project toggle, and user consent. - All three are off or unset, so merging this changes nothing at runtime. - `NEXT_PUBLIC_POSTHOG_SESSION_REPLAY` goes into Vercel on Preview scope first, to test on a preview build. - Production scope comes later, once we are ready to record there. - `NEXT_PUBLIC_*` is inlined at build time, so each scope needs a rebuild afterwards. - Text inside HTML attributes (`title`, `alt`, `href`) is still recorded as-is. - posthog-js exposes no hook for masking attributes, so covering it needs `ph-no-capture` per component. - Staging has no server-side masking config, so that is where this gets verified. - Plan: enable recording on staging, verify masked text on a preview, then decide on production. - Network timing stays on for the dashboard performance work. Payloads stay off. - Tests cover both masking functions and the config values. ## Screenshots https://github.com/user-attachments/assets/aa064a04-f977-4453-a3da-2fe0cdcead08 <img width="889" height="651" alt="CleanShot 2026-07-31 at 10 13 43" src="https://github.com/user-attachments/assets/f1d07946-fd68-42b2-89f1-d201bc605638" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added privacy-focused session replay for Studio. * Text and form inputs are masked by default, with explicit opt-in capture. * Network recordings remove query strings and fragments. * Headers, request bodies, canvas data, and console logs are excluded. * **Bug Fixes** * Improved whitespace and capture-attribute handling during masking. * Session replay remains disabled without a masking policy or explicit enablement. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
import * as Sentry from '@sentry/nextjs'
|
|
import { PageTelemetry, posthogClient, useParams, useUser } from 'common'
|
|
import { useEffect, useRef } from 'react'
|
|
import { useConsentToast } from 'ui-patterns/consent'
|
|
|
|
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { API_URL, IS_PLATFORM } from '@/lib/constants'
|
|
import { IS_SESSION_REPLAY_ENABLED, SESSION_REPLAY_CONFIG } from '@/lib/session-replay'
|
|
|
|
export function Telemetry() {
|
|
// Although this is "technically" breaking the rules of hooks
|
|
// IS_PLATFORM never changes within a session, so this won't cause any issues
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
const { hasAcceptedConsent } = IS_PLATFORM ? useConsentToast() : { hasAcceptedConsent: true }
|
|
|
|
// Get org from selected organization query because it's not
|
|
// always available in the URL params
|
|
const { data: organization } = useSelectedOrganizationQuery()
|
|
|
|
const user = useUser()
|
|
|
|
// Project ref from the URL params, mirroring the backend's `request.params.ref`
|
|
const { ref: projectRef } = useParams()
|
|
|
|
// Mirror the user's org-list length into a PostHog person property so feature
|
|
// flags and analytics can segment by current org membership. signup_timestamp
|
|
// is set on the same identify so flag audiences requiring both properties see
|
|
// them together on /decide. Only fires when the value changes.
|
|
const { data: organizations } = useOrganizationsQuery()
|
|
const lastSentRef = useRef<{
|
|
userId: string
|
|
orgCount: number
|
|
signupTimestamp?: string
|
|
} | null>(null)
|
|
useEffect(() => {
|
|
if (!user?.id || !organizations) return
|
|
const orgCount = organizations.length
|
|
const signupTimestamp = user.created_at ?? undefined
|
|
const last = lastSentRef.current
|
|
if (
|
|
last?.userId === user.id &&
|
|
last.orgCount === orgCount &&
|
|
last.signupTimestamp === signupTimestamp
|
|
) {
|
|
return
|
|
}
|
|
lastSentRef.current = { userId: user.id, orgCount, signupTimestamp }
|
|
posthogClient.identify(user.id, {
|
|
org_count: orgCount,
|
|
...(signupTimestamp && { signup_timestamp: signupTimestamp }),
|
|
})
|
|
}, [user?.id, user?.created_at, organizations])
|
|
|
|
useEffect(() => {
|
|
// don't set the sentry user id if the user hasn't logged in (so that Sentry errors show null user id instead of anonymous id)
|
|
if (user?.id) {
|
|
Sentry.setUser({ id: user.id })
|
|
}
|
|
|
|
// Tag Sentry events with the current project ref and customer org slug so backend/
|
|
// frontend errors can be filtered by project / org. Passing a null value clears
|
|
// the tag, so stale values don't leak across navigation.
|
|
Sentry.setTag('project_ref', projectRef ?? null)
|
|
Sentry.setTag('org_slug', organization?.slug ?? null)
|
|
}, [user?.id, projectRef, organization?.slug])
|
|
|
|
return (
|
|
<PageTelemetry
|
|
API_URL={API_URL}
|
|
hasAcceptedConsent={hasAcceptedConsent}
|
|
enabled={IS_PLATFORM}
|
|
organizationSlug={organization?.slug}
|
|
sessionReplay={IS_SESSION_REPLAY_ENABLED ? SESSION_REPLAY_CONFIG : undefined}
|
|
/>
|
|
)
|
|
}
|