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..8dedaef8db --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-onboarding-copy-analytics.md @@ -0,0 +1,616 @@ +# 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 + + void 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. + +1. Track `onboarding_cli_command_copied` only after `copyText(cliCommand.value)` + returns `true`. +2. 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 +} + +const onboardingAttemptIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + +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' + || !onboardingAttemptIdPattern.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 new file mode 100644 index 0000000000..3a31050df5 --- /dev/null +++ b/docs/superpowers/specs/2026-08-13-onboarding-copy-analytics-design.md @@ -0,0 +1,149 @@ +# 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_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; +- `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. 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 +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, 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. + +`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. diff --git a/messages/en.context.json b/messages/en.context.json index de3c079ea1..e6ef235495 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 4d9afee62b..d68e741cd8 100644 --- a/messages/en.json +++ b/messages/en.json @@ -379,17 +379,12 @@ "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-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-caption": "Let your AI assistant guide you through setting up Capgo. Copy the onboarding instructions to get started.", "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 75d2aa39e7..e88f8966b7 100644 --- a/src/components/dashboard/AppOnboardingFlow.vue +++ b/src/components/dashboard/AppOnboardingFlow.vue @@ -1,6 +1,7 @@