From 1bd69afc78b4da509b0735f5d5f1a9dd523b0663 Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Mon, 28 Sep 2026 11:18:08 +0200 Subject: [PATCH 1/2] refactor: delegate recordings integration to plugin skill --- .../features/onboarding/build-prompt.test.ts | 161 +++++++----------- .../features/onboarding/recording.test.ts | 34 ---- src/features/onboarding/build-prompt.ts | 10 +- src/features/onboarding/sections/recording.ts | 53 ++---- .../steps/determine-recording-sdk.md | 20 --- .../onboarding/steps/integrate-recording.md | 151 ++++------------ 6 files changed, 105 insertions(+), 324 deletions(-) delete mode 100644 __tests__/features/onboarding/recording.test.ts delete mode 100644 src/features/onboarding/steps/determine-recording-sdk.md diff --git a/__tests__/features/onboarding/build-prompt.test.ts b/__tests__/features/onboarding/build-prompt.test.ts index 3ffea73..d0f85e1 100644 --- a/__tests__/features/onboarding/build-prompt.test.ts +++ b/__tests__/features/onboarding/build-prompt.test.ts @@ -1,4 +1,3 @@ -import { basename, resolve } from 'node:path'; import { buildOnboardingPrompt } from '@features/onboarding/index.js'; describe('buildOnboardingPrompt', () => { @@ -133,175 +132,129 @@ describe('buildOnboardingPrompt', () => { }); }); - describe('session recordings MCP setup', () => { + describe('session recordings skill reference', () => { const recordingOpts = { ...baseOpts, goals: ['session-recordings' as const], }; - it('instructs the agent to create a recording policy and targeting-key rule', () => { - const sut = buildOnboardingPrompt(recordingOpts); + it('delegates to setup-session-recording skill via CLI when installed via CLI', () => { + const sut = buildOnboardingPrompt({ + ...recordingOpts, + pluginInstallMethod: 'cli', + }); - expect(sut).toContain('mcp__confidence-flags__createRecordingPolicy'); - expect(sut).toContain('mcp__confidence-flags__addRecordingRule'); - expect(sut).toContain('targetingKeySelector'); - expect(sut).toContain('`enabled`: true'); - expect(sut).toContain('STATUS: Setting up recording policy...'); + expect(sut).toContain( + 'Invoke the `/confidence:setup-session-recording` skill as a **methodology reference**', + ); + expect(sut).not.toContain('Read `.claude/skills/setup-session-recording/SKILL.md`'); }); - it('includes Confidence resources in the final change summary', () => { - const sut = buildOnboardingPrompt(recordingOpts); + it('delegates to setup-session-recording skill as file path when installed via download', () => { + const sut = buildOnboardingPrompt({ + ...recordingOpts, + pluginInstallMethod: 'download', + }); - expect(sut).toContain('Created recording policy with targeting key'); expect(sut).toContain( - 'Created recording rule (Record all visitors, 100% audience, 100% sessions, enabled)', + 'Read `.claude/skills/setup-session-recording/SKILL.md` as a **methodology reference**', ); }); - it('instructs the agent to pass a 100% audience instead of sending 0', () => { - const sut = buildOnboardingPrompt(recordingOpts); + it('uses cursor skills dir for cursor with download method', () => { + const sut = buildOnboardingPrompt({ + ...recordingOpts, + ide: 'cursor', + pluginInstallMethod: 'download', + }); - expect(sut).toContain('`stableAudiencePercentage`: 100'); - expect(sut).toContain('`sessionSampleRate`: 1'); - expect(sut).toContain('agents often send `0`'); + expect(sut).toContain('Read `.cursor/skills/setup-session-recording/SKILL.md`'); }); - it('matches recording policies to the client resource name, not display name', () => { - const sut = buildOnboardingPrompt(recordingOpts); + it('uses codex skill invocation for codex with CLI method', () => { + const sut = buildOnboardingPrompt({ + ...recordingOpts, + ide: 'codex', + pluginInstallMethod: 'cli', + }); - expect(sut).toContain('never reuse a policy because its display name looks similar'); expect(sut).toContain( - "Reuse a policy only when its `clients` list contains this client's resource name", + 'Invoke the `$setup-session-recording` skill as a **methodology reference**', ); - expect(sut).toContain('pass each non-empty `nextPageToken` back as `pageToken`'); - expect(sut).toContain("`clientName` set to this client's resource name"); - expect(sut).toContain('Keep the returned resource name (`clients/` from `name:`)'); - expect(sut).not.toContain('cannot match an existing policy to a client'); }); - it('treats goal selection as confirmation to enable recording', () => { + it('overrides skill formatting with STATUS-line-only output', () => { const sut = buildOnboardingPrompt(recordingOpts); - expect(sut).toContain('explicit confirmation to start recording'); - expect(sut).toContain('enable the rule immediately without asking another question'); - expect(sut).toContain('Tell the user afterward that the rule is enabled'); - }); - - it('names the client and policy after the resolved project dir, not the framework', () => { - const sut = buildOnboardingPrompt({ ...recordingOpts, projectDir: '/tmp/checkout-web' }); - - expect(sut).toContain('with the display name "checkout-web"'); - expect(sut).toContain('with `displayName` "checkout-web Session Recording"'); - expect(sut).toContain('never after the framework'); - expect(sut).not.toContain('react Session Recording'); - }); - - it('resolves --dir . so the client is not named "."', () => { - const sut = buildOnboardingPrompt({ ...recordingOpts, projectDir: '.' }); - const name = basename(resolve('.')); - - expect(sut).toContain(`with the display name "${name}"`); - expect(sut).not.toContain('with the display name "."'); + expect(sut).toContain('Ignore its output formatting entirely'); + expect(sut).toContain('no step tracker, no EDUCATE blocks, no AskUserQuestion calls'); + expect(sut).toContain('STATUS: Setting up session recording...'); }); - it('does not reuse a colliding client from another project', () => { + it('produces a single recording step instead of two', () => { const sut = buildOnboardingPrompt(recordingOpts); - expect(sut).toContain('Do not reuse a client solely because its display name already exists'); - expect(sut).toContain( - 'unless the colliding client is the one created earlier in this same run', - ); - expect(sut).toContain( - 'Never call `mcp__confidence-flags__getClientSecret` for a colliding client', - ); - expect(sut).not.toContain( - 'If the tool says that display name already exists, keep the resource name', - ); + const recordingHeaders = sut.match(/^## \d+\. .*[Rr]ecord/gm) ?? []; + expect(recordingHeaders).toHaveLength(1); + expect(sut).not.toContain('Determine Session Recording SDK'); }); - it('refers to targeting key and policy by name instead of numbered steps', () => { + it('includes example STATUS lines for all recording phases', () => { const sut = buildOnboardingPrompt(recordingOpts); - expect(sut).toContain('from the **Targeting key** item in'); - expect(sut).toContain('the resource name from the **Policy** item in'); - expect(sut).not.toContain('from step 2'); - expect(sut).not.toContain('from step 3'); + expect(sut).toContain('STATUS: Checking session recording availability...'); + expect(sut).toContain('STATUS: Setting up recording policy...'); + expect(sut).toContain('STATUS: Installing session recording SDK...'); + expect(sut).toContain('STATUS: Adding session recording provider...'); + expect(sut).toContain('STATUS: Configuring privacy and capture settings...'); + expect(sut).toContain('STATUS: Verifying project builds...'); }); - it('uses the framework public env var so the browser can read the client secret', () => { + it('includes guardrails for framework-specific env vars', () => { const sut = buildOnboardingPrompt(recordingOpts); expect(sut).toContain('VITE_CONFIDENCE_CLIENT_SECRET'); expect(sut).toContain('NEXT_PUBLIC_CONFIDENCE_CLIENT_SECRET'); expect(sut).toContain('REACT_APP_CONFIDENCE_CLIENT_SECRET'); - expect(sut).toContain('clientSecret: '); - expect(sut).toContain('fill ``'); }); - it('gates recording behind an existing consent tool when one is found', () => { + it('includes guardrails for consent-gated recording', () => { const sut = buildOnboardingPrompt(recordingOpts); - expect(sut).toContain('OneTrust, Cookiebot, Usercentrics, Didomi'); expect(sut).toContain("`mode: 'manual'`"); expect(sut).toContain('only after analytics or recording consent is granted'); - expect(sut).toContain('Fill ``'); - }); - - it('tells the agent what recording-rule status to write for each path', () => { - const sut = buildOnboardingPrompt(recordingOpts); - - expect(sut).toContain('Fill `` with "No recording rule was created'); - expect(sut).toContain('Fill `` with "The recording rule is enabled'); - expect(sut).toContain( - 'Fill `` with "The existing recording rule records nobody', - ); }); - it('flags a reused rule that has no audience segment', () => { + it('includes guardrails for client naming', () => { const sut = buildOnboardingPrompt(recordingOpts); - expect(sut).toContain('STATUS: Existing recording rule records nobody'); - expect(sut).toContain('An audience segment (`segments/`) is the healthy'); - expect(sut).toContain('including rules with no targeting conditions'); - expect(sut).toContain('records nobody and no MCP tool can repair'); + expect(sut).toContain('Name clients after the project, never after the framework'); }); - it('keeps the client secret out of output and source', () => { + it('includes guardrails for secret handling', () => { const sut = buildOnboardingPrompt(recordingOpts); - expect(sut).toContain('write the Frontend client secret to `.env` under that exact name'); - expect(sut).toContain('Ensure `.env` is in `.gitignore`'); + expect(sut).toContain('`.env`'); + expect(sut).toContain('`.gitignore`'); expect(sut).toContain( 'never echo the secret in STATUS lines, the report, or generated source', ); }); - it('picks the targeting key from the context schema like flags', () => { + it('tells the agent to fill report placeholders for recording rule and consent status', () => { const sut = buildOnboardingPrompt(recordingOpts); - expect(sut).toContain('mcp__confidence-flags__getContextSchema'); - expect(sut).toContain('first available entity field'); - expect(sut).toContain('Do not assume `user_id` or `targeting_key`'); - expect(sut).toContain('`localStorage` only in a browser entrypoint'); - expect(sut).not.toContain("inspect the app's auth/session code"); + expect(sut).toContain('``'); + expect(sut).toContain('``'); }); - it('reuses the feature flag identity when both goals are selected', () => { + it('correlates sessions with flag evaluations when both goals are selected', () => { const sut = buildOnboardingPrompt({ ...recordingOpts, goals: ['feature-flags', 'session-recordings'], }); - expect(sut).toContain('If feature flags were integrated earlier, reuse that entity field'); - expect(sut).toContain('reuse the flag identity if present'); - }); - - it('uses Codex MCP tool names for Codex', () => { - const sut = buildOnboardingPrompt({ ...recordingOpts, ide: 'codex' }); - - expect(sut).toContain('confidence-flags:createRecordingPolicy'); - expect(sut).toContain('confidence-flags:addRecordingRule'); - expect(sut).not.toContain('mcp__confidence-flags__createRecordingPolicy'); + expect(sut).toContain('same identity field in `context` so sessions correlate'); }); }); }); diff --git a/__tests__/features/onboarding/recording.test.ts b/__tests__/features/onboarding/recording.test.ts deleted file mode 100644 index c7f0c69..0000000 --- a/__tests__/features/onboarding/recording.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { basename, resolve } from 'node:path'; -import { projectDisplayName, projectParentName } from '@features/onboarding/sections/recording.js'; - -describe('projectDisplayName', () => { - it('uses the last segment of an absolute project dir', () => { - const sut = projectDisplayName('/tmp/checkout-web'); - expect(sut).toBe('checkout-web'); - }); - - it('resolves a relative project dir instead of using "." as the client name', () => { - const sut = projectDisplayName('.'); - - expect(sut).toBe(basename(resolve('.'))); - expect(sut).not.toBe('.'); - }); - - it('resolves ".." to the parent folder name', () => { - const sut = projectDisplayName('..'); - - expect(sut).toBe(basename(resolve('..'))); - expect(sut).not.toBe('..'); - }); - - it('falls back to "project" when the resolved path has no basename', () => { - expect(projectDisplayName('/')).toBe('project'); - }); -}); - -describe('projectParentName', () => { - it('uses the parent folder of the resolved project dir', () => { - const sut = projectParentName('/tmp/checkout-web'); - expect(sut).toBe('tmp'); - }); -}); diff --git a/src/features/onboarding/build-prompt.ts b/src/features/onboarding/build-prompt.ts index d90264b..725ecbc 100644 --- a/src/features/onboarding/build-prompt.ts +++ b/src/features/onboarding/build-prompt.ts @@ -4,7 +4,7 @@ import { buildToolVars } from './tool-vars.js'; import { preflight } from './sections/preflight.js'; import { scaffold } from './sections/scaffold.js'; import { integrateViaSkill } from './sections/integrate.js'; -import { determineRecordingSDK, integrateRecording } from './sections/recording.js'; +import { integrateRecording } from './sections/recording.js'; import { instrumentEvents } from './sections/event-tracking.js'; import { generateReport } from './sections/report.js'; import { summary, rules } from './sections/summary.js'; @@ -44,14 +44,8 @@ export function buildOnboardingPrompt({ integrateViaSkill(framework, steps.next(), isEmptyProject, ide, pluginInstallMethod), ), - addIf(withRecordings, () => determineRecordingSDK(framework, steps.next(), tools)), addIf(withRecordings, () => - integrateRecording({ - step: steps.next(), - isEmptyProject, - projectDir, - toolVars: tools, - }), + integrateRecording(framework, steps.next(), isEmptyProject, ide, pluginInstallMethod), ), addIf(withEventTracking, () => diff --git a/src/features/onboarding/sections/recording.ts b/src/features/onboarding/sections/recording.ts index c23f438..232b686 100644 --- a/src/features/onboarding/sections/recording.ts +++ b/src/features/onboarding/sections/recording.ts @@ -1,51 +1,24 @@ -import { basename, dirname, resolve } from 'node:path'; -import { CONFIDENCE_DOCS_URL } from '@lib/constants.js'; +import type { IdeId, PluginInstallationMethod } from '@shared-kernel/types.js'; import { loadStep } from '../steps/load.js'; +import { referenceInstruction } from '../tool-vars.js'; -const FALLBACK_PROJECT_NAME = 'project'; - -type IntegrateRecordingParams = { - step: number; - isEmptyProject: boolean; - projectDir: string; - toolVars: Record; -}; - -export function projectDisplayName(projectDir: string): string { - return basename(resolve(projectDir)) || FALLBACK_PROJECT_NAME; -} - -export function projectParentName(projectDir: string): string { - return basename(dirname(resolve(projectDir))) || FALLBACK_PROJECT_NAME; -} - -export function determineRecordingSDK( +export function integrateRecording( framework: string, step: number, - toolVars: Record, + isEmptyProject: boolean, + ide: IdeId, + pluginInstallMethod?: PluginInstallationMethod | null, ): string { - return loadStep('determine-recording-sdk.md', { - STEP: step, - FRAMEWORK: framework, - DOCS_URL: CONFIDENCE_DOCS_URL, - ...toolVars, - }); -} - -export function integrateRecording({ - step, - isEmptyProject, - projectDir, - toolVars, -}: IntegrateRecordingParams): string { return loadStep('integrate-recording.md', { STEP: step, - PROJECT_NAME: projectDisplayName(projectDir), - PARENT_NAME: projectParentName(projectDir), - DOCS_URL: CONFIDENCE_DOCS_URL, - ANALYSIS_CONTEXT: isEmptyProject + FRAMEWORK: framework, + SKILL_READ_INSTRUCTION: referenceInstruction( + 'setup-session-recording', + ide, + pluginInstallMethod, + ), + DOMAIN_CONTEXT: isEmptyProject ? "The project was just scaffolded — configure recording on the sample app's main view." : "Identify the app's entry point or root layout where the session recorder should be initialized.", - ...toolVars, }); } diff --git a/src/features/onboarding/steps/determine-recording-sdk.md b/src/features/onboarding/steps/determine-recording-sdk.md deleted file mode 100644 index 0c03312..0000000 --- a/src/features/onboarding/steps/determine-recording-sdk.md +++ /dev/null @@ -1,20 +0,0 @@ -## {{STEP}}. Determine Session Recording SDK - -Print "STATUS: Checking session recording availability..." - -Confidence Session Recording is available as a browser SDK: `@spotify-confidence/session-recording`. -It captures DOM events in real time and streams them to the Confidence backend for replay and analysis. - -Check whether the project is a browser-based application (React, Next.js, Vue, Svelte, plain JS/TS with a DOM). - -- If yes: print "STATUS: Session recording supported — proceeding." and continue. -- If not (Node.js server, Go, Python, Swift, Kotlin, Java, etc.): print "STATUS: Session recording not supported — skipping." and skip to the report step. - -If docs MCP tools are available (from preflight): - -- Call `{{DOCS_searchDocumentation}}` with query "session recording integration". -- If results mention a session recording SDK, call `{{DOCS_getCodeSnippetAndSdkIntegrationTips}}` to get the integration guide. - -If docs MCP tools are NOT available: - -- Search the web for "Confidence session recording" at {{DOCS_URL}}, or check https://github.com/spotify/confidence-sdk-js for the latest API. diff --git a/src/features/onboarding/steps/integrate-recording.md b/src/features/onboarding/steps/integrate-recording.md index cb0b82d..6017a1e 100644 --- a/src/features/onboarding/steps/integrate-recording.md +++ b/src/features/onboarding/steps/integrate-recording.md @@ -1,131 +1,46 @@ ## {{STEP}}. Integrate Session Recording -Skip this step if the previous step determined that session recording is not available for this platform. +Print "STATUS: Setting up session recording..." -### {{STEP}}a. Analyze project for recording integration +The detected framework is **{{FRAMEWORK}}**. -Print "STATUS: Analyzing project for session recording..." +{{SKILL_READ_INSTRUCTION}} — use it for how to check platform compatibility, scan for consent tools, set up recording policies and rules, which SDK to install, how to initialize the recorder, and how to configure privacy and capture settings. Ignore its output formatting entirely: no step tracker, no EDUCATE blocks, no AskUserQuestion calls. -{{ANALYSIS_CONTEXT}} +Execute the skill's workflow automatically, without pausing for user input: -**Detect the source root** — check for `src`, `app`, `lib`, `pages`, `server` and use the first match (or `.`). Exclude `node_modules`, `.venv`, `vendor`, `target`, `build`, `dist`, `.next`, `__pycache__` from scans. +- **Discovery** (skill steps 1–2): check whether the platform supports session recording (browser-based only), scan the project for the entry point and existing consent tools. +- **Setup** (skill steps 3–4): resolve or create the Confidence client and recording policy, create and enable the recording rule. +- **Integration** (skill steps 5–8): install the session recording SDK, initialize the recorder, configure privacy and capture settings, verify the build. -**Consent tool** — look for an existing consent or cookie-banner implementation: OneTrust, Cookiebot, Usercentrics, Didomi, `react-cookie-consent`, a custom consent context, or a cookie-consent state hook. Note whether analytics or recording consent is already modeled. You will use this in {{STEP}}d. +If the skill's platform check determines session recording is not supported (non-browser project), print "STATUS: Session recording not supported — skipping." and skip to the next step. -### {{STEP}}b. Resolve client and recording policy +{{DOMAIN_CONTEXT}} -Print "STATUS: Setting up recording policy..." +**Example STATUS lines for this step:** -If flag management is unavailable from preflight, skip MCP calls in this substep. Write placeholders for the client secret and document in the report that the user must create a recording policy under Recordings > Settings. Fill `` with "No recording rule was created — create a policy and rule under Recordings > Settings before sessions will be captured". If they need setup details, search {{DOCS_URL}}. +- "STATUS: Checking session recording availability..." +- "STATUS: Session recording supported — proceeding." +- "STATUS: Session recording not supported — skipping." +- "STATUS: Analyzing project for session recording..." +- "STATUS: Scanning for consent tools..." +- "STATUS: Setting up recording policy..." +- "STATUS: Created recording policy: " +- "STATUS: Reusing recording policy: " +- "STATUS: Enabled recording rule" +- "STATUS: Installing session recording SDK..." +- "STATUS: Adding session recording provider..." +- "STATUS: Writing ..." +- "STATUS: Configuring privacy and capture settings..." +- "STATUS: Verifying project builds..." +- "STATUS: Fixing build errors..." -If flag management is available: +Note: when naming project files, use file name only, no path. -1. **Client** — reuse the Confidence client from an earlier feature-flags step if one was created in this same run. Otherwise call `{{FLAGS_createClient}}` with the display name "{{PROJECT_NAME}}" and `clientType` `Frontend`. Name it after this project, never after the framework — a framework name collides with clients from unrelated projects and attaches this policy to the wrong app. Keep the returned resource name (`clients/` from `name:`). +**Session-recording guardrails:** - Do not reuse a client solely because its display name already exists. A folder named `app`, `web`, or `frontend` often belongs to a different project. If `{{FLAGS_createClient}}` reports that display name is taken, call it again with a unique name: "{{PROJECT_NAME}} ({{PARENT_NAME}})", then "{{PROJECT_NAME}}-2", then "-3", until creation succeeds — unless the colliding client is the one created earlier in this same run, in which case keep that resource name. Never call `{{FLAGS_getClientSecret}}` for a colliding client you did not create in this run. - -2. **Targeting key** — same as flags. Call `{{FLAGS_getContextSchema}}` with the client's display name. Use the first available entity field (typically `visitor_id`). Do not assume `user_id` or `targeting_key`. If feature flags were integrated earlier, reuse that entity field. If the schema has no entity field, call `{{FLAGS_addContextField}}` with `fieldName` `visitor_id`, `fieldType` `string`, and `isEntity` `"true"` (string, not boolean). Fill it with a persisted visitor ID: reuse the flag identity if present, otherwise an existing anonymous/device ID, or generate once and store where the app already persists client state (`localStorage` only in a browser entrypoint). - -3. **Policy** — never reuse a policy because its display name looks similar. Call `{{FLAGS_listRecordingPolicies}}` and inspect every page: pass each non-empty `nextPageToken` back as `pageToken` until `nextPageToken` is empty. Reuse a policy only when its `clients` list contains this client's resource name (`clients/`). If nothing matches, call `{{FLAGS_createRecordingPolicy}}` with `displayName` "{{PROJECT_NAME}} Session Recording" and `clientName` set to this client's resource name. Keep the returned policy resource name. - -4. **Rule** — call `{{FLAGS_getRecordingPolicy}}` with `recordingPolicy` set to that resource name. If the policy has no rule yet, call `{{FLAGS_addRecordingRule}}` like this: - - Good: `targetingKeySelector` from the **Targeting key** item in {{STEP}}b, omit `targetingJson`, `stableAudiencePercentage`: 100, `sessionSampleRate`: 1, `enabled`: true - Bad: omitting the percentages (agents often send `0`, which records nobody) - - Pass `recordingPolicy` (the resource name from the **Policy** item in {{STEP}}b) and `displayName` "Record all visitors". - - Selecting Session Recordings in the wizard is explicit confirmation to start recording, so enable the rule immediately without asking another question. The MCP creates an unrestricted `segments/` audience even though `targetingJson` is omitted. Tell the user afterward that the rule is enabled and records 100% of visitors and sessions. Fill `` with "The recording rule is enabled — sessions are captured once the app runs with the client secret". - - If the policy already has a rule that is not enabled, call `{{FLAGS_setRecordingRuleEnabled}}` with that rule's resource name and `enabled` true. - - When reusing an existing rule, read its audience from the `{{FLAGS_getRecordingPolicy}}` output. An audience segment (`segments/`) is the healthy 100%-of-visitors representation, including rules with no targeting conditions. An audience of "all users" means the pre-fix rule has no segment, which records nobody and no MCP tool can repair — print "STATUS: Existing recording rule records nobody" and add a "Before you merge" item telling the user to delete that rule under Recordings > Settings and add a new one. Fill `` with "The existing recording rule records nobody — delete it under Recordings > Settings and add a new one". - -Print "STATUS: Created recording policy: " after a new policy, or "STATUS: Reusing recording policy: " when reusing. Print "STATUS: Enabled recording rule" after the rule is active. - -In the final change summary, include "Created recording policy with targeting key" and "Created recording rule (Record all visitors, 100% audience, 100% sessions, enabled)" when those resources were created. - -### {{STEP}}c. Install the session recording SDK - -Print "STATUS: Installing session recording SDK..." - -```bash -npm install @spotify-confidence/session-recording -# or: yarn add / pnpm add -``` - -### {{STEP}}d. Initialize the recorder - -Print "STATUS: Adding session recording provider..." - -Pick the env var the browser can actually read, then write the Frontend client secret to `.env` under that exact name (exposing it to the browser is intended). Use the same name in generated code and fill `` in the report: - -- Vite: `VITE_CONFIDENCE_CLIENT_SECRET` via `import.meta.env.VITE_CONFIDENCE_CLIENT_SECRET` -- Next.js client code: `NEXT_PUBLIC_CONFIDENCE_CLIENT_SECRET` via `process.env.NEXT_PUBLIC_CONFIDENCE_CLIENT_SECRET` -- Create React App: `REACT_APP_CONFIDENCE_CLIENT_SECRET` via `process.env.REACT_APP_CONFIDENCE_CLIENT_SECRET` -- Other browser bundlers: follow that framework's public-env convention -- Server-only entrypoints: `CONFIDENCE_CLIENT_SECRET` via `process.env.CONFIDENCE_CLIENT_SECRET` - -Ensure `.env` is in `.gitignore`, and never echo the secret in STATUS lines, the report, or generated source. - -Add to the app's entry point (e.g. `main.ts`, `index.tsx`, root layout): - -```ts -import { initSessionRecorder } from '@spotify-confidence/session-recording'; - -const recorder = initSessionRecorder({ - clientSecret: , - context: { - visitor_id: '', - }, -}); -``` - -Use the same field name as `targetingKeySelector`, filled with the identity from the **Targeting key** item in {{STEP}}b. Rename `visitor_id` in this snippet if the schema's first entity field is different. - -The function always returns a `SessionRecorder` — safe to call, never throws. - -**Start mode** — if {{STEP}}a found a consent tool, pass `mode: 'manual'` and call `recorder.start()` only after analytics or recording consent is granted. Fill `` with "Recording starts only after the user grants analytics or recording consent". If none was found, keep the SDK default (recording starts automatically) and fill `` with "No consent tool was found — recording starts when the app loads; mention session recording in the privacy policy and gate it behind consent where required (e.g. EU)". - -### {{STEP}}e. Configure privacy and capture settings - -Print "STATUS: Configuring privacy and capture settings..." - -Scan the project's components and templates to determine the right configuration. The available options are: - -**Privacy** (what to hide from recordings): - -- `maskInputs` (boolean, default `true`) — masks all ``, `