Skip to content

Commit 963bccf

Browse files
committed
feat(cli): add test for non-duplicate error reporting
1 parent 9db8d9a commit 963bccf

File tree

4 files changed

+84
-44
lines changed

4 files changed

+84
-44
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { CliError, TaskAbortSignal } from "@fern-api/task-context";
2+
import { describe, expect, it, vi } from "vitest";
3+
4+
import { reportError } from "../telemetry/reportError.js";
5+
import { createMockCliContext } from "./mockCliContext.js";
6+
7+
vi.mock("../telemetry/reportError.js", async (importOriginal) => {
8+
const original = await importOriginal<typeof import("../telemetry/reportError.js")>();
9+
return {
10+
...original,
11+
reportError: vi.fn(original.reportError)
12+
};
13+
});
14+
15+
const reportErrorSpy = vi.mocked(reportError);
16+
17+
describe("error reporting", () => {
18+
it("task failure via failAndThrow reports exactly once through the full chain", async () => {
19+
reportErrorSpy.mockClear();
20+
const cliContext = await createMockCliContext();
21+
22+
try {
23+
await cliContext.runTask(async (context) => {
24+
context.failAndThrow("bad", undefined, { code: CliError.Code.ConfigError });
25+
});
26+
} catch (error) {
27+
expect(error).toBeInstanceOf(TaskAbortSignal);
28+
cliContext.failWithoutThrowing(undefined, error);
29+
}
30+
31+
expect(reportErrorSpy).toHaveBeenCalledOnce();
32+
});
33+
34+
it("uncaught task error reports exactly once through the full chain", async () => {
35+
reportErrorSpy.mockClear();
36+
const cliContext = await createMockCliContext();
37+
38+
try {
39+
await cliContext.runTask(async () => {
40+
throw new CliError({ message: "something broke", code: CliError.Code.InternalError });
41+
});
42+
} catch (error) {
43+
expect(error).toBeInstanceOf(TaskAbortSignal);
44+
cliContext.failWithoutThrowing(undefined, error);
45+
}
46+
47+
expect(reportErrorSpy).toHaveBeenCalledOnce();
48+
});
49+
});

packages/cli/cli/src/cli-context/CliContext.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import {
77
CliError,
88
Finishable,
99
PosthogEvent,
10-
resolveErrorCode,
1110
Startable,
12-
shouldReportToSentry,
1311
TaskAbortSignal,
1412
TaskContext,
1513
TaskResult
@@ -19,6 +17,7 @@ import { Workspace } from "@fern-api/workspace-loader";
1917
import { input, select } from "@inquirer/prompts";
2018
import chalk from "chalk";
2119
import { maxBy } from "lodash-es";
20+
import { reportError } from "../telemetry/reportError.js";
2221
import { SentryClient } from "../telemetry/SentryClient.js";
2322
import { CliEnvironment } from "./CliEnvironment.js";
2423
import { StdoutRedirector } from "./StdoutRedirector.js";
@@ -127,27 +126,10 @@ export class CliContext {
127126
public failWithoutThrowing(message?: string, error?: unknown, options?: { code?: CliError.Code }): void {
128127
this.didSucceed = false;
129128
if (error instanceof TaskAbortSignal) {
130-
// We already tracked the true error, so we can just return.
131129
return;
132130
}
133-
134131
logErrorMessage({ message, error, logger: this.logger });
135-
136-
const code = resolveErrorCode(error, options?.code);
137-
138-
this.instrumentPostHogEvent({
139-
command: process.argv.join(" "),
140-
properties: {
141-
failed: true,
142-
source: "cli",
143-
error,
144-
errorCode: code
145-
}
146-
});
147-
148-
if (shouldReportToSentry(code)) {
149-
this.sentryClient.captureException(error ?? new CliError({ message: message ?? "", code }), code);
150-
}
132+
reportError(this, error, { ...options, message });
151133
}
152134

153135
/**
@@ -285,8 +267,8 @@ export class CliContext {
285267
}
286268
}
287269

288-
public async captureException(error: unknown, code?: CliError.Code): Promise<void> {
289-
await this.sentryClient.captureException(error, code);
270+
public captureException(error: unknown, code?: CliError.Code): void {
271+
this.sentryClient.captureException(error, code);
290272
}
291273

292274
public readonly logger = createLogger((level, ...args) => this.log(level, ...args));

packages/cli/cli/src/cli-context/TaskContextImpl.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@ import {
77
Finishable,
88
InteractiveTaskContext,
99
PosthogEvent,
10-
resolveErrorCode,
1110
Startable,
12-
shouldReportToSentry,
1311
TaskAbortSignal,
1412
TaskContext,
1513
TaskResult
1614
} from "@fern-api/task-context";
17-
1815
import chalk from "chalk";
1916

17+
import { reportError } from "../telemetry/reportError.js";
18+
2019
export declare namespace TaskContextImpl {
2120
export interface Init {
2221
logImmediately: (logs: Log[]) => void;
@@ -90,29 +89,11 @@ export class TaskContextImpl implements Startable<TaskContext>, Finishable, Task
9089

9190
public failWithoutThrowing(message?: string, error?: unknown, options?: { code?: CliError.Code }): void {
9291
this.result = TaskResult.Failure;
93-
9492
if (error instanceof TaskAbortSignal) {
95-
// We already tracked the true error, so we can just return.
9693
return;
9794
}
98-
9995
logErrorMessage({ message, error, logger: this.logger });
100-
101-
const code = resolveErrorCode(error, options?.code);
102-
103-
this.instrumentPostHogEventImpl({
104-
command: process.argv.join(" "),
105-
properties: {
106-
failed: true,
107-
source: "task",
108-
error,
109-
errorCode: code
110-
}
111-
});
112-
113-
if (shouldReportToSentry(code)) {
114-
this.captureException(error ?? new CliError({ message: message ?? "", code }), code);
115-
}
96+
reportError(this, error, { ...options, message });
11697
}
11798

11899
public captureException(error: unknown, code?: CliError.Code): void {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { PosthogEvent } from "@fern-api/task-context";
2+
import { CliError, resolveErrorCode, shouldReportToSentry, TaskAbortSignal } from "@fern-api/task-context";
3+
4+
export interface ErrorReporter {
5+
instrumentPostHogEvent: (event: PosthogEvent) => void;
6+
captureException: (error: unknown, code?: CliError.Code) => void;
7+
}
8+
9+
export function reportError(
10+
reporter: ErrorReporter,
11+
error: unknown,
12+
options?: { message?: string; code?: CliError.Code }
13+
): void {
14+
if (error instanceof TaskAbortSignal) {
15+
return;
16+
}
17+
const code = resolveErrorCode(error, options?.code);
18+
reporter.instrumentPostHogEvent({
19+
command: process.argv.join(" "),
20+
properties: {
21+
failed: true,
22+
errorCode: code
23+
}
24+
});
25+
if (shouldReportToSentry(code)) {
26+
reporter.captureException(error ?? new CliError({ message: options?.message ?? "", code }), code);
27+
}
28+
}

0 commit comments

Comments
 (0)