8000 fix: check for H3 error cause before re-capturing · getsentry/sentry-javascript@97f1621 · GitHub
[go: up one dir, main page]

Skip to content

Commit 97f1621

Browse files
committed
fix: check for H3 error cause before re-capturing
1 parent 7496387 commit 97f1621

File tree

2 files changed

+166
-0
lines changed

2 files changed

+166
-0
lines changed

packages/nuxt/src/runtime/hooks/captureErrorHook.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ export async function sentryCaptureErrorHook(error: Error, errorContext: Capture
2525
if (error.statusCode >= 300 && error.statusCode < 500) {
2626
return;
2727
}
28+
29+
// Check if the cause (original error) was already captured by middleware instrumentation
30+
// H3 wraps errors, so we need to check the cause property
31+
if (
32+
'cause' in error &&
33+
typeof error.cause === 'object' &&
34+
error.cause !== null &&
35+
'__sentry_captured__' in error.cause
36+
) {
37+
return;
38+
}
2839
}
2940

3041
const { method, path } = {
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import * as SentryCore from '@sentry/core';
2+
import { H3Error } from 'h3';
3+
import type { CapturedErrorContext } from 'nitropack/types';
4+
import { beforeEach, describe, expect, it, vi } from 'vitest';
5+
import { sentryCaptureErrorHook } from '../../../src/runtime/hooks/captureErrorHook';
6+
7+
vi.mock('@sentry/core', async importOriginal => {
8+
const mod = await importOriginal();
9+
return {
10+
...(mod as any),
11+
captureException: vi.fn(),
12+
flushIfServerless: vi.fn(),
13+
getClient: vi.fn(),
14+
getCurrentScope: vi.fn(() => ({
15+
setTransactionName: vi.fn(),
16+
})),
17+
};
18+
});
19+
20+
vi.mock('../../../src/runtime/utils', () => ({
21+
extractErrorContext: vi.fn(() => ({ test: 'context' })),
22+
}));
23+
24+
describe('sentryCaptureErrorHook', () => {
25+
const mockErrorContext: CapturedErrorContext = {
26+
event: {
27+
_method: 'GET',
28+
_path: '/test-path',
29+
} as any,
30+
};
31+
32+
beforeEach(() => {
33+
vi.clearAllMocks();
34+
(SentryCore.getClient as any).mockReturnValue({
35+
getOptions: () => ({}),
36+
});
37+
(SentryCore.flushIfServerless as any).mockResolvedValue(undefined);
38+
});
39+
40+
it('should capture regular errors', async () => {
41+
const error = new Error('Test error');
42+
43+
await sentryCaptureErrorHook(error, mockErrorContext);
44+
45+
expect(SentryCore.captureException).toHaveBeenCalledWith(
46+
error,
47+
expect.objectContaining({
48+
mechanism: { handled: false, type: 'auto.function.nuxt.nitro' },
49+
}),
50+
);
51+
});
52+
53+
it('should skip H3Error with 4xx status codes', async () => {
54+
const error = new H3Error('Not found');
55+
error.statusCode = 404;
56+
57+
await sentryCaptureErrorHook(error, mockErrorContext);
58+
59+
expect(SentryCore.captureException).not.toHaveBeenCalled();
60+
});
61+
62+
it('should skip H3Error with 3xx status codes', async () => {
63+
const error = new H3Error('Redirect');
64+
error.statusCode = 302;
65+
66+
await sentryCaptureErrorHook(error, mockErrorContext);
67+
68+
expect(SentryCore.captureException).not.toHaveBeenCalled();
69+
});
70+
71+
it('should capture H3Error with 5xx status codes', async () => {
72+
const error = new H3Error('Server error');
73+
error.statusCode = 500;
74+
75+
await sentryCaptureErrorHook(error, mockErrorContext);
76+
77+
expect(SentryCore.captureException).toHaveBeenCalledWith(
78+
error,
79+
expect.objectContaining({
80+
mechanism: { handled: false, type: 'auto.function.nuxt.nitro' },
81+
}),
82+
);
83+
});
84+
85+
it('should skip H3Error when cause has __sentry_captured__ flag', async () => {
86+
const originalError = new Error('Original error');
87+
// Mark the original error as already captured by middleware
88+
Object.defineProperty(originalError, '__sentry_captured__', {
89+
value: true,
90+
enumerable: false,
91+
});
92+
93+
const h3Error = new H3Error('Wrapped error', { cause: originalError });
94+
h3Error.statusCode = 500;
95+
96+
await sentryCaptureErrorHook(h3Error, mockErrorContext);
97+
98+
expect(SentryCore.captureException).not.toHaveBeenCalled();
99+
});
100+
101+
it('should capture H3Error when cause does not have __sentry_captured__ flag', async () => {
102+
const originalError = new Error('Original error');
103+
const h3Error = new H3Error('Wrapped error', { cause: originalError });
104+
h3Error.statusCode = 500;
105+
106+
await sentryCaptureErrorHook(h3Error, mockErrorContext);
107+
108+
expect(SentryCore.captureException).toHaveBeenCalledWith(
109+
h3Error,
110+
expect.objectContaining({
111+
mechanism: { handled: false, type: 'auto.function.nuxt.nitro' },
112+
}),
113+
);
114+
});
115+
116+
it('should capture H3Error when cause is not an object', 9E88 async () => {
117+
const h3Error = new H3Error('Error with string cause', { cause: 'string cause' });
118+
h3Error.statusCode = 500;
119+
120+
await sentryCaptureErrorHook(h3Error, mockErrorContext);
121+
122+
expect(SentryCore.captureException).toHaveBeenCalledWith(
123+
h3Error,
124+
expect.objectContaining({
125+
mechanism: { handled: false, type: 'auto.function.nuxt.nitro' },
126+
}),
127+
);
128+
});
129+
130+
it('should capture H3Error when there is no cause', async () => {
131+
const h3Error = new H3Error('Error without cause');
132+
h3Error.statusCode = 500;
133+
134+
await sentryCaptureErrorHook(h3Error, mockErrorContext);
135+
136+
expect(SentryCore.captureException).toHaveBeenCalledWith(
137+
h3Error,
138+
expect.objectContaining({
139+
mechanism: { handled: false, type: 'auto.function.nuxt.nitro' },
140+
}),
141+
);
142+
});
143+
144+
it('should skip when enableNitroErrorHandler is false', async () => {
145+
(SentryCore.getClient as any).mockReturnValue({
146+
getOptions: () => ({ enableNitroErrorHandler: false }),
147+
});
148+
149+
const error = new Error('Test error');
150+
151+
await sentryCaptureErrorHook(error, mockErrorContext);
152+
153+
expect(SentryCore.captureException).not.toHaveBeenCalled();
154+
});
155+
});

0 commit comments

Comments
 (0)
0