From dcadf221b6fe6090edcbfb443a3d425b5360488e Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 8 Jul 2024 14:53:00 +0200 Subject: [PATCH 1/6] feat(nuxt): Add server error hook --- packages/nuxt/src/module.ts | 4 +++- .../nuxt/src/runtime/plugins/sentry.server.ts | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 packages/nuxt/src/runtime/plugins/sentry.server.ts diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index b4b5a2d9d7ea..9c14abd6feea 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -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; @@ -44,6 +44,8 @@ export default defineNuxtModule({ `import "${buildDirResolver.resolve(`/${serverConfigFile}`)}"\n` + 'export default defineNuxtPlugin(() => {})', }); + + addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); } }, }); diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts new file mode 100644 index 000000000000..c642f862784b --- /dev/null +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -0,0 +1,23 @@ +import { captureException } from '@sentry/node'; +import { H3Error } from 'h3'; +import { defineNitroPlugin } from 'nitropack/runtime'; + +export default defineNitroPlugin(nitroApp => { + nitroApp.hooks.hook('error', (error, context) => { + // Do not handle 404 and 422 + if (error instanceof H3Error) { + if (error.statusCode === 404 || error.statusCode === 422) { + return; + } + } + + if (context) { + captureException(error, { + captureContext: { extra: { nuxt: context } }, + mechanism: { handled: false }, + }); + } else { + captureException(error); + } + }); +}); From 85359f86df6cd828a1765ce4427d5432cf061761 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 8 Jul 2024 14:56:31 +0200 Subject: [PATCH 2/6] add hint data --- packages/nuxt/src/runtime/plugins/sentry.server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index c642f862784b..56a9d98abd73 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -17,7 +17,7 @@ export default defineNitroPlugin(nitroApp => { mechanism: { handled: false }, }); } else { - captureException(error); + captureException(error, { mechanism: { handled: false } }); } }); }); From d77c2e50235e6e7dc4ce973881aa6397018e4852 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:36:41 +0200 Subject: [PATCH 3/6] Update packages/nuxt/src/runtime/plugins/sentry.server.ts --- packages/nuxt/src/runtime/plugins/sentry.server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 56a9d98abd73..9e39d779a877 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -13,7 +13,7 @@ export default defineNitroPlugin(nitroApp => { if (context) { captureException(error, { - captureContext: { extra: { nuxt: context } }, + captureContext: { contexts: { nuxt: context } }, mechanism: { handled: false }, }); } else { From 795d1eacd9bdec38907fdde8a4bd2c03b5854356 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Tue, 9 Jul 2024 10:35:11 +0200 Subject: [PATCH 4/6] extract structured context --- .../nuxt/src/runtime/plugins/sentry.server.ts | 9 ++- packages/nuxt/src/runtime/utils.ts | 28 +++++++ .../nuxt/test/client/runtime/utils.test.ts | 80 +++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 packages/nuxt/src/runtime/utils.ts create mode 100644 packages/nuxt/test/client/runtime/utils.test.ts diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 9e39d779a877..b1200fb94e6c 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -1,9 +1,10 @@ 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, context) => { + nitroApp.hooks.hook('error', (error, errorContext) => { // Do not handle 404 and 422 if (error instanceof H3Error) { if (error.statusCode === 404 || error.statusCode === 422) { @@ -11,9 +12,11 @@ export default defineNitroPlugin(nitroApp => { } } - if (context) { + if (errorContext) { + const structuredContext = extractErrorContext(errorContext); + captureException(error, { - captureContext: { contexts: { nuxt: context } }, + captureContext: { contexts: { nuxt: structuredContext } }, mechanism: { handled: false }, }); } else { diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts new file mode 100644 index 000000000000..60c9f5e8b153 --- /dev/null +++ b/packages/nuxt/src/runtime/utils.ts @@ -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 && errorContext.event) { + 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); +} diff --git a/packages/nuxt/test/client/runtime/utils.test.ts b/packages/nuxt/test/client/runtime/utils.test.ts new file mode 100644 index 000000000000..e16f23bb3f5b --- /dev/null +++ b/packages/nuxt/test/client/runtime/utils.test.ts @@ -0,0 +1,80 @@ +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 = { + event: {}, + 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(); + }); +}); From 9a6f43c845f8cae7d65e4bb9c02d6c81ab9b3dc5 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Tue, 9 Jul 2024 10:48:07 +0200 Subject: [PATCH 5/6] fix test --- packages/nuxt/src/runtime/utils.ts | 2 +- packages/nuxt/test/client/runtime/utils.test.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts index 60c9f5e8b153..07294806a546 100644 --- a/packages/nuxt/src/runtime/utils.ts +++ b/packages/nuxt/src/runtime/utils.ts @@ -13,7 +13,7 @@ export function extractErrorContext(errorContext: CapturedErrorContext): Context tags: undefined, }; - if (errorContext && errorContext.event) { + if (errorContext) { if (errorContext.event) { structuredContext.method = errorContext.event._method || undefined; structuredContext.path = errorContext.event._path || undefined; diff --git a/packages/nuxt/test/client/runtime/utils.test.ts b/packages/nuxt/test/client/runtime/utils.test.ts index e16f23bb3f5b..b0b039d52e54 100644 --- a/packages/nuxt/test/client/runtime/utils.test.ts +++ b/packages/nuxt/test/client/runtime/utils.test.ts @@ -37,7 +37,6 @@ describe('extractErrorContext', () => { it('handles errorContext.tags correctly, including when absent or of unexpected type', () => { const contextWithTags = { - event: {}, tags: ['tag1', 'tag2'], }; // eslint-disable-next-line @typescript-eslint/ban-ts-comment From 1af2673bd2750e45a41946a8e8bf8e14b4a27bd0 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Tue, 9 Jul 2024 15:25:09 +0200 Subject: [PATCH 6/6] review changes --- .../nuxt/src/runtime/plugins/sentry.server.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index b1200fb94e6c..f0a815375ec8 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -7,20 +7,17 @@ export default defineNitroPlugin(nitroApp => { nitroApp.hooks.hook('error', (error, errorContext) => { // Do not handle 404 and 422 if (error instanceof H3Error) { - if (error.statusCode === 404 || error.statusCode === 422) { + // Do not report if status code is 3xx or 4xx + if (error.statusCode >= 300 && error.statusCode < 500) { return; } } - if (errorContext) { - const structuredContext = extractErrorContext(errorContext); + const structuredContext = extractErrorContext(errorContext); - captureException(error, { - captureContext: { contexts: { nuxt: structuredContext } }, - mechanism: { handled: false }, - }); - } else { - captureException(error, { mechanism: { handled: false } }); - } + captureException(error, { + captureContext: { contexts: { nuxt: structuredContext } }, + mechanism: { handled: false }, + }); }); });