diff --git a/src/lib/api.ts b/src/lib/api.ts index ba478a829..ba51917af 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -261,6 +261,42 @@ export async function fetchSlackConnected( return parsed.data.results.some((i) => i.kind === 'slack'); } +/** Minimal shape of `/api/projects/:id/event_definitions/` — we only need + * to know whether any row came back, so results stay opaque. */ +const EventDefinitionsResponseSchema = z.object({ + results: z.array(z.unknown()), +}); + +/** + * Check whether the project has any custom event definitions — events the + * user's own code captures, as opposed to PostHog's built-in `$`-prefixed + * ones (`$pageview`, `$autocapture`, ...). Uses the same + * `event_type=event_custom` server-side filter as the PostHog UI's "custom + * events" view, so "none" here means exactly what the user sees there. + * Requires the `event_definition:read` scope. Throws on failure — callers + * decide how to degrade (self-driving's events check fails open and skips + * its proposal). + */ +export async function fetchHasCustomEvents( + accessToken: string, + projectId: number, + baseUrl: string, + signal?: AbortSignal, +): Promise { + const response = await axios.get( + `${baseUrl}/api/projects/${projectId}/event_definitions/`, + { + params: { event_type: 'event_custom', limit: 1 }, + headers: { + Authorization: `Bearer ${accessToken}`, + 'User-Agent': WIZARD_USER_AGENT, + }, + signal, + }, + ); + return EventDefinitionsResponseSchema.parse(response.data).results.length > 0; +} + export function handleApiError(error: unknown, operation: string): ApiError { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError<{ detail?: string }>; diff --git a/src/lib/oauth/program-scopes.ts b/src/lib/oauth/program-scopes.ts index 2102c93e7..20d92468c 100644 --- a/src/lib/oauth/program-scopes.ts +++ b/src/lib/oauth/program-scopes.ts @@ -151,6 +151,11 @@ export const AGENT_SKILL_SCOPE_ADDITIONS = [ * `llma-skill-file-get`) and author the user-approved custom * `signals-scout-*` skills (`llma-skill-create`). Canonical scout * bodies are never edited. + * • event_definition:read — the events-check screen probes whether the + * project has any custom (non-`$`) event definitions + * (`fetchHasCustomEvents`) to decide whether to propose a product + * analytics setup before the run. Already in the wizard OAuth app's + * production scope ceiling (the mcp-tutorial program requests it). * • product_enablement:write — the "Enable products" step turns on * Session Replay / Error Tracking / Support so their sources have * data to read (`products-enable`). A purpose-built scope: the @@ -172,6 +177,7 @@ export const SELF_DRIVING_SCOPE_ADDITIONS = [ 'external_data_source:write', 'llm_skill:read', 'llm_skill:write', + 'event_definition:read', 'product_enablement:write', ] as const; diff --git a/src/lib/programs/__tests__/self-driving-detect.test.ts b/src/lib/programs/__tests__/self-driving-detect.test.ts index ba4b992cf..6a0569ca5 100644 --- a/src/lib/programs/__tests__/self-driving-detect.test.ts +++ b/src/lib/programs/__tests__/self-driving-detect.test.ts @@ -9,6 +9,8 @@ import { import { detectPostHogPresent, POSTHOG_MANIFESTS, + POSTHOG_PRESENT_KEY, + SELF_DRIVING_CUSTOM_EVENTS_KEY, } from '@lib/programs/self-driving/detect'; import { toIntegrationReport } from '@lib/programs/self-driving/detect-agentic'; import { @@ -133,6 +135,7 @@ describe('selfDrivingConfig', () => { 'integration-check', 'health-check', 'auth', + 'events-check', 'integrate-detect', 'integrate-run', 'self-driving-handoff', @@ -447,6 +450,51 @@ describe('detectPostHogPresent', () => { }); }); +describe('events-check step', () => { + const step = selfDrivingConfig.steps.find((s) => s.id === 'events-check'); + + it('shows only on the PostHog-already-present path with no decision made', () => { + const session = buildSession({}); + session.frameworkContext[POSTHOG_PRESENT_KEY] = true; + expect(step?.show?.(session)).toBe(true); + }); + + it('is hidden when PostHog is absent (the integrate path owns that flow)', () => { + const session = buildSession({}); + session.frameworkContext[POSTHOG_PRESENT_KEY] = false; + expect(step?.show?.(session)).toBe(false); + }); + + it('is hidden when --integrate pre-decided the flow', () => { + const session = buildSession({}); + session.frameworkContext[POSTHOG_PRESENT_KEY] = true; + session.integrate = true; + expect(step?.show?.(session)).toBe(false); + }); + + it('is incomplete until the probe lands', () => { + const session = buildSession({}); + session.frameworkContext[POSTHOG_PRESENT_KEY] = true; + expect(step?.isComplete?.(session)).toBe(false); + }); + + it('completes silently when custom events exist', () => { + const session = buildSession({}); + session.frameworkContext[SELF_DRIVING_CUSTOM_EVENTS_KEY] = true; + expect(step?.isComplete?.(session)).toBe(true); + }); + + it('completes once the user answers the default-events proposal either way', () => { + for (const answer of [true, false]) { + const session = buildSession({}); + session.frameworkContext[SELF_DRIVING_CUSTOM_EVENTS_KEY] = false; + expect(step?.isComplete?.(session)).toBe(false); + session.integrate = answer; + expect(step?.isComplete?.(session)).toBe(true); + } + }); +}); + describe('integrate-detect step', () => { const step = selfDrivingConfig.steps.find((s) => s.id === 'integrate-detect'); @@ -515,6 +563,39 @@ describe('toIntegrationReport', () => { expect(p.instrumentable).toBe(false); expect(p.continuable).toBe(false); }); + + describe('instrumentExisting (events-check accepted a product analytics setup)', () => { + const opts = { instrumentExisting: true }; + + it('makes a supported project with PostHog instrumentable, not continuable', () => { + // The existing SDK is the point — the pick targets it for analytics + // events, and dropping continuable keeps it out of the picker twice. + const [p] = toIntegrationReport( + build({ targetId: Integration.nextjs, hasPostHog: true }), + opts, + ).projects; + expect(p.instrumentable).toBe(true); + expect(p.continuable).toBe(false); + }); + + it('still requires a supported framework', () => { + const [p] = toIntegrationReport( + build({ targetId: null, hasPostHog: true }), + opts, + ).projects; + expect(p.instrumentable).toBe(false); + expect(p.continuable).toBe(true); + }); + + it('leaves the no-PostHog classification unchanged', () => { + const [p] = toIntegrationReport( + build({ targetId: Integration.nextjs, hasPostHog: false }), + opts, + ).projects; + expect(p.instrumentable).toBe(true); + expect(p.continuable).toBe(false); + }); + }); }); describe('manifest list sync', () => { diff --git a/src/lib/programs/self-driving/ARCHITECTURE.md b/src/lib/programs/self-driving/ARCHITECTURE.md index c5f9165d4..6ec814825 100644 --- a/src/lib/programs/self-driving/ARCHITECTURE.md +++ b/src/lib/programs/self-driving/ARCHITECTURE.md @@ -97,7 +97,15 @@ are a cross-repo contract — change one, change both repos. **Program definition** (`src/lib/programs/self-driving/`, five files): `index.ts` (config + lifecycle), `prompt.ts` (the 9 steps + mechanics + project URLs), `detect.ts` (prerequisite check + abort vocabulary), `steps.ts` (TUI screen sequence -`detect → intro → health-check → auth → run → outro`), and `content/tips.ts` (the +`detect → intro → integration-check → health-check → auth → events-check → integrate-detect → +integrate-run → self-driving-handoff → run → outro`; the integration screens show only on the +no-PostHog path, and `events-check` only on the already-present path — after auth it probes the +project's event definitions via `fetchHasCustomEvents` (`src/lib/api.ts`, +`event_type=event_custom`, scope `event_definition:read`, fail-open) and, when the project +captures only default events, proposes a product analytics setup that routes into the same +integrate path — the standard integration program is the analytics setup, and +`detect-agentic.ts`'s `instrumentExisting` mode lets the picker target already-integrated +projects), and `content/tips.ts` (the program-owned `Tips`-sidebar copy that defines signal sources + scouts in plain language, wired via `getTips`; `RunScreen` falls back to `DEFAULT_TIPS` for every other program, so nothing else is affected). `selfDrivingConfig` is built from the @@ -134,7 +142,7 @@ No bridge (CI/non-interactive) → returns an error telling the agent to default overlay; cancelled/timed-out fields resolve to `CANCELLED_SENTINEL = '__cancelled__'`. **OAuth scopes** (`src/lib/oauth/program-scopes.ts`). Base `WIZARD_OAUTH_SCOPES` -(`src/lib/constants.ts`) ∪ `SELF_DRIVING_SCOPE_ADDITIONS` — **12 strings**, requested via a PKCE +(`src/lib/constants.ts`) ∪ `SELF_DRIVING_SCOPE_ADDITIONS` — **14 strings**, requested via a PKCE auth-code flow: | Scope | Why | @@ -145,6 +153,8 @@ auth-code flow: | `session_recording:read`, `survey:read`, `error_tracking:read` | Read-only usage probes (STEP 2). | | `external_data_source:read`, `external_data_source:write` | Create/verify warehouse sources (STEP 5). | | `llm_skill:read`, `llm_skill:write` | Read the authoring guide + canonical bodies, create approved custom scouts (STEP 7). | +| `event_definition:read` | The events-check screen's custom-events probe (`fetchHasCustomEvents`), before the run. Already in the prod ceiling (mcp-tutorial requests it). | +| `product_enablement:write` | STEP 3b `products-enable` — server-owned enable recipes (§9). | The prod `OAuthApplication.scopes` ceiling is an **exhaustive allow-list** (`posthog/scopes.py`, `scopes_within_ceiling`) — anything outside it is rejected at `/authorize`. Several of these diff --git a/src/lib/programs/self-driving/detect-agentic.ts b/src/lib/programs/self-driving/detect-agentic.ts index 61dc3db2e..900b0c28b 100644 --- a/src/lib/programs/self-driving/detect-agentic.ts +++ b/src/lib/programs/self-driving/detect-agentic.ts @@ -41,9 +41,11 @@ export type IntegrationProject = { integration: Integration | null; /** Whether a PostHog SDK is already installed in this project. */ hasPostHog: boolean; - /** integration != null && !hasPostHog — PostHog can be set up here. */ + /** Supported framework without PostHog — or with it, when the + * events-check path asked for existing installs to be instrumentable. */ instrumentable: boolean; - /** hasPostHog: skip integration and continue straight to Self-driving. */ + /** hasPostHog and not picked as an instrument target: skip integration + * and continue straight to Self-driving. */ continuable: boolean; /** Why the project can't be set up (only when !instrumentable). */ reason?: string; @@ -54,9 +56,22 @@ export type IntegrationDetectionReport = { projects: IntegrationProject[]; }; +export type IntegrationDetectOptions = { + /** + * Treat projects that already have the PostHog SDK as instrumentable + * (a supported framework is still required). Used by the events-check + * path: the user accepted a product analytics setup for a project whose + * SDK is already installed, so "already has PostHog" is the point, not a + * disqualifier. Such projects stop being offered as continue-with-existing + * so they don't appear in the picker twice. + */ + instrumentExisting?: boolean; +}; + function classify( integration: Integration | null, hasPostHog: boolean, + instrumentExisting: boolean, ): { instrumentable: boolean; reason?: string } { if (integration == null) { return { @@ -64,7 +79,7 @@ function classify( reason: 'Not a framework the wizard can set up yet', }; } - if (hasPostHog) { + if (hasPostHog && !instrumentExisting) { return { instrumentable: false, reason: 'Already has the PostHog SDK' }; } return { instrumentable: true }; @@ -73,7 +88,9 @@ function classify( /** Map a detection report into classified projects (exported for tests). */ export function toIntegrationReport( report: AgenticDetectionReport, + options: IntegrationDetectOptions = {}, ): IntegrationDetectionReport { + const instrumentExisting = options.instrumentExisting === true; return { repoType: report.repoType, projects: report.projects.map((p) => { @@ -81,13 +98,20 @@ export function toIntegrationReport( p.targetId && INTEGRATION_IDS.has(p.targetId) ? (p.targetId as Integration) : null; + const classified = classify( + integration, + p.hasPostHog, + instrumentExisting, + ); return { path: p.path, framework: p.framework, integration, hasPostHog: p.hasPostHog, - continuable: p.hasPostHog, - ...classify(integration, p.hasPostHog), + // Instrumentable-with-PostHog (events-check mode) projects are picker + // targets, not continue-with-existing rows. + continuable: p.hasPostHog && !classified.instrumentable, + ...classified, }; }), }; @@ -97,13 +121,14 @@ export function toIntegrationReport( export async function detectSelfDrivingIntegrationProjects( session: WizardSession, onEvent?: DetectEvent, + options?: IntegrationDetectOptions, ): Promise { const report = await detectProjectsWithAgent(session, { targets: INTEGRATION_TARGETS, purpose: 'set up a PostHog SDK integration', onEvent, }); - return toIntegrationReport(report); + return toIntegrationReport(report, options); } /** diff --git a/src/lib/programs/self-driving/detect.ts b/src/lib/programs/self-driving/detect.ts index adb3ecdff..0e5e53627 100644 --- a/src/lib/programs/self-driving/detect.ts +++ b/src/lib/programs/self-driving/detect.ts @@ -41,6 +41,18 @@ export const POSTHOG_PRESENT_KEY = 'postHogPresent'; */ export const SELF_DRIVING_INTEGRATE_PATH_KEY = 'selfDrivingIntegratePath'; +/** + * frameworkContext key holding the events-check probe result: does the + * PostHog project have any custom (non-`$`) event definitions? Unset until + * the probe runs (it needs credentials, so only after auth, and only on the + * PostHog-already-present path). `false` means the project captures only + * default events (or none) — the events-check screen then proposes setting + * up product analytics via the integrate path, and the integration-detect + * screen reads the same key to let already-integrated projects be + * instrumented instead of only "continued with". + */ +export const SELF_DRIVING_CUSTOM_EVENTS_KEY = 'selfDrivingCustomEvents'; + // Matches `posthog` at a dependency boundary (line start, or after "'/=:.@ or // whitespace): catches `com.posthog:posthog-android` and `@posthog/ai`, skips // substrings inside other words. diff --git a/src/lib/programs/self-driving/steps.ts b/src/lib/programs/self-driving/steps.ts index e43576978..fbf1b1cf6 100644 --- a/src/lib/programs/self-driving/steps.ts +++ b/src/lib/programs/self-driving/steps.ts @@ -1,17 +1,21 @@ /** * Self-driving program step list. * - * detect → intro → integration-check → health-check → auth → integrate-detect → - * integrate-run → self-driving-handoff → run → outro. A deterministic check in - * `detect` decides whether PostHog is already in the project: found → the - * integration screens are skipped and the integrate-run phase never shows; not - * found → integration-check reports it and the only action sets up PostHog. - * After auth, `integrate-detect` runs the Haiku detector and has the user pick - * which project to set PostHog up in (a monorepo can have several); - * `integrate-run` then runs the real integration program's agent (its own task - * list) in that project. `self-driving-handoff` then bridges to Self-driving - * ("PostHog is installed — now set up Self-driving") before the Self-driving - * run. No keep-skills step: the setup skill is transient, so postRun removes it. + * detect → intro → integration-check → health-check → auth → events-check → + * integrate-detect → integrate-run → self-driving-handoff → run → outro. A + * deterministic check in `detect` decides whether PostHog is already in the + * project: found → the integration screens are skipped and the integrate-run + * phase never shows; not found → integration-check reports it and the only + * action sets up PostHog. On the already-present path, `events-check` probes + * the project's event definitions after auth: only default (or no) events → + * it proposes setting up product analytics, which routes into the same + * integrate path. After auth, `integrate-detect` runs the Haiku detector and + * has the user pick which project to set PostHog up in (a monorepo can have + * several); `integrate-run` then runs the real integration program's agent + * (its own task list) in that project. `self-driving-handoff` then bridges to + * Self-driving ("PostHog is installed — now set up Self-driving") before the + * Self-driving run. No keep-skills step: the setup skill is transient, so + * postRun removes it. */ import { resolve, sep } from 'path'; @@ -22,6 +26,7 @@ import { integrationRunStep } from '@lib/programs/posthog-integration/index'; import { detectSelfDrivingPrerequisites, POSTHOG_PRESENT_KEY, + SELF_DRIVING_CUSTOM_EVENTS_KEY, SELF_DRIVING_INTEGRATE_PATH_KEY, } from './detect.js'; import { prepSelfDrivingIntegration } from './detect-agentic.js'; @@ -80,6 +85,21 @@ export const SELF_DRIVING_PROGRAM: ProgramStep[] = [ screenId: 'auth', isComplete: (session) => session.credentials !== null, }, + { + // Shown only on the PostHog-already-present path: after auth, probe + // whether the project captures any custom events. Default-only (or no) + // events → propose setting up product analytics first; accepting sets + // integrate=true and reuses the whole integrate path below (the standard + // integration program IS the analytics setup). Custom events found (or + // the probe failed — fail open, never nag) → completes silently. + id: 'events-check', + label: 'Events', + screenId: 'self-driving-events-check', + show: (session) => postHogPresent(session) && session.integrate === null, + isComplete: (session) => + session.frameworkContext[SELF_DRIVING_CUSTOM_EVENTS_KEY] === true || + session.integrate !== null, + }, { // After auth, before the integration runs: the detector scans the repo and // the user picks which project to set PostHog up in — a single project or diff --git a/src/ui/tui/screen-registry.tsx b/src/ui/tui/screen-registry.tsx index 2521e9799..55acb2936 100644 --- a/src/ui/tui/screen-registry.tsx +++ b/src/ui/tui/screen-registry.tsx @@ -29,6 +29,7 @@ import { SourceMapsOutroScreen } from './screens/SourceMapsOutroScreen.js'; import { AgentSkillIntroScreen } from './screens/AgentSkillIntroScreen.js'; import { SelfDrivingIntroScreen } from './screens/SelfDrivingIntroScreen.js'; import { SelfDrivingIntegrationCheckScreen } from './screens/SelfDrivingIntegrationCheckScreen.js'; +import { SelfDrivingEventsCheckScreen } from './screens/SelfDrivingEventsCheckScreen.js'; import { SelfDrivingIntegrationDetectScreen } from './screens/SelfDrivingIntegrationDetectScreen.js'; import { SelfDrivingHandoffScreen } from './screens/SelfDrivingHandoffScreen.js'; import { AuditIntroScreen } from './screens/audit/AuditIntroScreen.js'; @@ -91,6 +92,9 @@ export function createScreens( [ScreenId.SelfDrivingIntegrationCheck]: ( ), + [ScreenId.SelfDrivingEventsCheck]: ( + + ), [ScreenId.SelfDrivingIntegrationDetect]: ( ), diff --git a/src/ui/tui/screen-sequences.ts b/src/ui/tui/screen-sequences.ts index 39052fd96..7bcc71843 100644 --- a/src/ui/tui/screen-sequences.ts +++ b/src/ui/tui/screen-sequences.ts @@ -26,6 +26,7 @@ export enum ScreenId { AgentSkillIntro = 'agent-skill-intro', SelfDrivingIntro = 'self-driving-intro', SelfDrivingIntegrationCheck = 'self-driving-integration-check', + SelfDrivingEventsCheck = 'self-driving-events-check', SelfDrivingIntegrationDetect = 'self-driving-integration-detect', SelfDrivingHandoff = 'self-driving-handoff', AuditIntro = 'audit-intro', diff --git a/src/ui/tui/screens/SelfDrivingEventsCheckScreen.tsx b/src/ui/tui/screens/SelfDrivingEventsCheckScreen.tsx new file mode 100644 index 000000000..bb282793f --- /dev/null +++ b/src/ui/tui/screens/SelfDrivingEventsCheckScreen.tsx @@ -0,0 +1,100 @@ +/** + * SelfDrivingEventsCheckScreen — shown only when PostHog is already present. + * Probes the project's event definitions server-side (so events captured + * from other repos or the snippet count too): custom events found → resolves + * silently and the flow proceeds; only default events (or none) → proposes + * setting up product analytics, which routes into the same integrate path + * the no-PostHog flow uses (the standard integration program is the + * analytics setup). + * + * Runs after auth — the probe needs credentials. Fails open: a probe error + * counts as "has custom events" so a flaky API never nags the user. + */ + +import { Box, Text } from 'ink'; +import { useEffect, useRef, useSyncExternalStore } from 'react'; +import type { WizardStore } from '@ui/tui/store'; +import { LoadingBox, PickerMenu } from '@ui/tui/primitives/index'; +import { Colors } from '@ui/tui/styles'; +import { analytics, sessionProperties } from '@utils/analytics'; +import { fetchHasCustomEvents } from '@lib/api'; +import { SELF_DRIVING_CUSTOM_EVENTS_KEY } from '@lib/programs/self-driving/detect'; + +interface SelfDrivingEventsCheckScreenProps { + store: WizardStore; +} + +export const SelfDrivingEventsCheckScreen = ({ + store, +}: SelfDrivingEventsCheckScreenProps) => { + useSyncExternalStore( + (cb) => store.subscribe(cb), + () => store.getSnapshot(), + ); + + const { credentials, frameworkContext } = store.session; + const hasCustomEvents = frameworkContext[SELF_DRIVING_CUSTOM_EVENTS_KEY]; + const started = useRef(false); + + // Probe once, after auth. The result lands in frameworkContext — `true` + // completes the step (nothing to propose), `false` renders the proposal. + useEffect(() => { + if (!credentials || started.current || hasCustomEvents !== undefined) { + return; + } + started.current = true; + void (async () => { + // Fail open — an unreachable or unreadable endpoint must not nag a + // user whose events may be perfectly fine. + let result = true; + try { + result = await fetchHasCustomEvents( + credentials.accessToken, + credentials.projectId, + credentials.host.apiHost, + ); + } catch { + /* fail open */ + } + analytics.wizardCapture('self-driving events check', { + self_driving_has_custom_events: result, + ...sessionProperties(store.session), + }); + store.setFrameworkContext(SELF_DRIVING_CUSTOM_EVENTS_KEY, result); + })(); + }, [credentials, hasCustomEvents, store]); + + if (hasCustomEvents !== false) { + return ; + } + + return ( + + + Only default events found + + + + + This project isn't capturing any custom events yet — just + PostHog's built-in ones (pageviews, autocapture). Product + analytics events track what users actually do in your product, and + give Self-driving real behavior to watch. + + + + + { + const v = Array.isArray(value) ? value[0] : value; + store.setIntegrate(v === 'yes', { via: 'default-events-only' }); + }} + /> + + + ); +}; diff --git a/src/ui/tui/screens/SelfDrivingHandoffScreen.tsx b/src/ui/tui/screens/SelfDrivingHandoffScreen.tsx index e3565bee6..89a36f8e0 100644 --- a/src/ui/tui/screens/SelfDrivingHandoffScreen.tsx +++ b/src/ui/tui/screens/SelfDrivingHandoffScreen.tsx @@ -1,9 +1,10 @@ /** * SelfDrivingHandoffScreen — the bridge between the integration run and the - * Self-driving run. Shown only in the integrate path (PostHog wasn't present, so - * the wizard set it up first); the already-has-PostHog path skips straight to - * Self-driving with no note. Confirms the SDK is in, then hands off to the - * Self-driving run. + * Self-driving run. Shown only in the integrate path: PostHog wasn't present + * and the wizard set it up first, or the events-check found only default + * events and the wizard added product analytics. Any other path skips + * straight to Self-driving with no note. Confirms the work is in, then hands + * off to the Self-driving run. */ import { Box, Text } from 'ink'; @@ -13,7 +14,10 @@ import type { WizardStore } from '@ui/tui/store'; import { PickerMenu } from '@ui/tui/primitives/index'; import { Colors } from '@ui/tui/styles'; import { SETUP_REPORT_FILE } from '@lib/programs/posthog-integration/index'; -import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; +import { + SELF_DRIVING_CUSTOM_EVENTS_KEY, + SELF_DRIVING_INTEGRATE_PATH_KEY, +} from '@lib/programs/self-driving/detect'; interface SelfDrivingHandoffScreenProps { store: WizardStore; @@ -33,10 +37,15 @@ export const SelfDrivingHandoffScreen = ({ const dir = typeof rel === 'string' && rel !== '.' ? `${rel}/` : ''; const reportPath = `./${dir}${SETUP_REPORT_FILE}`; + // The events-check path re-ran the integration for analytics events on an + // existing install — "installed" would undersell what just happened. + const eventsPath = + store.session.frameworkContext[SELF_DRIVING_CUSTOM_EVENTS_KEY] === false; + return ( - ✔ PostHog is installed + ✔ {eventsPath ? 'Product analytics is set up' : 'PostHog is installed'} diff --git a/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx b/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx index bcd3092ae..8e261c7ac 100644 --- a/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx +++ b/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx @@ -16,7 +16,10 @@ import { LoadingBox, PickerMenu } from '@ui/tui/primitives/index'; import { Colors, Icons } from '@ui/tui/styles'; import { Integration } from '@lib/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; +import { + SELF_DRIVING_CUSTOM_EVENTS_KEY, + SELF_DRIVING_INTEGRATE_PATH_KEY, +} from '@lib/programs/self-driving/detect'; import { detectSelfDrivingIntegrationProjects, type IntegrationProject, @@ -51,6 +54,12 @@ export const SelfDrivingIntegrationDetectScreen = ({ const { credentials } = store.session; const accessToken = credentials?.accessToken; + // The events-check screen found only default events and the user accepted + // a product analytics setup — projects that already have the SDK are the + // instrument targets here, not continue-with-existing rows. + const instrumentExisting = + store.session.frameworkContext[SELF_DRIVING_CUSTOM_EVENTS_KEY] === false; + const [state, setState] = useState({ kind: 'loading' }); const [activity, setActivity] = useState([]); const started = useRef(false); @@ -87,6 +96,7 @@ export const SelfDrivingIntegrationDetectScreen = ({ setActivity((prev) => [...prev, line].slice(-MAX_ACTIVITY_LINES)); } }, + { instrumentExisting }, ); if (!cancelled) setState({ kind: 'ready', report }); } catch (err) { @@ -101,7 +111,7 @@ export const SelfDrivingIntegrationDetectScreen = ({ return () => { cancelled = true; }; - }, [accessToken, store]); + }, [accessToken, store, instrumentExisting]); if (!credentials) { return ; @@ -266,7 +276,12 @@ export const SelfDrivingIntegrationDetectScreen = ({ header: true, }); const options = [ - heading('new', 'New PostHog integration:'), + heading( + 'new', + instrumentExisting + ? 'Add product analytics to:' + : 'New PostHog integration:', + ), ...instrumentable.map((p) => ({ label: projectLabel(p), value: `${NEW}${p.path}`, @@ -299,7 +314,9 @@ export const SelfDrivingIntegrationDetectScreen = ({