Skip to content

Commit 47e1b7e

Browse files
authored
feat(nuxt): Add server error hook (#12796)
Reports errors thrown in nitro. Tests will be added when adding the E2E test application. closes #12795
1 parent 0952ec4 commit 47e1b7e

File tree

4 files changed

+133
-1
lines changed

4 files changed

+133
-1
lines changed

packages/nuxt/src/module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3-
import { addPlugin, addPluginTemplate, createResolver, defineNuxtModule } from '@nuxt/kit';
3+
import { addPlugin, addPluginTemplate, addServerPlugin, createResolver, defineNuxtModule } from '@nuxt/kit';
44
import type { SentryNuxtOptions } from './common/types';
55

66
export type ModuleOptions = SentryNuxtOptions;
@@ -44,6 +44,8 @@ export default defineNuxtModule<ModuleOptions>({
4444
`import "${buildDirResolver.resolve(`/${serverConfigFile}`)}"\n` +
4545
'export default defineNuxtPlugin(() => {})',
4646
});
47+
48+
addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server'));
4749
}
4850
},
4951
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { captureException } from '@sentry/node';
2+
import { H3Error } from 'h3';
3+
import { defineNitroPlugin } from 'nitropack/runtime';
4+
import { extractErrorContext } from '../utils';
5+
6+
export default defineNitroPlugin(nitroApp => {
7+
nitroApp.hooks.hook('error', (error, errorContext) => {
8+
// Do not handle 404 and 422
9+
if (error instanceof H3Error) {
10+
// Do not report if status code is 3xx or 4xx
11+
if (error.statusCode >= 300 && error.statusCode < 500) {
12+
return;
13+
}
14+
}
15+
16+
const structuredContext = extractErrorContext(errorContext);
17+
18+
captureException(error, {
19+
captureContext: { contexts: { nuxt: structuredContext } },
20+
mechanism: { handled: false },
21+
});
22+
});
23+
});

packages/nuxt/src/runtime/utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { Context } from '@sentry/types';
2+
import { dropUndefinedKeys } from '@sentry/utils';
3+
import type { CapturedErrorContext } from 'nitropack';
4+
5+
/**
6+
* Extracts the relevant context information from the error context (H3Event in Nitro Error)
7+
* and created a structured context object.
8+
*/
9+
export function extractErrorContext(errorContext: CapturedErrorContext): Context {
10+
const structuredContext: Context = {
11+
method: undefined,
12+
path: undefined,
13+
tags: undefined,
14+
};
15+
16+
if (errorContext) {
17+
if (errorContext.event) {
18+
structuredContext.method = errorContext.event._method || undefined;
19+
structuredContext.path = errorContext.event._path || undefined;
20+
}
21+
22+
if (Array.isArray(errorContext.tags)) {
23+
structuredContext.tags = errorContext.tags || undefined;
24+
}
25+
}
26+
27+
return dropUndefinedKeys(structuredContext);
28+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { extractErrorContext } from '../../../src/runtime/utils';
3+
4+
describe('extractErrorContext', () => {
5+
it('returns empty object for undefined or empty context', () => {
6+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7+
// @ts-ignore
8+
expect(extractErrorContext(undefined)).toEqual({});
9+
expect(extractErrorContext({})).toEqual({});
10+
});
11+
12+
it('extracts properties from errorContext and drops them if missing', () => {
13+
const context = {
14+
event: {
15+
_method: 'GET',
16+
_path: '/test',
17+
},
18+
tags: ['tag1', 'tag2'],
19+
};
20+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
21+
// @ts-ignore
22+
expect(extractErrorContext(context)).toEqual({
23+
method: 'GET',
24+
path: '/test',
25+
tags: ['tag1', 'tag2'],
26+
});
27+
28+
const partialContext = {
29+
event: {
30+
_path: '/test',
31+
},
32+
};
33+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
34+
// @ts-ignore
35+
expect(extractErrorContext(partialContext)).toEqual({ path: '/test' });
36+
});
37+
38+
it('handles errorContext.tags correctly, including when absent or of unexpected type', () => {
39+
const contextWithTags = {
40+
tags: ['tag1', 'tag2'],
41+
};
42+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
43+
// @ts-ignore
44+
expect(extractErrorContext(contextWithTags)).toEqual({
45+
tags: ['tag1', 'tag2'],
46+
});
47+
48+
const contextWithoutTags = {
49+
event: {},
50+
};
51+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
52+
// @ts-ignore
53+
expect(extractErrorContext(contextWithoutTags)).toEqual({});
54+
55+
const contextWithInvalidTags = {
56+
event: {},
57+
tags: 'not-an-array',
58+
};
59+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
60+
// @ts-ignore
61+
expect(extractErrorContext(contextWithInvalidTags)).toEqual({});
62+
});
63+
64+
it('gracefully handles unexpected context structure without throwing errors', () => {
65+
const weirdContext1 = {
66+
unexpected: 'value',
67+
};
68+
const weirdContext2 = ['value'];
69+
const weirdContext3 = 123;
70+
71+
expect(() => extractErrorContext(weirdContext1)).not.toThrow();
72+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
73+
// @ts-ignore
74+
expect(() => extractErrorContext(weirdContext2)).not.toThrow();
75+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
76+
// @ts-ignore
77+
expect(() => extractErrorContext(weirdContext3)).not.toThrow();
78+
});
79+
});

0 commit comments

Comments
 (0)