Skip to content
File

Blob: tests/client/lib/report-client-error.test.ts

typescript113 lines
1import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2 
3const initMock = vi.fn();
4const captureExceptionMock = vi.fn();
5const setTagMock = vi.fn();
6const setExtrasMock = vi.fn();
7const withScopeMock = vi.fn(
8 (callback: (scope: { setTag: typeof setTagMock; setExtras: typeof setExtrasMock }) => void) =>
9 callback({
10 setTag: setTagMock,
11 setExtras: setExtrasMock,
12 }),
13);
14 
15vi.mock("@sentry/react", () => ({
16 init: initMock,
17 captureException: captureExceptionMock,
18 withScope: withScopeMock,
19}));
20 
21beforeEach(() => {
22 vi.resetModules();
23 initMock.mockReset();
24 captureExceptionMock.mockReset();
25 setTagMock.mockReset();
26 setExtrasMock.mockReset();
27 withScopeMock.mockClear();
28 vi.spyOn(console, "error").mockImplementation(() => {});
29});
30 
31afterEach(() => {
32 vi.restoreAllMocks();
33});
34 
35function flushMicrotasks() {
36 return Promise.resolve().then(() => Promise.resolve());
37}
38 
39describe("reportClientError", () => {
40 it("queues reports until Sentry finishes loading, then flushes them through Sentry", async () => {
41 const { primeClientErrorReporting, reportClientError } = await import("@/client/lib/report-client-error");
42 
43 const initPromise = primeClientErrorReporting({
44 sentry_dsn: "https://public@example.ingest.sentry.io/1",
45 });
46 const error = new Error("boom");
47 reportClientError({ source: "test.queue", error, context: { pageId: "page-1" } });
48 
49 expect(initMock).not.toHaveBeenCalled();
50 expect(captureExceptionMock).not.toHaveBeenCalled();
51 expect(console.error).toHaveBeenCalledTimes(1);
52 
53 await initPromise;
54 await flushMicrotasks();
55 
56 expect(initMock).toHaveBeenCalledWith(
57 expect.objectContaining({
58 dsn: "https://public@example.ingest.sentry.io/1",
59 sendDefaultPii: false,
60 }),
61 );
62 expect(withScopeMock).toHaveBeenCalledTimes(1);
63 expect(setTagMock).toHaveBeenCalledWith("source", "test.queue");
64 expect(setExtrasMock).toHaveBeenCalledWith({ pageId: "page-1" });
65 expect(captureExceptionMock).toHaveBeenCalledWith(error);
66 });
67 
68 it("stays console-only when bootstrap config is missing", async () => {
69 const { primeClientErrorReporting, reportClientError } = await import("@/client/lib/report-client-error");
70 
71 await primeClientErrorReporting(null);
72 const error = new Error("boom without bootstrap");
73 reportClientError({ source: "test.retry", error, context: { pageId: "page-2" } });
74 
75 expect(initMock).not.toHaveBeenCalled();
76 expect(captureExceptionMock).not.toHaveBeenCalled();
77 expect(console.error).toHaveBeenCalledTimes(1);
78 });
79 
80 it("filters expected API errors out of Sentry while keeping console logging", async () => {
81 const { primeClientErrorReporting, reportClientError } = await import("@/client/lib/report-client-error");
82 
83 await primeClientErrorReporting({
84 sentry_dsn: "https://public@example.ingest.sentry.io/1",
85 });
86 
87 reportClientError({
88 source: "test.expected",
89 error: { error: "not_found", message: "missing" },
90 });
91 
92 expect(console.error).toHaveBeenCalledTimes(1);
93 expect(captureExceptionMock).not.toHaveBeenCalled();
94 });
95 
96 it("stays console-only when the runtime config has no Sentry DSN", async () => {
97 const { primeClientErrorReporting, reportClientError } = await import("@/client/lib/report-client-error");
98 
99 await primeClientErrorReporting({
100 sentry_dsn: null,
101 });
102 
103 reportClientError({
104 source: "test.console-only",
105 error: new Error("boom"),
106 });
107 
108 expect(initMock).not.toHaveBeenCalled();
109 expect(captureExceptionMock).not.toHaveBeenCalled();
110 expect(console.error).toHaveBeenCalledTimes(1);
111 });
112});