diff --git a/bin.ts b/bin.ts index f4bb1bd58..2c518a703 100644 --- a/bin.ts +++ b/bin.ts @@ -66,6 +66,7 @@ import { warehouseCommand } from './src/commands/warehouse'; import { selfDrivingCommand } from './src/commands/self-driving'; import { slackCommand } from './src/commands/slack'; import { uploadSourcemapsCommand } from './src/commands/upload-sourcemaps'; +import { errorTrackingCommand } from './src/commands/error-tracking'; import { skillCommand } from './src/commands/skill'; import { cliCommand } from './src/commands/cli'; import { recoverOrphanedSettingsBackups } from './src/lib/agent/claude-settings'; @@ -100,5 +101,6 @@ Wizard.use(basicIntegrationCommand) .use(selfDrivingCommand) .use(slackCommand) .use(uploadSourcemapsCommand) + .use(errorTrackingCommand) .use(skillCommand) .init(); diff --git a/e2e-harness/action-registry.ts b/e2e-harness/action-registry.ts index 609432786..25585b2dd 100644 --- a/e2e-harness/action-registry.ts +++ b/e2e-harness/action-registry.ts @@ -104,6 +104,7 @@ export const ACTION_REGISTRY: Partial> = { [ScreenId.AgentSkillIntro]: [confirmSetupAction], [ScreenId.AiObservabilityIntro]: [confirmSetupAction], [ScreenId.MetricsIntro]: [confirmSetupAction], + [ScreenId.ErrorTrackingIntro]: [confirmSetupAction], [ScreenId.AuditIntro]: [confirmSetupAction], [ScreenId.DoctorIntro]: [confirmSetupAction], [ScreenId.WarehouseIntro]: [confirmSetupAction], diff --git a/e2e-harness/e2e-profile.ts b/e2e-harness/e2e-profile.ts index 4ce08f2bb..c5a042fa2 100644 --- a/e2e-harness/e2e-profile.ts +++ b/e2e-harness/e2e-profile.ts @@ -283,6 +283,7 @@ export function decideE2eAction( case ScreenId.AgentSkillIntro: case ScreenId.AiObservabilityIntro: case ScreenId.MetricsIntro: + case ScreenId.ErrorTrackingIntro: case ScreenId.AuditIntro: case ScreenId.SourceMapsIntro: case ScreenId.DoctorIntro: diff --git a/e2e-harness/profiles.ts b/e2e-harness/profiles.ts index c846ea2b3..9134faba3 100644 --- a/e2e-harness/profiles.ts +++ b/e2e-harness/profiles.ts @@ -25,6 +25,7 @@ import metricsE2e from '@lib/programs/metrics/test/e2e.json'; import replayVisionE2e from '@lib/programs/replay-vision/test/e2e.json'; import selfDrivingE2e from '@lib/programs/self-driving/test/e2e.json'; import sourceMapsE2e from '@lib/programs/error-tracking-upload-source-maps/test/e2e.json'; +import errorTrackingE2e from '@lib/programs/error-tracking/test/e2e.json'; import warehouseSourceE2e from '@lib/programs/warehouse-source/test/e2e.json'; const PROFILES: Partial> = { @@ -36,6 +37,7 @@ const PROFILES: Partial> = { [Program.SelfDriving]: selfDrivingE2e.profile as WizardE2eProfile, [Program.ErrorTrackingUploadSourceMaps]: sourceMapsE2e.profile as WizardE2eProfile, + [Program.ErrorTracking]: errorTrackingE2e.profile as WizardE2eProfile, [Program.WarehouseSource]: warehouseSourceE2e.profile as WizardE2eProfile, }; @@ -46,6 +48,7 @@ const VARIATIONS: Partial> = { aiObservabilityE2e.variations as WizardE2eVariation[], [Program.Metrics]: metricsE2e.variations as WizardE2eVariation[], [Program.ReplayVision]: replayVisionE2e.variations as WizardE2eVariation[], + [Program.ErrorTracking]: errorTrackingE2e.variations as WizardE2eVariation[], [Program.WarehouseSource]: warehouseSourceE2e.variations as WizardE2eVariation[], }; diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index 7d3da90d0..5a77c6e4d 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -25,6 +25,7 @@ import { } from '@lib/programs/program-registry'; import type { Harness, Sequence } from '@lib/constants'; import { buildSession } from '@lib/wizard-session'; +import { initLocalDev } from '@lib/local-dev'; import { runAgent } from '@lib/agent/agent-runner'; import { authenticate } from '@lib/agent/runner/shared/authenticate'; import { getOrAskForProjectData } from '@utils/setup-utils'; @@ -195,6 +196,17 @@ async function main() { // requires-interactive-mode the moment they need to ask a question. process.env.WIZARD_ASK_AUTODRIVE = '1'; + // The bin initializes the local-dev singleton from its yargs middleware; + // this host bypasses yargs, so `getSkillsBaseUrl()` would silently resolve + // to production even when the session carries the local flags. Initialize it + // here from the same env-backed spellings, before anything reads it. + initLocalDev({ + localDev: process.env.POSTHOG_WIZARD_LOCAL_DEV === 'true', + localMcp: envFlag('POSTHOG_WIZARD_LOCAL_MCP'), + localContextMill: envFlag('POSTHOG_WIZARD_LOCAL_CONTEXT_MILL'), + localPosthog: envFlag('POSTHOG_WIZARD_LOCAL_POSTHOG'), + }); + const { store } = startTUI(VERSION, programId); store.session = buildSession({ installDir: process.env.APP_DIR!, diff --git a/src/commands/error-tracking.ts b/src/commands/error-tracking.ts new file mode 100644 index 000000000..3db0b2816 --- /dev/null +++ b/src/commands/error-tracking.ts @@ -0,0 +1,15 @@ +import { errorTrackingConfig } from '@lib/programs/error-tracking/index'; + +import type { Command } from './command'; +import { nativeCommandFactory } from './factories/native-command-factory'; + +/** + * `wizard error-tracking` — flat skill command, set up error tracking today. + * + * Wires up exception capture and — where the platform needs it — source-map / + * debug-symbol upload. Runs the `error-tracking` orchestrator flow, which + * reuses the integration-v2 install/init mini-agents when the repo has no + * PostHog integration yet, so it works on uninstrumented projects too. + */ +export const errorTrackingCommand: Command = + nativeCommandFactory(errorTrackingConfig); diff --git a/src/lib/agent/runner/__tests__/switchboard.test.ts b/src/lib/agent/runner/__tests__/switchboard.test.ts index aa925c6dc..23cf03315 100644 --- a/src/lib/agent/runner/__tests__/switchboard.test.ts +++ b/src/lib/agent/runner/__tests__/switchboard.test.ts @@ -64,6 +64,7 @@ describe('switchboard PROGRAM_BINDINGS', () => { if (program === 'error-tracking-upload-source-maps') continue; // pinned below if (program === 'metrics') continue; // pinned below if (program === 'replay-vision') continue; // pinned below + if (program === 'error-tracking') continue; // pinned below expect(resolveBinding({ program, flags: {} })).toEqual(DEFAULT_RESOLVED); } }); @@ -113,6 +114,17 @@ describe('switchboard PROGRAM_BINDINGS', () => { }, trace: { harness: 'binding', model: 'binding', sequence: 'binding' }, }, + { + name: 'binds error-tracking to the orchestrator on pi; stage models come from the flow frontmatter', + ctx: { program: 'error-tracking', flags: {} }, + binding: { + sequence: Sequence.orchestrator, + harness: Harness.pi, + model: DEFAULT_AGENT_MODEL, + thinkingLevel: undefined, + }, + trace: { harness: 'binding', model: 'binding', sequence: 'binding' }, + }, { name: 'falls back to DEFAULT_BINDING for an unmapped program', ctx: { program: 'not-a-program', flags: {} }, @@ -232,7 +244,7 @@ describe('switchboard composed clamp', () => { model: GPT5_6_SOL_MODEL, thinkingLevel: 'medium', } - : program === 'metrics' + : program === 'metrics' || program === 'error-tracking' ? { ...DEFAULT_RESOLVED, harness: Harness.pi } : DEFAULT_RESOLVED, ); diff --git a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts index 16383d72a..04a628ba7 100644 --- a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts +++ b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts @@ -278,9 +278,9 @@ describe('isolation — everything on at once', () => { ...LINEAR_ANTHROPIC_DEFAULT, model: SONNET_5_MODEL, }); - } else if (program === 'metrics') { - // Orchestrator + pi from its OWN binding, not the flag; stage models - // are pinned context-mill side in the flow frontmatter. + } else if (program === 'metrics' || program === 'error-tracking') { + // Orchestrator + pi from their OWN bindings, not the flag; stage + // models are pinned context-mill side in the flow frontmatter. expect(resolved).toEqual({ ...ORCHESTRATOR_PI_DEFAULT, }); diff --git a/src/lib/agent/runner/switchboard/index.ts b/src/lib/agent/runner/switchboard/index.ts index fb4232036..f8c183ae4 100644 --- a/src/lib/agent/runner/switchboard/index.ts +++ b/src/lib/agent/runner/switchboard/index.ts @@ -154,6 +154,14 @@ export const PROGRAM_BINDINGS: Partial> = { harness: Harness.anthropic, model: DEFAULT_AGENT_MODEL, }, + // Orchestrator on pi, like metrics. The binding routes only; every stage's + // model and effort are pinned context-mill side in the flow's frontmatter + // (`model_pi`/`effort_pi`: terra seed, sol tasks, luna report). + 'error-tracking': { + sequence: Sequence.orchestrator, + harness: Harness.pi, + model: DEFAULT_AGENT_MODEL, + }, 'ai-observability': { sequence: Sequence.linear, harness: Harness.anthropic, diff --git a/src/lib/oauth/program-scopes.ts b/src/lib/oauth/program-scopes.ts index 69da4f59a..3e3c25686 100644 --- a/src/lib/oauth/program-scopes.ts +++ b/src/lib/oauth/program-scopes.ts @@ -250,6 +250,25 @@ export const REPLAY_VISION_SCOPE_ADDITIONS = [ 'replay_scanner:write', ] as const; +/** + * Extra scopes the error-tracking program needs on top of + * `WIZARD_OAUTH_SCOPES`. + * • error_tracking:read — the flow's report task reads symbol sets and + * issue state through `posthog_exec` to tell the user where to verify + * that captured exceptions actually land. + * • product_enablement:write — the report task turns on the Error Tracking + * product (`products-enable`) so the captured exceptions have a UI to + * land in; the server owns the enable recipe. + * + * No OAuth-ceiling edit needed — both are unprivileged public scope objects + * covered by the apps' `@default` sentinel, and self-driving already requests + * both. + */ +export const ERROR_TRACKING_SCOPE_ADDITIONS = [ + 'error_tracking:read', + 'product_enablement:write', +] as const; + /** * Per-program scope additions, layered on top of `WIZARD_OAUTH_SCOPES`. * @@ -280,6 +299,7 @@ const PROGRAM_SCOPE_ADDITIONS: Partial> = { ], slack: CONNECT_SLACK_SCOPE_ADDITIONS, 'replay-vision': REPLAY_VISION_SCOPE_ADDITIONS, + 'error-tracking': ERROR_TRACKING_SCOPE_ADDITIONS, }; /** diff --git a/src/lib/programs/__tests__/error-tracking.test.ts b/src/lib/programs/__tests__/error-tracking.test.ts new file mode 100644 index 000000000..819e07640 --- /dev/null +++ b/src/lib/programs/__tests__/error-tracking.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from 'vitest'; + +import { Integration } from '@lib/constants'; +import { + errorTrackingConfig, + SYMBOL_UPLOAD_CLI_FRAMEWORKS, +} from '@lib/programs/error-tracking/index'; + +describe('error-tracking program', () => { + test('runs the error-tracking agent flow', () => { + expect(errorTrackingConfig.agentFlow).toBe('error-tracking'); + }); + + test('detects the framework before the agent-skill steps', () => { + expect(errorTrackingConfig.steps[0]?.id).toBe('detect'); + expect(errorTrackingConfig.steps[0]?.onReady).toBeDefined(); + }); + + test('declares ci prerequisite work for headless runs', () => { + expect(errorTrackingConfig.ciPreRun).toBeDefined(); + }); + + test('shows the program-specific intro screen', () => { + const intro = errorTrackingConfig.steps.find((s) => s.id === 'intro'); + expect(intro?.screenId).toBe('error-tracking-intro'); + }); + + test('pre-installs no skill — the flow resolves variants per framework', () => { + // There is no bare `error-tracking` menu entry; a seeded skillId would + // send the linear path to a skill-not-found abort and mislead the intro. + expect(errorTrackingConfig.skillId).toBeUndefined(); + expect(errorTrackingConfig.run).toBeDefined(); + if ( + errorTrackingConfig.run && + typeof errorTrackingConfig.run !== 'function' + ) { + expect(errorTrackingConfig.run.skillId).toBeUndefined(); + } + }); +}); + +describe('error-tracking posthog-cli pre-install set', () => { + test('contains only real Integration values', () => { + for (const integration of SYMBOL_UPLOAD_CLI_FRAMEWORKS) { + expect(Object.values(Integration)).toContain(integration); + } + }); + + test('covers the symbol-upload platforms and no web ones', () => { + // Keep in lockstep with VARIANTS_REQUIRING_POSTHOG_CLI in the source-maps + // program: their builds shell out to a machine-global posthog-cli. + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.swift)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.android)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.reactNative)).toBe( + true, + ); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.flutter)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.go)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.rust)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.nextjs)).toBe(false); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.javascript_web)).toBe( + false, + ); + }); +}); diff --git a/src/lib/programs/error-tracking/index.ts b/src/lib/programs/error-tracking/index.ts new file mode 100644 index 000000000..2a32f2314 --- /dev/null +++ b/src/lib/programs/error-tracking/index.ts @@ -0,0 +1,204 @@ +import { Integration } from '@lib/constants'; +import { detectFramework, gatherFrameworkContext } from '@lib/detection/index'; +import { scopeInstallDirToProject } from '@lib/detection/project-scope'; +import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { getContentBlocks } from '@lib/programs/agent-skill/content/index'; +import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/steps'; +import { detectPostHogIntegration } from '@lib/programs/posthog-integration/detect'; +import type { + ProgramConfig, + ProgramReadyContext, + ProgramStep, +} from '@lib/programs/program-step'; +import type { WizardSession } from '@lib/wizard-session'; +import { installOrUpdatePostHogCli } from '@steps/install-cli-steering'; +import { getUI } from '@ui'; +import { analytics } from '@utils/analytics'; +import { wizardAbort } from '@utils/wizard-abort'; +import { ErrorCodes } from '@lib/errors'; + +const ERROR_TRACKING_REPORT_FILE = 'posthog-error-tracking-report.md'; +const ERROR_TRACKING_DOCS_URL = 'https://posthog.com/docs/error-tracking'; + +/** + * Frameworks whose symbol upload shells out to a machine-global `posthog-cli` + * with no npx / local-dep fallback. The wizard pre-installs the CLI for them + * because warlock blocks the agent's `npm install -g`. Mirrors + * `VARIANTS_REQUIRING_POSTHOG_CLI` in the source-maps program, but keyed by + * wizard `Integration` because here the framework is known before the flow's + * seed picks an uploader variant (`swift` maps to the `ios` uploader). + */ +export const SYMBOL_UPLOAD_CLI_FRAMEWORKS: ReadonlySet = new Set([ + Integration.swift, + Integration.android, + Integration.reactNative, + Integration.flutter, + Integration.go, + Integration.rust, +]); + +let postHogCliInstallAttempted = false; + +/** + * Pre-install posthog-cli when the detected framework's symbol upload will + * shell out to it. Warn, don't fail — the run still instruments exception + * capture; only the release build's upload step needs the CLI. + */ +function maybePreinstallPostHogCli(integration: Integration): void { + if (!SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(integration)) return; + if (postHogCliInstallAttempted) return; + postHogCliInstallAttempted = true; + + const result = installOrUpdatePostHogCli(); + if (!result.success) { + analytics.wizardCapture('error tracking posthog-cli preinstall failed', { + integration, + error: String(result.error).slice(0, 500), + }); + analytics.captureException( + result.errorObject ?? + new Error(`posthog-cli pre-install failed: ${result.error}`), + { source: 'error_tracking_cli_preinstall', integration }, + ); + getUI().log.warn( + `Could not pre-install posthog-cli (${result.error}). Your release build ` + + `will fail to upload debug symbols until it's installed: npm install -g @posthog/cli@latest`, + ); + } +} + +/** + * Framework detection ahead of the run, exactly like the default integration + * program. The orchestrator requires it: `session.skillId` must hold the + * detected framework id before the run arm starts, because the runner + * resolves the reference integration skill and every task's mini-skill + * variants (`integration-v2-install`, `integration-v2-error-tracking-step`, …) + * against it in preflight. Unlike replay-vision there is no platform + * allow-list — every detectable framework has an error-tracking-step variant. + */ +const DETECT_STEP: ProgramStep = { + id: 'detect', + label: 'Detecting framework', + onReady: async (ctx: ProgramReadyContext) => { + const integration = await detectFramework(ctx.session.installDir); + if (integration) maybePreinstallPostHogCli(integration); + await detectPostHogIntegration(ctx); + }, +}; + +const ERROR_TRACKING_STEPS: ProgramStep[] = [ + DETECT_STEP, + ...AGENT_SKILL_STEPS.map((step) => + step.id === 'intro' ? { ...step, screenId: 'error-tracking-intro' } : step, + ), +]; + +/** + * Mode-agnostic run instructions. The orchestrator's seed reads them as + * context on top of its own flow prompts; a linear override + * (`--sequence=linear`) relies on them entirely, so they spell out the + * skill-menu lookups the flow's tasks would otherwise perform. + */ +const ERROR_TRACKING_PROMPT = `Set up PostHog error tracking end-to-end: + +1. If PostHog is not integrated yet, install and initialize the SDK first — + do not abort. Pick the matching variant from the skill menu's + "integration-v2/install" and "integration-v2/init" categories. + +2. Wire up exception capture: install the "error-tracking" skill variant that + matches this project's platform (\`load_skill_menu\` with + \`category: "error-tracking"\`) and follow it. Set capture up in one place — + the SDK's own mechanism, never manual capture calls sprinkled across files. + +3. When the platform ships minified bundles or stripped binaries (browser JS, + React Native, iOS, Android, Flutter, Go, Rust), wire up source-map / + debug-symbol upload too: install the matching + "error-tracking-upload-source-maps" skill variant and follow it, including + credentials and CI. Skip this step on platforms with readable stack traces + (plain Python, Ruby, PHP, Elixir, JVM servers). + +The final report is written to ./${ERROR_TRACKING_REPORT_FILE}.`; + +/** + * `wizard error-tracking` — flat command on the orchestrator sequence. + * + * Makes uncaught errors reach PostHog with readable stack traces. The + * orchestrator runs the `error-tracking` agent flow (context-mill + * `context/agents/error-tracking`): the seed enqueues the install/init tasks + * (sharing integration-v2's step-skills, like replay-vision) when the project + * has no PostHog yet, then exception capture, then — when the platform needs + * it — the source-map subgraph adapted from the standalone + * `upload-source-maps` flow. + * + * Departures from a plain `createSkillProgram`: + * - No `run.skillId`: the flow's tasks resolve per-framework mini-skills + * themselves (there is no bare `error-tracking` menu entry), so the intro is + * a custom screen rather than the generic skill intro. + * - `DETECT_STEP` in front, so `session.skillId` carries the framework id the + * orchestrator's preflight resolves reference + mini-skill variants with. It + * also pre-installs posthog-cli for symbol-upload platforms, which the agent + * cannot (warlock blocks \`npm install -g\`). + * - `agentFlow` pinned (the id would default to the same value — explicit so + * renaming the program can't silently detach the flow). + * - `ciPreRun` mirrors replay-vision: scope the install dir to the right + * project (monorepos), then detect the framework — the headless equivalent + * of the detect step's onReady hook. + */ +export const errorTrackingConfig: ProgramConfig = { + command: 'error-tracking', + description: 'Set up PostHog error tracking, source-map upload included', + id: 'error-tracking', + agentFlow: 'error-tracking', + steps: ERROR_TRACKING_STEPS, + reportFile: ERROR_TRACKING_REPORT_FILE, + getContentBlocks, + + run: { + integrationLabel: 'error-tracking', + customPrompt: () => ERROR_TRACKING_PROMPT, + successMessage: `Error tracking configured! View the report at ./${ERROR_TRACKING_REPORT_FILE}`, + reportFile: ERROR_TRACKING_REPORT_FILE, + docsUrl: ERROR_TRACKING_DOCS_URL, + spinnerMessage: 'Setting up error tracking...', + estimatedDurationMinutes: 8, + // The flow can park on wizard_ask while the user does slow work (mint a + // personal API key in the browser, run a build and trigger the test + // error). The orchestrator caps per-task asks itself; this covers the + // linear fallback. + askTimeoutMs: 30 * 60 * 1000, + }, + + ciPreRun: async (session: WizardSession): Promise => { + await scopeInstallDirToProject(session); + + const integration = await detectFramework(session.installDir); + if (!integration) { + await wizardAbort({ + code: ErrorCodes.DetectNoFramework, + message: 'Could not auto-detect your framework for this project.', + }); + return; + } + maybePreinstallPostHogCli(integration); + session.integration = integration; + analytics.setTag('integration', integration); + + const frameworkConfig = FRAMEWORK_REGISTRY[integration]; + session.frameworkConfig = frameworkConfig; + session.skillId = integration; + + const context = await gatherFrameworkContext(frameworkConfig, { + installDir: session.installDir, + debug: session.debug, + signup: session.signup, + ci: true, + benchmark: session.benchmark, + yaraReport: session.yaraReport, + }); + for (const [key, value] of Object.entries(context)) { + if (!(key in session.frameworkContext)) { + session.frameworkContext[key] = value; + } + } + }, +}; diff --git a/src/lib/programs/error-tracking/test/e2e.json b/src/lib/programs/error-tracking/test/e2e.json new file mode 100644 index 000000000..454e873e1 --- /dev/null +++ b/src/lib/programs/error-tracking/test/e2e.json @@ -0,0 +1,53 @@ +{ + "program": "error-tracking", + "summary": "Happy path: detect framework, confirm intro, run the error-tracking orchestrator flow (install/init when PostHog is absent, exception capture, the source-map subgraph where the platform needs it, report), delete installed skills. The driver answers the flow's asks: the upload key via wizard_ask (id \"api-key\"; the raw key is vaulted by the ask tool, the agent only sees a secretRef) and declines the test-affordance offer.", + "profile": { + "setup": "first", + "healthCheck": "dismiss", + "mcp": "skip", + "slack": "skip", + "skills": "delete", + "ask": "first", + "askAnswers": [ + { "match": "api-key", "value": "${SOURCE_MAPS_CLI_KEY}", "secret": true }, + { "match": "test-affordance", "value": "no" } + ] + }, + "variations": [ + { + "name": "default", + "summary": "orchestrator / pi — the program's pinned binding; stage models from the flow frontmatter (terra seed, sol tasks, luna report)" + }, + { + "name": "linear-fallback", + "summary": "linear arm: the customPrompt drives a single agent through menu-installed skills", + "sequence": "linear" + } + ], + "path": [ + { + "screen": "detect", + "auto": "(headless) — framework detection puts the framework id on session.skillId; pre-installs posthog-cli for symbol-upload platforms" + }, + { + "screen": "error-tracking-intro", + "auto": "confirm & continue" + }, + { + "screen": "auth", + "auto": "(external) — the runner resolves credentials from the phx key" + }, + { + "screen": "run", + "auto": "(external) — the orchestrator flow; the profile answers wizard_ask \"api-key\" with SOURCE_MAPS_CLI_KEY and \"test-affordance\" with \"no\"" + }, + { + "screen": "outro", + "auto": "dismiss" + }, + { + "screen": "keep-skills", + "auto": "delete the installed skills" + } + ] +} diff --git a/src/lib/programs/program-registry.ts b/src/lib/programs/program-registry.ts index 2633c295e..7e3156948 100644 --- a/src/lib/programs/program-registry.ts +++ b/src/lib/programs/program-registry.ts @@ -21,6 +21,7 @@ import { posthogDoctorConfig } from './posthog-doctor/index.js'; import { webAnalyticsDoctorConfig } from './web-analytics-doctor/index.js'; import { migrationConfig } from './migration/index.js'; import { errorTrackingUploadSourceMapsConfig } from './error-tracking-upload-source-maps/index.js'; +import { errorTrackingConfig } from './error-tracking/index.js'; import { selfDrivingConfig } from './self-driving/index.js'; import { AGENT_SKILL_STEPS } from './agent-skill/index.js'; import { getContentBlocks as agentSkillContentBlocks } from './agent-skill/content/index.js'; @@ -70,6 +71,7 @@ export const PROGRAM_REGISTRY = [ revenueAnalyticsConfig, warehouseSourceConfig, errorTrackingUploadSourceMapsConfig, + errorTrackingConfig, auditConfig, eventsAuditConfig, posthogDoctorConfig, @@ -97,6 +99,7 @@ export const Program = { RevenueAnalyticsSetup: revenueAnalyticsConfig.id, WarehouseSource: warehouseSourceConfig.id, ErrorTrackingUploadSourceMaps: errorTrackingUploadSourceMapsConfig.id, + ErrorTracking: errorTrackingConfig.id, Migration: migrationConfig.id, Audit: auditConfig.id, EventsAudit: eventsAuditConfig.id, diff --git a/src/ui/tui/screen-registry.tsx b/src/ui/tui/screen-registry.tsx index 42765deec..1b18a3d88 100644 --- a/src/ui/tui/screen-registry.tsx +++ b/src/ui/tui/screen-registry.tsx @@ -30,6 +30,7 @@ import { SourceMapsOutroScreen } from './screens/SourceMapsOutroScreen.js'; import { AgentSkillIntroScreen } from './screens/AgentSkillIntroScreen.js'; import { AiObservabilityIntroScreen } from './screens/AiObservabilityIntroScreen.js'; import { MetricsIntroScreen } from './screens/MetricsIntroScreen.js'; +import { ErrorTrackingIntroScreen } from './screens/ErrorTrackingIntroScreen.js'; import { SelfDrivingIntroScreen } from './screens/SelfDrivingIntroScreen.js'; import { SelfDrivingIntegrationCheckScreen } from './screens/SelfDrivingIntegrationCheckScreen.js'; import { SelfDrivingIntegrationDetectScreen } from './screens/SelfDrivingIntegrationDetectScreen.js'; @@ -95,6 +96,7 @@ export function createScreens( ), [ScreenId.MetricsIntro]: , + [ScreenId.ErrorTrackingIntro]: , [ScreenId.SelfDrivingIntro]: , [ScreenId.SelfDrivingIntegrationCheck]: ( diff --git a/src/ui/tui/screen-sequences.ts b/src/ui/tui/screen-sequences.ts index 8131b3f7f..1da8347c0 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', AiObservabilityIntro = 'ai-observability-intro', MetricsIntro = 'metrics-intro', + ErrorTrackingIntro = 'error-tracking-intro', SelfDrivingIntro = 'self-driving-intro', SelfDrivingIntegrationCheck = 'self-driving-integration-check', SelfDrivingIntegrationDetect = 'self-driving-integration-detect', diff --git a/src/ui/tui/screens/ErrorTrackingIntroScreen.tsx b/src/ui/tui/screens/ErrorTrackingIntroScreen.tsx new file mode 100644 index 000000000..cceb74362 --- /dev/null +++ b/src/ui/tui/screens/ErrorTrackingIntroScreen.tsx @@ -0,0 +1,92 @@ +import { Box, Text } from 'ink'; +import { useState, useSyncExternalStore } from 'react'; +import type { WizardStore } from '@ui/tui/store'; +import { IntroScreenLayout } from '@ui/tui/screens/IntroScreenLayout'; +import { + SkillSourceInfo, + useSkillEntry, +} from '@ui/tui/screens/SkillSourceInfo'; + +interface ErrorTrackingIntroScreenProps { + store: WizardStore; +} + +export const ErrorTrackingIntroScreen = ({ + store, +}: ErrorTrackingIntroScreenProps) => { + useSyncExternalStore( + (cb) => store.subscribe(cb), + () => store.getSnapshot(), + ); + + const [showingMoreInfo, setShowingMoreInfo] = useState(false); + const { session } = store; + // error-tracking resolves its skill variants per framework at run time, so + // there's no single pre-seeded skillId. Fall back to the group id for the + // "more info" lookup. + const skillId = session.skillId ?? 'error-tracking'; + const { skillEntry, fetchFailed } = useSkillEntry(skillId, session.localMcp); + + const body = showingMoreInfo ? ( + + + + The wizard is an agent that executes PostHog tasks. Its code is open + source: https://github.com/PostHog/wizard + + + + + The{' '} + + error-tracking + {' '} + program makes uncaught errors reach PostHog with readable stack traces. + It installs and initializes the PostHog SDK when the project doesn't + have it yet, wires up exception capture through the SDK's own mechanism, + and — on platforms that ship minified bundles or stripped binaries — + sets up source-map / debug-symbol upload for your production builds. + + + + + + ) : ( + + + Let's make uncaught errors reach PostHog with readable stack traces. + + + ); + + const menuOptions = showingMoreInfo + ? [{ label: 'Back', value: 'back' }] + : [ + { label: 'Continue', value: 'continue' }, + { label: 'More info', value: 'more-info' }, + { label: 'Cancel', value: 'cancel' }, + ]; + + const handleSelect = (value: string) => { + if (value === 'cancel') process.exit(0); + else if (value === 'more-info') setShowingMoreInfo(true); + else if (value === 'back') setShowingMoreInfo(false); + else store.completeSetup(); + }; + + return ( + + ); +};