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
28 changes: 28 additions & 0 deletions src/__tests__/wizard-abort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,34 @@ describe('wizardAbort', () => {
});
});

it('does not capture an expected abort as an exception', async () => {
// A benign, user-driven abort (e.g. declining the GitHub connection) still
// carries a WizardError for context/logging, but must not reach error
// tracking — it is a normal outcome, not an exception to triage.
const error = new WizardError('Agent aborted: github connection declined', {
integration: 'self-driving',
error_type: 'ABORT',
});

await expect(wizardAbort({ error, expected: true })).rejects.toThrow(
'process.exit called',
);

expect(mockAnalytics.captureException).not.toHaveBeenCalled();
expect(mockAnalytics.shutdown).toHaveBeenCalledWith('cancelled');
});

it('still captures an unexpected abort even when an error is provided', async () => {
const error = new WizardError('Agent aborted: unreachable', {});

await expect(wizardAbort({ error, expected: false })).rejects.toThrow(
'process.exit called',
);

expect(mockAnalytics.captureException).toHaveBeenCalledWith(error, {});
expect(mockAnalytics.shutdown).toHaveBeenCalledWith('error');
});

it('runs registered cleanup functions before analytics and display', async () => {
const callOrder: string[] = [];

Expand Down
4 changes: 4 additions & 0 deletions src/lib/agent/runner/sequence/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ export async function runLinearProgram(
});
await wizardAbort({
outroData,
// A matched, `expected` abort case (e.g. the user declining the GitHub
// connection) is a benign outcome — render the outro and fire the
// `agent aborted` event above, but keep it out of error tracking.
expected: matched?.expected ?? false,
error: new WizardError(`Agent aborted: ${reason}`, {
integration: config.integrationLabel,
error_type: AgentErrorType.ABORT,
Expand Down
8 changes: 8 additions & 0 deletions src/lib/agent/runner/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export interface AbortCase {
message: string;
body: string;
docsUrl?: string;
/**
* Mark a benign, user-driven or environment-driven abort (e.g. the user
* declining an optional connection) as expected. Expected aborts still
* render their outro and fire the `agent aborted` analytics event, but are
* NOT reported to error tracking via `captureException` — they are normal
* outcomes, not exceptions worth triaging.
*/
expected?: boolean;
}

/**
Expand Down
24 changes: 24 additions & 0 deletions src/lib/programs/__tests__/self-driving-detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,30 @@ describe('SELF_DRIVING_ABORT_CASES', () => {
expect(matched[0].body).toBeTruthy();
});

it('marks benign, user/environment-driven aborts as expected (kept out of error tracking)', () => {
// Declining GitHub or running non-interactively are normal outcomes, not
// exceptions worth triaging — they must be flagged `expected` so
// wizardAbort skips captureException.
const expectedReasons = [
'github connection declined',
'requires-interactive-mode',
'requirements-incomplete',
];
for (const reason of expectedReasons) {
const [c] = SELF_DRIVING_ABORT_CASES.filter((c) => c.match.test(reason));
expect(c?.expected, reason).toBe(true);
}
});

it('keeps the unavailable-access abort as a genuine (unexpected) error', () => {
// Signals being unreachable in open beta is a real failure — it should
// still be reported to error tracking.
const [c] = SELF_DRIVING_ABORT_CASES.filter((c) =>
c.match.test('self-driving is not available for this project'),
);
expect(c?.expected).not.toBe(true);
});

it('frames the unavailable-access abort as open beta, not a closed per-team beta', () => {
// STEP 1 no longer gates on access — Self-driving is open beta — but the
// abort is kept as a safety net. Its copy must say the product is still
Expand Down
10 changes: 10 additions & 0 deletions src/lib/programs/self-driving/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ export const SELF_DRIVING_ABORT_CASES: AbortCase[] = [
'open fixes, so setup cannot finish without it. Nothing was left ' +
'half-configured. When you are ready to install the PostHog GitHub ' +
'App, run the wizard again.',
// The user chose not to connect GitHub — an expected cancellation, not an
// error. The friendly outro above already explains next steps; don't file
// an error-tracking exception for it.
expected: true,
},
{
// Skill emits: [ABORT] requires-interactive-mode
Expand All @@ -118,6 +122,9 @@ export const SELF_DRIVING_ABORT_CASES: AbortCase[] = [
'Self-driving setup asks questions along the way (GitHub and ' +
'issue trackers), so it needs an interactive terminal. Run ' +
'the wizard outside CI / non-interactive mode.',
// Running in a non-interactive environment is an expected precondition
// failure, not an exception to triage.
expected: true,
},
{
// The wizard_ask tool's own error texts (non-interactive host, ask cap
Expand All @@ -130,6 +137,9 @@ export const SELF_DRIVING_ABORT_CASES: AbortCase[] = [
'environment was non-interactive, or the question budget ran out). ' +
'Nothing was left half-configured. Run the wizard again in an ' +
'interactive terminal.',
// Couldn't collect the required answers (non-interactive / budget) — a
// benign, expected outcome rather than an error.
expected: true,
},
];

Expand Down
16 changes: 13 additions & 3 deletions src/utils/wizard-abort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ interface WizardAbortOptions {
outroData?: OutroData;
error?: Error | WizardError;
exitCode?: number;
/**
* Mark this as an expected, benign abort (e.g. the user declining an
* optional connection). The outro still renders and analytics still shut
* down cleanly, but the `error` is NOT sent to error tracking via
* `captureException` — it is a normal outcome, not an exception to triage.
*/
expected?: boolean;
}

const cleanupFns: Array<() => void> = [];
Expand Down Expand Up @@ -59,6 +66,7 @@ export async function wizardAbort(
outroData,
error,
exitCode = 1,
expected = false,
} = options ?? {};

logToFile(`[wizard-abort] exitCode=${exitCode}, message: ${message}`);
Expand All @@ -69,15 +77,17 @@ export async function wizardAbort(
// 1. Run registered cleanup functions
runCleanups();

// 2. Capture error in analytics (if provided)
if (error) {
// 2. Capture error in analytics (if provided). Skip for expected aborts —
// e.g. a user declining an optional connection — so a normal cancellation
// does not pollute error tracking with a WizardError exception.
if (error && !expected) {
analytics.captureException(error, {
...((error instanceof WizardError && error.context) || {}),
});
}

// 3. Shutdown analytics
await analytics.shutdown(error ? 'error' : 'cancelled');
await analytics.shutdown(error && !expected ? 'error' : 'cancelled');

// 4. Render the error outro. Synthesize OutroData from `message`
// when the caller didn't provide structured data.
Expand Down
Loading