Skip to content

Commit

Permalink
feat(nuxt): Add server error hook (#12796)
Browse files Browse the repository at this point in the history
Reports errors thrown in nitro. Tests will be added when adding the E2E
test application.

closes #12795
  • Loading branch information
s1gr1d authored Jul 9, 2024
1 parent 0952ec4 commit 47e1b7e
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 1 deletion.
4 changes: 3 additions & 1 deletion packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { addPlugin, addPluginTemplate, createResolver, defineNuxtModule } from '@nuxt/kit';
import { addPlugin, addPluginTemplate, addServerPlugin, createResolver, defineNuxtModule } from '@nuxt/kit';
import type { SentryNuxtOptions } from './common/types';

export type ModuleOptions = SentryNuxtOptions;
Expand Down Expand Up @@ -44,6 +44,8 @@ export default defineNuxtModule<ModuleOptions>({
`import "${buildDirResolver.resolve(`/${serverConfigFile}`)}"\n` +
'export default defineNuxtPlugin(() => {})',
});

addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server'));
}
},
});
Expand Down
23 changes: 23 additions & 0 deletions packages/nuxt/src/runtime/plugins/sentry.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { captureException } from '@sentry/node';
import { H3Error } from 'h3';
import { defineNitroPlugin } from 'nitropack/runtime';
import { extractErrorContext } from '../utils';

export default defineNitroPlugin(nitroApp => {
nitroApp.hooks.hook('error', (error, errorContext) => {
// Do not handle 404 and 422
if (error instanceof H3Error) {
// Do not report if status code is 3xx or 4xx
if (error.statusCode >= 300 && error.statusCode < 500) {
return;
}
}

const structuredContext = extractErrorContext(errorContext);

captureException(error, {
captureContext: { contexts: { nuxt: structuredContext } },
mechanism: { handled: false },
});
});
});
28 changes: 28 additions & 0 deletions packages/nuxt/src/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Context } from '@sentry/types';
import { dropUndefinedKeys } from '@sentry/utils';
import type { CapturedErrorContext } from 'nitropack';

/**
* Extracts the relevant context information from the error context (H3Event in Nitro Error)
* and created a structured context object.
*/
export function extractErrorContext(errorContext: CapturedErrorContext): Context {
const structuredContext: Context = {
method: undefined,
path: undefined,
tags: undefined,
};

if (errorContext) {
if (errorContext.event) {
structuredContext.method = errorContext.event._method || undefined;
structuredContext.path = errorContext.event._path || undefined;
}

if (Array.isArray(errorContext.tags)) {
structuredContext.tags = errorContext.tags || undefined;
}
}

return dropUndefinedKeys(structuredContext);
}
79 changes: 79 additions & 0 deletions packages/nuxt/test/client/runtime/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect, it } from 'vitest';
import { extractErrorContext } from '../../../src/runtime/utils';

describe('extractErrorContext', () => {
it('returns empty object for undefined or empty context', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(extractErrorContext(undefined)).toEqual({});
expect(extractErrorContext({})).toEqual({});
});

it('extracts properties from errorContext and drops them if missing', () => {
const context = {
event: {
_method: 'GET',
_path: '/test',
},
tags: ['tag1', 'tag2'],
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(extractErrorContext(context)).toEqual({
method: 'GET',
path: '/test',
tags: ['tag1', 'tag2'],
});

const partialContext = {
event: {
_path: '/test',
},
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(extractErrorContext(partialContext)).toEqual({ path: '/test' });
});

it('handles errorContext.tags correctly, including when absent or of unexpected type', () => {
const contextWithTags = {
tags: ['tag1', 'tag2'],
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(extractErrorContext(contextWithTags)).toEqual({
tags: ['tag1', 'tag2'],
});

const contextWithoutTags = {
event: {},
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(extractErrorContext(contextWithoutTags)).toEqual({});

const contextWithInvalidTags = {
event: {},
tags: 'not-an-array',
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(extractErrorContext(contextWithInvalidTags)).toEqual({});
});

it('gracefully handles unexpected context structure without throwing errors', () => {
const weirdContext1 = {
unexpected: 'value',
};
const weirdContext2 = ['value'];
const weirdContext3 = 123;

expect(() => extractErrorContext(weirdContext1)).not.toThrow();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(() => extractErrorContext(weirdContext2)).not.toThrow();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(() => extractErrorContext(weirdContext3)).not.toThrow();
});
});

0 comments on commit 47e1b7e

Please sign in to comment.