From 6da28b42bd5e3e8e4f650b7d56a772a403951385 Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Thu, 13 Aug 2026 13:53:12 +0200 Subject: [PATCH 01/10] fix(onboarding): clarify AI setup caption --- messages/en.json | 2 +- tests/app-onboarding-apikey-loading.unit.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/messages/en.json b/messages/en.json index 0882a5ee53..5c2428de2e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -379,7 +379,7 @@ "app-onboarding-appid-taken-pick-another": "App ID {appId} is already used. Pick another one or use one of the suggestions.", "app-onboarding-appid-taken-switched": "App ID {original} was already taken, so Capgo switched to {replacement}.", "app-onboarding-ai-help-button": "Copy AI instructions", - "app-onboarding-ai-help-caption": "Choose whether to include your API key before copying instructions for ChatGPT, Claude, Cursor, or another assistant.", + "app-onboarding-ai-help-caption": "Use your preferred AI assistant to help set up Capgo. Copy the onboarding instructions to get step-by-step guidance.", "app-onboarding-ai-help-copy-description": "Including it gives your assistant a ready-to-run command. Only include it with an assistant you trust.", "app-onboarding-ai-help-copy-title": "Include your API key?", "app-onboarding-ai-help-copy-with-key": "Copy with API key", diff --git a/tests/app-onboarding-apikey-loading.unit.test.ts b/tests/app-onboarding-apikey-loading.unit.test.ts index a24ce8e5e1..7bd81f5866 100644 --- a/tests/app-onboarding-apikey-loading.unit.test.ts +++ b/tests/app-onboarding-apikey-loading.unit.test.ts @@ -47,6 +47,7 @@ describe('app onboarding API key loading state', () => { it.concurrent('provides secure onboarding copy in the English locale', () => { expect(englishMessages['app-onboarding-command-apikey-loading']).toBe('Creating your secure API key…') + expect(englishMessages['app-onboarding-ai-help-caption']).toBe('Use your preferred AI assistant to help set up Capgo. Copy the onboarding instructions to get step-by-step guidance.') expect(englishMessages['app-onboarding-ai-help-without-key']).toContain('do not ask me to paste my real Capgo API key into this chat') expect(englishMessages['app-onboarding-ai-help-with-key']).toContain('do not repeat the API key in your response') expect(englishMessages['app-onboarding-ai-help-prompt']).toContain('3. Help me verify the installation succeeded.\n4. {apiKeyGuidance}') From 8399e54b45054f320218c70c128c35b6baebbe30 Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Thu, 13 Aug 2026 15:47:13 +0200 Subject: [PATCH 02/10] docs: design onboarding copy analytics --- ...-08-13-onboarding-copy-analytics-design.md | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-13-onboarding-copy-analytics-design.md diff --git a/docs/superpowers/specs/2026-08-13-onboarding-copy-analytics-design.md b/docs/superpowers/specs/2026-08-13-onboarding-copy-analytics-design.md new file mode 100644 index 0000000000..e1ec3a4fcb --- /dev/null +++ b/docs/superpowers/specs/2026-08-13-onboarding-copy-analytics-design.md @@ -0,0 +1,151 @@ +# Onboarding Copy Analytics Design + +## Goal + +Simplify AI-assisted onboarding by always copying instructions that contain the +real Capgo API key, and measure the two sensitive copy actions without exposing +the key, copied command, or AI prompt to analytics. + +The AI-instructions copy must reach both PostHog and Bento. The CLI-command copy +must reach PostHog only. Both PostHog events must remain associated with the +active browser session. + +## User Experience + +Remove the API-key choice dialog from `AppOnboardingFlow.vue`. Clicking **Copy +AI instructions** first ensures an API key exists and then immediately copies +the AI onboarding prompt with the real-key CLI command. + +If API-key provisioning fails, do not copy a redacted fallback. Keep the +existing error toast and stop. If clipboard writing fails, keep the existing +manual-copy dialog and do not emit a successful-copy analytics event. + +The existing CLI command remains clickable when its API key is ready. This is +the onboarding action described as copying the API key because the copied CLI +command contains that key; there is no separate API-key-only button in this +component. + +## Event Contract + +Add two canonical events: + +- `onboarding_ai_instructions_copied` +- `onboarding_cli_command_copied` + +Both frontend PostHog captures include safe, structured onboarding context: + +- `onboarding_session_id`: the active PostHog browser session ID; +- `onboarding_version`: the existing onboarding analytics version; +- `flow`: `pre_org` or `existing_org`; +- `app_id` and `org_id` when available; +- `intent` when selected; +- `existing_app` when known; +- `resumed`: whether the flow resumed a pending app; +- `setup_command`: `builder` or `ota`. + +Do not include the API key, CLI command, prompt, app name, email address, store +URL, or other free-form user input. + +Events fire only after `navigator.clipboard.writeText` succeeds. Tracking is +best-effort and must never make a successful copy look unsuccessful. + +## PostHog Session Preservation + +Capture both PostHog events directly in the browser with the existing +`pushEvent` service. This preserves PostHog's native browser session and normal +client context. + +Expose a small read-only helper from the PostHog service for obtaining the +active session ID. Attach it as `onboarding_session_id` to the structured event +properties and to the Bento-forwarding request. Local development may return no +session ID because the existing PostHog integration is intentionally disabled +there; event delivery remains best-effort. + +Do not rely on the server-side PostHog capture for the AI-copy event because it +would create a second event and would not naturally carry the browser SDK's +session context. + +## Bento Forwarding + +After the browser captures `onboarding_ai_instructions_copied`, send the same +event name and safe metadata through the existing authenticated +`/private/events` frontend helper. + +The backend recognizes this exact event as an allowlisted frontend-captured +event. It builds a server-owned Bento payload and sends it through the existing +Bento tracking path, while disabling the backend PostHog provider for this +request. LogSnag may continue to receive the backend event. + +Implement backend PostHog suppression as an internal option on the shared +tracking dispatcher. The client cannot select arbitrary providers: the +`/private/events` route applies the option only after matching the exact +allowlisted event name. All other event behavior remains unchanged. + +The Bento event is `app:ai_instructions_copied`, uses the onboarding email +preference, and includes the verified organization, app, session ID, and safe +onboarding context. The backend verifies the caller's organization/app access +using the existing tracking-v2 flow before constructing the Bento payload. + +Use a per-session uniqueness key so repeated retries or double delivery do not +duplicate the Bento signal for the same onboarding session and app. + +`onboarding_cli_command_copied` never calls `/private/events`; it remains +PostHog-only. + +## Implementation Shape + +Change `copyText` to return whether clipboard writing succeeded. On success: + +1. `copyCliCommand` emits `onboarding_cli_command_copied` directly to PostHog. +2. `copyAiInstructions` emits `onboarding_ai_instructions_copied` directly to + PostHog, then sends the allowlisted backend event asynchronously for Bento. + +Remove the redacted CLI-command computed value, the conditional API-key prompt +guidance, and the dialog-specific translations and tests. Keep the prompt's +with-key safety guidance, rewritten as unconditional copy. + +Centralize the safe event properties in one helper inside the onboarding +component or its existing analytics helper so both copy actions use identical +session and flow context. + +## Failure And Security Boundaries + +- API-key provisioning failure stops AI-instruction copying and tracking. +- Clipboard failure emits neither successful-copy event. +- PostHog or Bento tracking failure never blocks or reverses a successful copy. +- The Bento request is fire-and-forget from the UI. +- The backend derives organization authorization and Bento routing; the browser + cannot provide an arbitrary Bento event or disable providers for other events. +- Analytics payloads must never serialize `apiKey`, `cliCommand`, or the prompt. + +## Testing + +Frontend tests verify: + +- AI copying no longer opens the API-key choice dialog; +- the real-key command is always used after successful key loading; +- key-loading and clipboard failures emit no success events; +- both copy actions use the same safe session/context builder; +- the AI event calls browser PostHog and `/private/events` once; +- the CLI-command event calls browser PostHog only; +- no analytics payload contains the key, command, or prompt. + +Backend tests verify: + +- the allowlisted AI-copy event builds the expected Bento payload; +- backend PostHog is skipped only for that exact event; +- the event still uses tracking-v2 organization/app authorization; +- arbitrary events cannot request provider suppression or Bento forwarding; +- the session uniqueness key produces one Bento signal per app/session. + +Run frontend and backend lint, focused unit tests, TypeScript type checking, and +the production frontend build. Keep the production-mode frontend server alive +for browser verification throughout implementation. + +## Scope + +This change removes the copy-choice dialog, always includes the API key in AI +instructions, adds the two copy events, and adds the minimum backend allowlist +needed for Bento delivery without duplicate PostHog capture. It does not change +API-key generation, other API-key pages, onboarding navigation, database schema, +or unrelated tracking events. From 15d6c0f9f68dd28d49244bd22101a88190f92306 Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Thu, 13 Aug 2026 16:27:39 +0200 Subject: [PATCH 03/10] docs: plan onboarding copy analytics --- .../2026-08-13-onboarding-copy-analytics.md | 609 ++++++++++++++++++ ...-08-13-onboarding-copy-analytics-design.md | 20 +- 2 files changed, 618 insertions(+), 11 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-13-onboarding-copy-analytics.md diff --git a/docs/superpowers/plans/2026-08-13-onboarding-copy-analytics.md b/docs/superpowers/plans/2026-08-13-onboarding-copy-analytics.md new file mode 100644 index 0000000000..0e9dcbf5dc --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-onboarding-copy-analytics.md @@ -0,0 +1,609 @@ +# Onboarding Copy Analytics Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Always include the API key in copied AI onboarding instructions and record successful AI/CLI copy actions in the correct PostHog and Bento destinations without leaking sensitive content or losing onboarding-session correlation. + +**Architecture:** The existing onboarding progress tracker will own browser PostHog capture and return its safe `onboarding_attempt_id` context. `AppOnboardingFlow.vue` will emit events only after clipboard success, sending the AI-copy event through `/private/events` for Bento in addition to direct browser PostHog. The backend will recognize that exact event, construct a server-owned Bento payload, and use an internal provider option to skip duplicate server-side PostHog capture. + +**Tech Stack:** Vue 3 Composition API, TypeScript, PostHog browser SDK, Hono, Supabase tracking utilities, Bento, Vitest, Bun. + +--- + +### Task 1: Add safe onboarding copy-event context + +**Files:** +- Modify: `src/utils/onboardingProgressAnalytics.ts` +- Modify: `tests/onboarding-progress-analytics.unit.test.ts` + +- [ ] **Step 1: Write the failing tracker test** + +Add a test that initializes the tracker, views `setup`, calls the new copy-event +method, and verifies browser capture plus returned properties share the same +attempt ID: + +```ts +it('captures copy events with the active onboarding attempt context', () => { + const capture = vi.fn() + const tracker = createOnboardingProgressTracker({ + capture, + flow: 'pre_org', + resumed: true, + steps: ['intent', 'details', 'organization', 'setup'], + supaHost: 'https://api.capgo.app', + }) + tracker.viewStep('setup') + capture.mockClear() + + const properties = tracker.trackCopyEvent('onboarding_ai_instructions_copied', { + app_id: 'com.example.app', + existing_app: true, + intent: 'ota', + org_id: 'org-id', + setup_command: 'ota', + }) + + expect(capture).toHaveBeenCalledWith( + 'onboarding_ai_instructions_copied', + 'https://api.capgo.app', + expect.objectContaining({ + app_id: 'com.example.app', + existing_app: true, + flow: 'pre_org', + intent: 'ota', + onboarding_attempt_id: expect.any(String), + onboarding_version: ONBOARDING_ANALYTICS_VERSION, + org_id: 'org-id', + resumed: true, + setup_command: 'ota', + step: 'setup', + }), + ) + expect(properties).toEqual(capture.mock.calls[0]?.[2]) +}) +``` + +- [ ] **Step 2: Run the focused test and verify it fails** + +Run: + +```bash +bun test tests/onboarding-progress-analytics.unit.test.ts +``` + +Expected: FAIL because `trackCopyEvent` does not exist. + +- [ ] **Step 3: Implement the typed copy-event method** + +In `src/utils/onboardingProgressAnalytics.ts`, add narrow event/context types and +return the captured safe properties: + +```ts +export type OnboardingCopyEvent + = 'onboarding_ai_instructions_copied' + | 'onboarding_cli_command_copied' + +export interface OnboardingCopyEventProperties { + app_id?: string + existing_app?: boolean + intent?: OnboardingIntent + org_id?: string + setup_command: 'builder' | 'ota' +} + +function trackCopyEvent(name: OnboardingCopyEvent, details: OnboardingCopyEventProperties) { + if (!activeStep) + return null + + const properties = sharedProperties(activeStep) + if (!properties) + return null + + const eventProperties: AnalyticsProperties = { + ...properties, + setup_command: details.setup_command, + } + if (details.app_id) + eventProperties.app_id = details.app_id + if (details.existing_app !== undefined) + eventProperties.existing_app = details.existing_app + if (details.intent) + eventProperties.intent = details.intent + if (details.org_id) + eventProperties.org_id = details.org_id + safelyCapture(name, eventProperties) + return eventProperties +} +``` + +Remove `null` from the private `AnalyticsPrimitive` union because these event +builders omit unavailable properties rather than serializing null. This keeps +the returned event context assignable to the frontend tracking helper's `Tags` +type without a cast. + +Expose `trackCopyEvent` in the tracker's returned object. Do not accept commands, +prompts, keys, names, emails, URLs, or arbitrary property records. + +- [ ] **Step 4: Run the focused tracker tests** + +Run: + +```bash +bun test tests/onboarding-progress-analytics.unit.test.ts +``` + +Expected: PASS. + +- [ ] **Step 5: Commit the tracker change** + +```bash +git add src/utils/onboardingProgressAnalytics.ts tests/onboarding-progress-analytics.unit.test.ts +git commit -m "feat(analytics): add onboarding copy event context" +``` + +### Task 2: Remove the dialog and track successful frontend copies + +**Files:** +- Modify: `src/components/dashboard/AppOnboardingFlow.vue` +- Modify: `src/services/tracking.ts` +- Modify: `messages/en.json` +- Modify: `messages/en.context.json` +- Modify: `tests/app-onboarding-apikey-loading.unit.test.ts` + +- [ ] **Step 1: Replace the dialog test with failing direct-copy assertions** + +Update the final test in `tests/app-onboarding-apikey-loading.unit.test.ts` to +assert the direct behavior: + +```ts +it.concurrent('always includes the API key and tracks successful copy actions', () => { + const copyHandlerStart = onboardingSource.indexOf('async function copyAiInstructions()') + const copyHandlerEnd = onboardingSource.indexOf('function goToInstallStep()', copyHandlerStart) + const copyHandler = onboardingSource.slice(copyHandlerStart, copyHandlerEnd) + + expect(copyHandler).toContain('await loadApiKey()') + expect(copyHandler).toContain('if (!apiKey.value)') + expect(copyHandler).toContain('await copyText(createAiHelpPrompt())') + expect(copyHandler).toContain("trackSuccessfulCopy('onboarding_ai_instructions_copied')") + expect(copyHandler).not.toContain('dialogStore.openDialog({') + expect(copyHandler).not.toContain('redactedCliCommand') + expect(onboardingSource).toContain("trackSuccessfulCopy('onboarding_cli_command_copied')") +}) +``` + +Update the English-copy assertions so they require unconditional with-key +guidance and verify the four dialog keys plus the without-key guidance are gone. + +- [ ] **Step 2: Run the focused onboarding test and verify it fails** + +Run: + +```bash +bun test tests/app-onboarding-apikey-loading.unit.test.ts +``` + +Expected: FAIL because the dialog and redacted branch still exist. + +- [ ] **Step 3: Allow safe backend event-only properties in the frontend helper** + +Extend `TrackOptions` in `src/services/tracking.ts` with the already-supported +backend field: + +```ts + nonPersonTags?: Tags +``` + +This keeps volatile onboarding context out of PostHog person properties if a +future backend provider records it. Do not add provider-selection fields to the +client contract. + +- [ ] **Step 4: Implement success-aware clipboard handling and copy tracking** + +In `AppOnboardingFlow.vue`: + +1. Make `copyText` return `true` after `writeText` succeeds and `false` after the + fallback dialog is dismissed. +2. Delete `redactedCliCommand`. +3. Change `createAiHelpPrompt` to take no command argument and always use + `cliCommand.value` with the with-key guidance. +4. Add a `trackSuccessfulCopy` helper that calls + `progressTracker?.trackCopyEvent(...)` with only safe values: + +```ts +function trackSuccessfulCopy(event: OnboardingCopyEvent) { + const orgId = currentOrg.value?.gid + const appId = createdApp.value?.app_id || generatedAppId.value || undefined + const properties = progressTracker?.trackCopyEvent(event, { + ...(appId ? { app_id: appId } : {}), + ...(existingApp.value !== null ? { existing_app: existingApp.value } : {}), + ...(selectedIntent.value ? { intent: selectedIntent.value } : {}), + ...(orgId ? { org_id: orgId } : {}), + setup_command: usesBuilderSetupCommand.value ? 'builder' : 'ota', + }) + + if (event !== 'onboarding_ai_instructions_copied' || !properties || !orgId || !appId) + return + + sendEvent({ + channel: 'onboarding', + event, + icon: '🤖', + nonPersonTags: properties, + notify: false, + org_id: orgId, + tags: { app_id: appId }, + tracking_version: 2, + }).catch() +} +``` + +Keep `app_id` in `tags` because the backend's existing authorization path reads +it there. Put the full safe event context in `nonPersonTags` so it remains event +metadata, not PostHog person state. + +5. Track `onboarding_cli_command_copied` only after `copyText(cliCommand.value)` + returns `true`. +6. In `copyAiInstructions`, return immediately after key-loading failure or an + empty key, copy `createAiHelpPrompt()` directly, and track + `onboarding_ai_instructions_copied` only after success. + +- [ ] **Step 5: Remove obsolete translations** + +Delete these keys from both `messages/en.json` and `messages/en.context.json`: + +```text +app-onboarding-ai-help-copy-description +app-onboarding-ai-help-copy-title +app-onboarding-ai-help-copy-with-key +app-onboarding-ai-help-copy-without-key +``` + +Delete `app-onboarding-ai-help-without-key`. Keep +`app-onboarding-ai-help-with-key` as the unconditional prompt guidance. + +- [ ] **Step 6: Run the focused frontend tests** + +Run: + +```bash +bun test tests/app-onboarding-apikey-loading.unit.test.ts tests/onboarding-progress-analytics.unit.test.ts +``` + +Expected: PASS. + +- [ ] **Step 7: Commit the frontend behavior** + +```bash +git add src/components/dashboard/AppOnboardingFlow.vue src/services/tracking.ts messages/en.json messages/en.context.json tests/app-onboarding-apikey-loading.unit.test.ts +git commit -m "feat(onboarding): copy AI instructions with API key" +``` + +### Task 3: Support internal PostHog provider suppression + +**Files:** +- Modify: `supabase/functions/_backend/utils/tracking.ts` +- Modify: `tests/tracking.unit.test.ts` + +- [ ] **Step 1: Write the failing provider-selection test** + +Add a test proving `posthog: false` skips only PostHog: + +```ts +it('can skip PostHog while preserving LogSnag and Bento delivery', async () => { + const { sendEventToTracking } = await import('../supabase/functions/_backend/utils/tracking.ts') + + await sendEventToTracking(createContext(), { + bento: { + data: { app_id: 'com.example.app' }, + event: 'app:ai_instructions_copied', + preferenceKey: 'onboarding', + uniqId: 'app:ai_instructions_copied:com.example.app:attempt-id', + }, + channel: 'onboarding', + event: 'onboarding_ai_instructions_copied', + notify: false, + sentToBento: true, + user_id: 'org-id', + }, { background: false, posthog: false }) + + expect(logsnagTrackMock).toHaveBeenCalledOnce() + expect(posthogMock).not.toHaveBeenCalled() + expect(notifToOrgMembersMock).toHaveBeenCalledOnce() +}) +``` + +- [ ] **Step 2: Run the tracking test and verify it fails** + +Run: + +```bash +bun test tests/tracking.unit.test.ts +``` + +Expected: FAIL because `SendEventToTrackingOptions` has no `posthog` option and +the provider still runs. + +- [ ] **Step 3: Implement the internal option** + +Add the option: + +```ts +export interface SendEventToTrackingOptions { + background?: boolean + ip?: string + posthog?: boolean + strict?: boolean +} +``` + +Build the tracking task list with LogSnag always present and PostHog added only +when `options.posthog !== false`. Do not change Bento execution or defaults. + +- [ ] **Step 4: Run the focused tracking test** + +Run: + +```bash +bun test tests/tracking.unit.test.ts +``` + +Expected: PASS, including all existing provider tests. + +- [ ] **Step 5: Commit provider selection** + +```bash +git add supabase/functions/_backend/utils/tracking.ts tests/tracking.unit.test.ts +git commit -m "feat(tracking): support internal PostHog suppression" +``` + +### Task 4: Allowlist AI-copy forwarding to Bento + +**Files:** +- Create: `supabase/functions/_backend/utils/onboarding_copy_tracking.ts` +- Create: `tests/onboarding-copy-tracking.unit.test.ts` +- Modify: `supabase/functions/_backend/private/events.ts` +- Modify: `tests/events.test.ts` + +- [ ] **Step 1: Write failing pure Bento-mapping tests** + +Create `tests/onboarding-copy-tracking.unit.test.ts` with tests that require the +exact event, verified org/app IDs, and a non-empty onboarding attempt ID: + +```ts +import { describe, expect, it } from 'vitest' +import { + AI_INSTRUCTIONS_COPIED_EVENT, + buildAiInstructionsCopiedBentoEvent, + isFrontendPosthogCapturedEvent, +} from '../supabase/functions/_backend/utils/onboarding_copy_tracking.ts' + +describe('onboarding copy tracking', () => { + it('builds a per-attempt Bento event for the allowlisted AI copy', () => { + const attemptId = '7e64f484-4171-47b6-86f7-0ef5d49e0ef8' + expect(buildAiInstructionsCopiedBentoEvent({ + appId: 'com.example.app', + event: AI_INSTRUCTIONS_COPIED_EVENT, + nonPersonTags: { + flow: 'pre_org', + onboarding_attempt_id: attemptId, + onboarding_version: 2, + resumed: false, + setup_command: 'ota', + }, + orgId: 'org-id', + })).toEqual(expect.objectContaining({ + event: 'app:ai_instructions_copied', + once: true, + preferenceKey: 'onboarding', + uniqId: `app:ai_instructions_copied:com.example.app:${attemptId}`, + })) + }) + + it('rejects arbitrary or incomplete events', () => { + expect(buildAiInstructionsCopiedBentoEvent({ + appId: 'com.example.app', + event: 'arbitrary_event', + nonPersonTags: { onboarding_attempt_id: '7e64f484-4171-47b6-86f7-0ef5d49e0ef8' }, + orgId: 'org-id', + })).toBeUndefined() + expect(buildAiInstructionsCopiedBentoEvent({ + appId: 'com.example.app', + event: AI_INSTRUCTIONS_COPIED_EVENT, + nonPersonTags: {}, + orgId: 'org-id', + })).toBeUndefined() + expect(isFrontendPosthogCapturedEvent('arbitrary_event')).toBe(false) + }) +}) +``` + +- [ ] **Step 2: Run the new test and verify it fails** + +Run: + +```bash +bun test tests/onboarding-copy-tracking.unit.test.ts +``` + +Expected: FAIL because the utility does not exist. + +- [ ] **Step 3: Implement the pure allowlist and Bento builder** + +Create the backend utility with the exact constant, safe property selection, and +`once: true` uniqueness: + +```ts +import type { BentoTrackingPayload } from './tracking.ts' + +export const AI_INSTRUCTIONS_COPIED_EVENT = 'onboarding_ai_instructions_copied' + +interface AiInstructionsCopiedInput { + appId?: string + event: string + nonPersonTags?: Record + orgId?: string +} + +export function isFrontendPosthogCapturedEvent(event: string) { + return event === AI_INSTRUCTIONS_COPIED_EVENT +} + +export function buildAiInstructionsCopiedBentoEvent(input: AiInstructionsCopiedInput): BentoTrackingPayload | undefined { + const attemptId = input.nonPersonTags?.onboarding_attempt_id + if (!isFrontendPosthogCapturedEvent(input.event) || !input.orgId || !input.appId || typeof attemptId !== 'string' || !/^[0-9a-f-]{36}$/i.test(attemptId)) + return undefined + + const context = input.nonPersonTags ?? {} + return { + data: { + app_id: input.appId, + onboarding_attempt_id: attemptId, + org_id: input.orgId, + ...(typeof context.existing_app === 'boolean' ? { existing_app: context.existing_app } : {}), + ...(context.flow === 'pre_org' || context.flow === 'existing_org' ? { flow: context.flow } : {}), + ...(context.intent === 'ota' || context.intent === 'builder' || context.intent === 'both' || context.intent === 'exploring' ? { intent: context.intent } : {}), + ...(typeof context.onboarding_version === 'number' ? { onboarding_version: context.onboarding_version } : {}), + ...(typeof context.resumed === 'boolean' ? { resumed: context.resumed } : {}), + ...(context.setup_command === 'builder' || context.setup_command === 'ota' ? { setup_command: context.setup_command } : {}), + }, + event: 'app:ai_instructions_copied', + once: true, + preferenceKey: 'onboarding', + uniqId: `app:ai_instructions_copied:${input.appId}:${attemptId}`, + } +} +``` + +- [ ] **Step 4: Wire the builder into `/private/events`** + +Import the utility, build `aiInstructionsCopiedBentoEvent` from the verified +organization/app IDs plus `trackedBody.nonPersonTags`, append it to the existing +Bento selection, and call the dispatcher with an internal option: + +```ts +const aiInstructionsCopiedBentoEvent = buildAiInstructionsCopiedBentoEvent({ + appId, + event: trackedBody.event, + nonPersonTags: body.nonPersonTags, + orgId: onboardingOrgId, +}) +const bentoEvent = onboardingBentoEvent + ?? builderBentoEvent + ?? bundleIncompatibleBentoEvent + ?? aiInstructionsCopiedBentoEvent + +await sendEventToTracking(c, addAuthenticatedApiKeyIdToTrackingPayload({ + ...trackedBody, + bento: bentoEvent, + sentToBento: Boolean(bentoEvent), + groups: verifiedOrgId ? { organization: verifiedOrgId } : undefined, +}, apikeyId), { + posthog: !isFrontendPosthogCapturedEvent(trackedBody.event), +}) +``` + +The route—not the request body—owns the provider decision. + +- [ ] **Step 5: Add an authenticated endpoint regression case** + +In `tests/events.test.ts`, add a tracking-v2 request using the test user's own +app/org, the exact event, `tags.app_id`, and +`nonPersonTags.onboarding_attempt_id`. Assert HTTP 200 and `{ status: 'ok' }`. +This proves the existing permission path accepts the new frontend payload. + +- [ ] **Step 6: Run focused backend tests** + +Run: + +```bash +bun test tests/onboarding-copy-tracking.unit.test.ts tests/tracking.unit.test.ts +``` + +Expected: PASS. + +If local Supabase is running, also run: + +```bash +bun test tests/events.test.ts +``` + +Expected: PASS. + +- [ ] **Step 7: Commit the Bento allowlist** + +```bash +git add supabase/functions/_backend/utils/onboarding_copy_tracking.ts supabase/functions/_backend/private/events.ts tests/onboarding-copy-tracking.unit.test.ts tests/events.test.ts +git commit -m "feat(onboarding): forward AI copy event to Bento" +``` + +### Task 5: Verify the complete change and publish the PR + +**Files:** +- Verify: all files changed in Tasks 1-4 +- Preserve: `codedb.snapshot` +- Preserve: unrelated generated changes in `src/components.d.ts` + +- [ ] **Step 1: Run formatting and lint** + +Run: + +```bash +bun lint +bun lint:backend +``` + +Expected: both commands exit 0. + +- [ ] **Step 2: Run focused and full unit tests** + +Run: + +```bash +bun test tests/app-onboarding-apikey-loading.unit.test.ts tests/onboarding-progress-analytics.unit.test.ts tests/onboarding-copy-tracking.unit.test.ts tests/tracking.unit.test.ts +bun test:unit +``` + +Expected: all tests pass. + +- [ ] **Step 3: Run type checking and production build** + +Run: + +```bash +bun typecheck +bun build +``` + +Expected: both commands exit 0. + +- [ ] **Step 4: Verify the live production frontend** + +Using the already-running `bun run serve:prod-no-cors` server at +`http://127.0.0.1:5175/`, confirm: + +- **Copy AI instructions** writes immediately without opening the choice dialog; +- copied instructions contain the real API-key command; +- the existing clipboard success toast appears; +- the CLI-command copy still works; +- no regression appears in setup-step layout or navigation. + +- [ ] **Step 5: Clean generated-only changes and inspect the diff** + +Remove only server/build-generated additions from `src/components.d.ts` with an +explicit patch. Do not modify or stage `codedb.snapshot`. Run: + +```bash +git status --short +git diff --check +git diff --stat origin/main...HEAD +``` + +Expected: only intentional implementation, tests, and design/plan files remain; +`git diff --check` exits 0. + +- [ ] **Step 6: Run the mandatory PR-ready workflow** + +Load and follow `.agents/skills/pr-ready/SKILL.md`. Resolve all relevant local +and remote failures, push the branch, update the PR title/body to describe the +behavior and analytics contract, and observe stable green twice at least five +minutes apart before handing off. diff --git a/docs/superpowers/specs/2026-08-13-onboarding-copy-analytics-design.md b/docs/superpowers/specs/2026-08-13-onboarding-copy-analytics-design.md index e1ec3a4fcb..3a31050df5 100644 --- a/docs/superpowers/specs/2026-08-13-onboarding-copy-analytics-design.md +++ b/docs/superpowers/specs/2026-08-13-onboarding-copy-analytics-design.md @@ -34,7 +34,8 @@ Add two canonical events: Both frontend PostHog captures include safe, structured onboarding context: -- `onboarding_session_id`: the active PostHog browser session ID; +- `onboarding_attempt_id`: the existing ID generated by the onboarding progress + tracker for this run through the flow; - `onboarding_version`: the existing onboarding analytics version; - `flow`: `pre_org` or `existing_org`; - `app_id` and `org_id` when available; @@ -53,13 +54,9 @@ best-effort and must never make a successful copy look unsuccessful. Capture both PostHog events directly in the browser with the existing `pushEvent` service. This preserves PostHog's native browser session and normal -client context. - -Expose a small read-only helper from the PostHog service for obtaining the -active session ID. Attach it as `onboarding_session_id` to the structured event -properties and to the Bento-forwarding request. Local development may return no -session ID because the existing PostHog integration is intentionally disabled -there; event delivery remains best-effort. +client context. Reuse the `onboarding_attempt_id` already generated by +`createOnboardingProgressTracker` as the explicit onboarding-flow correlation +property, and include it in the Bento-forwarding request. Do not rely on the server-side PostHog capture for the AI-copy event because it would create a second event and would not naturally carry the browser SDK's @@ -82,9 +79,10 @@ tracking dispatcher. The client cannot select arbitrary providers: the allowlisted event name. All other event behavior remains unchanged. The Bento event is `app:ai_instructions_copied`, uses the onboarding email -preference, and includes the verified organization, app, session ID, and safe -onboarding context. The backend verifies the caller's organization/app access -using the existing tracking-v2 flow before constructing the Bento payload. +preference, and includes the verified organization, app, onboarding attempt ID, +and safe onboarding context. The backend verifies the caller's +organization/app access using the existing tracking-v2 flow before constructing +the Bento payload. Use a per-session uniqueness key so repeated retries or double delivery do not duplicate the Bento signal for the same onboarding session and app. From e61228d19a86996b2b787178616389c2dc45c19c Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Thu, 13 Aug 2026 16:29:53 +0200 Subject: [PATCH 04/10] feat(analytics): add onboarding copy event context --- src/utils/onboardingProgressAnalytics.ts | 37 +++++++++++++++++- ...onboarding-progress-analytics.unit.test.ts | 39 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/utils/onboardingProgressAnalytics.ts b/src/utils/onboardingProgressAnalytics.ts index 6efc295e5d..f38dd16f30 100644 --- a/src/utils/onboardingProgressAnalytics.ts +++ b/src/utils/onboardingProgressAnalytics.ts @@ -4,6 +4,7 @@ export const ONBOARDING_ANALYTICS_VERSION = 2 export type OnboardingAnalyticsFlow = 'pre_org' | 'existing_org' export type OnboardingAnalyticsStep = 'intent' | 'details' | 'organization' | 'choice' | 'install' | 'setup' +export type OnboardingCopyEvent = 'onboarding_ai_instructions_copied' | 'onboarding_cli_command_copied' export type OnboardingIntent = 'ota' | 'builder' | 'both' | 'exploring' export type OnboardingDetailsEvent = | 'onboarding_app_id_entered' @@ -22,7 +23,7 @@ export type OnboardingDetailsEvent | 'onboarding_store_import_succeeded' | 'onboarding_store_url_entered' -type AnalyticsPrimitive = string | number | boolean | null +type AnalyticsPrimitive = string | number | boolean type AnalyticsProperties = Record type CaptureEvent = (name: string, supaHost: string, properties?: AnalyticsProperties) => void @@ -38,6 +39,14 @@ export interface OnboardingDetailsEventProperties { icon_source?: 'file' | 'store' } +export interface OnboardingCopyEventProperties { + app_id?: string + existing_app?: boolean + intent?: OnboardingIntent + org_id?: string + setup_command: 'builder' | 'ota' +} + export type OnboardingDetailsField = 'app_id' | 'app_name' | 'store_url' export function createOnboardingDetailsFieldDebouncer( @@ -172,8 +181,34 @@ export function createOnboardingProgressTracker(options: CreateOnboardingProgres safelyCapture(name, { ...properties, ...details }) } + function trackCopyEvent(name: OnboardingCopyEvent, details: OnboardingCopyEventProperties) { + if (!activeStep) + return null + + const properties = sharedProperties(activeStep) + if (!properties) + return null + + const eventProperties: AnalyticsProperties = { + ...properties, + setup_command: details.setup_command, + } + if (details.app_id) + eventProperties.app_id = details.app_id + if (details.existing_app !== undefined) + eventProperties.existing_app = details.existing_app + if (details.intent) + eventProperties.intent = details.intent + if (details.org_id) + eventProperties.org_id = details.org_id + + safelyCapture(name, eventProperties) + return eventProperties + } + return { completeStep, + trackCopyEvent, trackDetailsEvent, viewStep, } diff --git a/tests/onboarding-progress-analytics.unit.test.ts b/tests/onboarding-progress-analytics.unit.test.ts index 3a17d72378..a1424d291f 100644 --- a/tests/onboarding-progress-analytics.unit.test.ts +++ b/tests/onboarding-progress-analytics.unit.test.ts @@ -137,6 +137,45 @@ describe('onboarding progress analytics', () => { ) }) + it.concurrent('captures copy events with the active onboarding attempt context', () => { + const capture = vi.fn() + const tracker = createOnboardingProgressTracker({ + capture, + flow: 'pre_org', + resumed: true, + steps, + supaHost: 'https://supabase.capgo.test', + }) + tracker.viewStep('setup') + capture.mockClear() + + const properties = tracker.trackCopyEvent('onboarding_ai_instructions_copied', { + app_id: 'com.example.app', + existing_app: true, + intent: 'ota', + org_id: 'org-id', + setup_command: 'ota', + }) + + expect(capture).toHaveBeenCalledWith( + 'onboarding_ai_instructions_copied', + 'https://supabase.capgo.test', + expect.objectContaining({ + app_id: 'com.example.app', + existing_app: true, + flow: 'pre_org', + intent: 'ota', + onboarding_attempt_id: expect.any(String), + onboarding_version: ONBOARDING_ANALYTICS_VERSION, + org_id: 'org-id', + resumed: true, + setup_command: 'ota', + step: 'setup', + }), + ) + expect(properties).toEqual(capture.mock.calls[0]?.[2]) + }) + it.concurrent('deduplicates completion for one visit and resets timing after back navigation', () => { let now = 10 const capture = vi.fn() From b7b91e57d2bda1da36f326e4e736e8a70a24881e Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Thu, 13 Aug 2026 16:31:42 +0200 Subject: [PATCH 05/10] feat(onboarding): copy AI instructions with API key --- messages/en.context.json | 5 -- messages/en.json | 5 -- .../dashboard/AppOnboardingFlow.vue | 74 +++++++++++-------- src/services/tracking.ts | 4 + ...app-onboarding-apikey-loading.unit.test.ts | 25 ++++--- 5 files changed, 62 insertions(+), 51 deletions(-) diff --git a/messages/en.context.json b/messages/en.context.json index 2a6546f3c5..ef0e1c345a 100644 --- a/messages/en.context.json +++ b/messages/en.context.json @@ -388,13 +388,8 @@ "app-not-found-description": "Used in Capgo web console areas: components, components/dashboard, pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "app-onboarding-ai-help-button": "Used in Capgo web console areas: components/dashboard. Role: button or action label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "app-onboarding-ai-help-caption": "Used in Capgo web console areas: components/dashboard. Role: UI sentence. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", - "app-onboarding-ai-help-copy-description": "Used in Capgo web console areas: components/dashboard. Role: dialog explanatory text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", - "app-onboarding-ai-help-copy-title": "Used in Capgo web console areas: components/dashboard. Role: dialog title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", - "app-onboarding-ai-help-copy-with-key": "Used in Capgo web console areas: components/dashboard. Role: button or action label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", - "app-onboarding-ai-help-copy-without-key": "Used in Capgo web console areas: components/dashboard. Role: button or action label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "app-onboarding-ai-help-prompt": "Used in Capgo web console areas: components/dashboard. Role: UI sentence. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "app-onboarding-ai-help-with-key": "Used in Capgo web console areas: components/dashboard. Role: AI prompt instruction confirming the user intentionally included their API key. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", - "app-onboarding-ai-help-without-key": "Used in Capgo web console areas: components/dashboard. Role: AI prompt instruction for safely replacing a redacted API key. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "app-onboarding-ai-help-status-existing": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "app-onboarding-ai-help-status-new": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "app-onboarding-ai-help-title": "Used in Capgo web console areas: components/dashboard. Role: section or dialog title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", diff --git a/messages/en.json b/messages/en.json index 5c2428de2e..df9469f8b7 100644 --- a/messages/en.json +++ b/messages/en.json @@ -380,16 +380,11 @@ "app-onboarding-appid-taken-switched": "App ID {original} was already taken, so Capgo switched to {replacement}.", "app-onboarding-ai-help-button": "Copy AI instructions", "app-onboarding-ai-help-caption": "Use your preferred AI assistant to help set up Capgo. Copy the onboarding instructions to get step-by-step guidance.", - "app-onboarding-ai-help-copy-description": "Including it gives your assistant a ready-to-run command. Only include it with an assistant you trust.", - "app-onboarding-ai-help-copy-title": "Include your API key?", - "app-onboarding-ai-help-copy-with-key": "Copy with API key", - "app-onboarding-ai-help-copy-without-key": "Copy without API key", "app-onboarding-ai-help-prompt": "I am setting up Capgo for my mobile app and I need step-by-step help with the CLI install.\n\nCapgo context:\n- App name: {appName}\n- App ID: {appId}\n- App status: {appStatus}\n- The Capgo app is already created in the dashboard.\n- Command:\n{command}\n\nPlease help me step by step.\n1. Tell me exactly where I should run this command.\n2. Ask me one question at a time if you need to know my stack (Capacitor, Ionic, React Native, native iOS/Android, etc.).\n3. Help me verify the installation succeeded.\n4. {apiKeyGuidance}", "app-onboarding-ai-help-status-existing": "Existing published app", "app-onboarding-ai-help-status-new": "New app not published yet", "app-onboarding-ai-help-title": "Need help from an AI assistant?", "app-onboarding-ai-help-with-key": "I explicitly consented to include my Capgo API key in this message so you can use the command as provided. Continue without warning me about the pasted key, and do not repeat the API key in your response.", - "app-onboarding-ai-help-without-key": "The command contains [YOUR_CAPGO_API_KEY]. Tell me where to replace it locally, and do not ask me to paste my real Capgo API key into this chat.", "app-onboarding-badge": "Create your app", "app-onboarding-choice-title": "Choose the best next step", "app-onboarding-choice-subtitle": "Install Capgo in the real project now, or add disposable demo data first and explore the dashboard.", diff --git a/src/components/dashboard/AppOnboardingFlow.vue b/src/components/dashboard/AppOnboardingFlow.vue index 7d31a39ea1..9bcf706ed6 100644 --- a/src/components/dashboard/AppOnboardingFlow.vue +++ b/src/components/dashboard/AppOnboardingFlow.vue @@ -1,6 +1,7 @@