mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 18:11:51 +08: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 -->
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
import type { CapturedNetworkRequest, SessionRecordingOptions } from 'common'
|
|
|
|
/**
|
|
* Enables session replay in Studio. Recording also requires "Record user
|
|
* sessions" in PostHog, which www and docs share.
|
|
*/
|
|
export const IS_SESSION_REPLAY_ENABLED = process.env.NEXT_PUBLIC_POSTHOG_SESSION_REPLAY === 'true'
|
|
|
|
/**
|
|
* Setting `data-ph-capture="true"` on an element opts its text in to session
|
|
* recording. All text is opted out by default.
|
|
*/
|
|
const CAPTURE_DATASET_KEY = 'phCapture'
|
|
|
|
/**
|
|
* Returns asterisks for all text except text inside elements marked
|
|
* `data-ph-capture="true"`.
|
|
*/
|
|
export function maskReplayText(text: string, element?: HTMLElement): string {
|
|
if (element?.dataset[CAPTURE_DATASET_KEY] === 'true') return text
|
|
return '*'.repeat(text.trim().length)
|
|
}
|
|
|
|
/**
|
|
* Strips query strings and fragments from recorded URLs, which posthog-js applies
|
|
* to page URLs as well as network requests. Auth callbacks carry tokens in the
|
|
* fragment.
|
|
*/
|
|
export function maskReplayNetworkRequest(request: CapturedNetworkRequest): CapturedNetworkRequest {
|
|
if (request.name) {
|
|
const separatorIndex = request.name.search(/[?#]/)
|
|
if (separatorIndex !== -1) {
|
|
request.name = request.name.slice(0, separatorIndex)
|
|
}
|
|
}
|
|
return request
|
|
}
|
|
|
|
export const SESSION_REPLAY_CONFIG: SessionRecordingOptions = {
|
|
// Match posthog-js defaults, but set here so the PostHog UI can't relax them.
|
|
maskAllInputs: true,
|
|
maskTextSelector: '*',
|
|
maskTextFn: maskReplayText,
|
|
// Keeps network capture to URL, status and timing. Overrides the PostHog UI.
|
|
recordHeaders: false,
|
|
recordBody: false,
|
|
// Canvas is captured as images, which text masking can't reach.
|
|
captureCanvas: { recordCanvas: false },
|
|
maskCapturedNetworkRequestFn: maskReplayNetworkRequest,
|
|
}
|