Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -100,5 +101,6 @@ Wizard.use(basicIntegrationCommand)
.use(selfDrivingCommand)
.use(slackCommand)
.use(uploadSourcemapsCommand)
.use(errorTrackingCommand)
.use(skillCommand)
.init();
1 change: 1 addition & 0 deletions e2e-harness/action-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const ACTION_REGISTRY: Partial<Record<ScreenName, DriverAction[]>> = {
[ScreenId.AgentSkillIntro]: [confirmSetupAction],
[ScreenId.AiObservabilityIntro]: [confirmSetupAction],
[ScreenId.MetricsIntro]: [confirmSetupAction],
[ScreenId.ErrorTrackingIntro]: [confirmSetupAction],
[ScreenId.AuditIntro]: [confirmSetupAction],
[ScreenId.DoctorIntro]: [confirmSetupAction],
[ScreenId.WarehouseIntro]: [confirmSetupAction],
Expand Down
1 change: 1 addition & 0 deletions e2e-harness/e2e-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions e2e-harness/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<ProgramId, WizardE2eProfile>> = {
Expand All @@ -36,6 +37,7 @@ const PROFILES: Partial<Record<ProgramId, WizardE2eProfile>> = {
[Program.SelfDriving]: selfDrivingE2e.profile as WizardE2eProfile,
[Program.ErrorTrackingUploadSourceMaps]:
sourceMapsE2e.profile as WizardE2eProfile,
[Program.ErrorTracking]: errorTrackingE2e.profile as WizardE2eProfile,
[Program.WarehouseSource]: warehouseSourceE2e.profile as WizardE2eProfile,
};

Expand All @@ -46,6 +48,7 @@ const VARIATIONS: Partial<Record<ProgramId, WizardE2eVariation[]>> = {
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[],
};
Expand Down
12 changes: 12 additions & 0 deletions scripts/tui-host.no-jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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!,
Expand Down
15 changes: 15 additions & 0 deletions src/commands/error-tracking.ts
Original file line number Diff line number Diff line change
@@ -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);
14 changes: 13 additions & 1 deletion src/lib/agent/runner/__tests__/switchboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
Expand Down Expand Up @@ -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: {} },
Expand Down Expand Up @@ -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,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
8 changes: 8 additions & 0 deletions src/lib/agent/runner/switchboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ export const PROGRAM_BINDINGS: Partial<Record<ProgramId, ProgramBinding>> = {
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,
Expand Down
20 changes: 20 additions & 0 deletions src/lib/oauth/program-scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*
Expand Down Expand Up @@ -280,6 +299,7 @@ const PROGRAM_SCOPE_ADDITIONS: Partial<Record<ProgramId, readonly string[]>> = {
],
slack: CONNECT_SLACK_SCOPE_ADDITIONS,
'replay-vision': REPLAY_VISION_SCOPE_ADDITIONS,
'error-tracking': ERROR_TRACKING_SCOPE_ADDITIONS,
};

/**
Expand Down
65 changes: 65 additions & 0 deletions src/lib/programs/__tests__/error-tracking.test.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
Loading
Loading