From 98cf2c1048de099de318da41ed7d915055654010 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 22 Nov 2024 20:44:43 +0100 Subject: [PATCH 1/3] feat(nuxt): Add filter for not found source maps (devtools) --- packages/nuxt/src/server/sdk.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 81ab93e6ccb2..9b70762e5fcc 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -49,6 +49,25 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { ), ); + getGlobalScope().addEventProcessor( + Object.assign( + (event => { + if (event.exception?.values) { + const errorMsg = event.exception.values[0]?.value; + // The browser devtools try to get the source maps, but as client source maps may not be available there is going to be an error (no problem for the application though) + if (errorMsg?.match(/^ENOENT: no such file or directory, open '.*\/_nuxt\/.*\.js\.map'/)) { + options.debug && DEBUG_BUILD && logger.log('ClientSourceMapErrorFilter filtered error: ', errorMsg); + return null; + } + return event; + } else { + return event; + } + }) satisfies EventProcessor, + { id: 'ClientSourceMapErrorFilter' }, + ), + ); + return client; } From 3d614bd6d8964bfec0d3b8698f727f61a9f03113 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 22 Nov 2024 21:50:46 +0100 Subject: [PATCH 2/3] add tests --- packages/nuxt/src/server/sdk.ts | 81 +++++++++++++------------ packages/nuxt/test/server/sdk.test.ts | 87 ++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 40 deletions(-) diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 9b70762e5fcc..6c4dd4712a1a 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -26,49 +26,52 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { const client = initNode(sentryOptions); - getGlobalScope().addEventProcessor( - Object.assign( - (event => { - if (event.type === 'transaction') { - // Filter out transactions for Nuxt build assets - // This regex matches the default path to the nuxt-generated build assets (`_nuxt`). - // todo: the buildAssetDir could be changed in the nuxt config - change this to a more generic solution - if (event.transaction?.match(/^GET \/_nuxt\//)) { - options.debug && - DEBUG_BUILD && - logger.log('NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); - return null; - } + getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options)); + getGlobalScope().addEventProcessor(clientSourceMapErrorFilter(options)); - return event; - } else { - return event; - } - }) satisfies EventProcessor, - { id: 'NuxtLowQualityTransactionsFilter' }, - ), - ); + return client; +} - getGlobalScope().addEventProcessor( - Object.assign( - (event => { - if (event.exception?.values) { - const errorMsg = event.exception.values[0]?.value; - // The browser devtools try to get the source maps, but as client source maps may not be available there is going to be an error (no problem for the application though) - if (errorMsg?.match(/^ENOENT: no such file or directory, open '.*\/_nuxt\/.*\.js\.map'/)) { - options.debug && DEBUG_BUILD && logger.log('ClientSourceMapErrorFilter filtered error: ', errorMsg); - return null; - } - return event; - } else { - return event; - } - }) satisfies EventProcessor, - { id: 'ClientSourceMapErrorFilter' }, - ), +/** + * Filter out transactions for Nuxt build assets + * This regex matches the default path to the nuxt-generated build assets (`_nuxt`). + * + * Only exported for testing + */ +export function lowQualityTransactionsFilter(options: SentryNuxtServerOptions): EventProcessor { + return Object.assign( + (event => { + if (event.type === 'transaction' && event.transaction?.match(/^GET \/_nuxt\//)) { + // todo: the buildAssetDir could be changed in the nuxt config - change this to a more generic solution + options.debug && + DEBUG_BUILD && + logger.log('NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); + return null; + } else { + return event; + } + }) satisfies EventProcessor, + { id: 'NuxtLowQualityTransactionsFilter' }, ); +} - return client; +/** + * The browser devtools try to get the source maps, but as client source maps may not be available there is going to be an error (no problem for the application though). + * + * Only exported for testing + */ +export function clientSourceMapErrorFilter(options: SentryNuxtServerOptions): EventProcessor { + return Object.assign( + (event => { + const errorMsg = event.exception?.values?.[0]?.value; + if (errorMsg?.match(/^ENOENT: no such file or directory, open '.*\/_nuxt\/.*\.js\.map'/)) { + options.debug && DEBUG_BUILD && logger.log('NuxtClientSourceMapErrorFilter filtered error: ', errorMsg); + return null; + } + return event; + }) satisfies EventProcessor, + { id: 'NuxtClientSourceMapErrorFilter' }, + ); } function getNuxtDefaultIntegrations(options: NodeOptions): Integration[] { diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index 7ff68478e36d..b2cf356abf1a 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -1,13 +1,24 @@ import * as SentryNode from '@sentry/node'; import type { NodeClient } from '@sentry/node'; +import { Scope } from '@sentry/node'; +import { getGlobalScope } from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; +import type { EventProcessor } from '@sentry/types'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { SentryNuxtServerOptions } from '../../src/common/types'; import { init } from '../../src/server'; -import { mergeRegisterEsmLoaderHooks } from '../../src/server/sdk'; +import { clientSourceMapErrorFilter, mergeRegisterEsmLoaderHooks } from '../../src/server/sdk'; const nodeInit = vi.spyOn(SentryNode, 'init'); +let passedEventProcessors: EventProcessor[] = []; +const addEventProcessor = vi + .spyOn(getGlobalScope(), 'addEventProcessor') + .mockImplementation((eventProcessor: EventProcessor) => { + passedEventProcessors = [...passedEventProcessors, eventProcessor]; + return new Scope(); + }); + describe('Nuxt Server SDK', () => { describe('init', () => { beforeEach(() => { @@ -83,6 +94,80 @@ describe('Nuxt Server SDK', () => { expect.any(Object), ); }); + + it('registers an event processor', async () => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + expect(addEventProcessor).toHaveBeenCalledTimes(2); + expect(passedEventProcessors[0]?.id).toEqual('NuxtLowQualityTransactionsFilter'); + expect(passedEventProcessors[1]?.id).toEqual('NuxtClientSourceMapErrorFilter'); + }); + }); + + describe('clientSourceMapErrorFilter', () => { + const options = { debug: false }; + const filter = clientSourceMapErrorFilter(options); + + describe('filters out errors', () => { + it.each([ + [ + 'source map errors with leading /', + { + exception: { values: [{ value: "ENOENT: no such file or directory, open '/path/to/_nuxt/file.js.map'" }] }, + }, + ], + [ + 'source map errors without leading /', + { exception: { values: [{ value: "ENOENT: no such file or directory, open 'path/to/_nuxt/file.js.map'" }] } }, + ], + [ + 'source map errors with long path', + { + exception: { + values: [ + { + value: + "ENOENT: no such file or directory, open 'path/to/public/_nuxt/public/long/long/path/file.js.map'", + }, + ], + }, + }, + ], + ])('filters out %s', (_, event) => { + // @ts-expect-error Event type is not correct in tests + expect(filter(event)).toBeNull(); + }); + }); + + describe('does not filter out errors', () => { + it.each([ + ['other errors', { exception: { values: [{ value: 'Some other error' }] } }], + ['events with no exceptions', {}], + [ + 'events without _nuxt in path', + { + exception: { values: [{ value: "ENOENT: no such file or directory, open '/path/to/other/file.js.map'" }] }, + }, + ], + [ + 'source map errors with different casing', + { + exception: { values: [{ value: "ENOENT: No Such file or directory, open '/path/to/_nuxt/file.js.map'" }] }, + }, + ], + [ + 'non-source-map file', + { exception: { values: [{ value: "ENOENT: no such file or directory, open '/path/to/_nuxt/file.js'" }] } }, + ], + ['events with no exception values', { exception: { values: [] } }], + ['events with null exception value', { exception: { values: [null] } }], + ])('does not filter out %s', (_, event) => { + // @ts-expect-error Event type is not correct in tests + expect(filter(event)).toEqual(event); + }); + }); }); describe('mergeRegisterEsmLoaderHooks', () => { From 99cbf2f6f1afdd4ac49b5891eba346966d48e8a7 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 22 Nov 2024 22:44:47 +0100 Subject: [PATCH 3/3] fix tests --- packages/nuxt/test/server/sdk.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index b2cf356abf1a..56888afc9a79 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -11,14 +11,6 @@ import { clientSourceMapErrorFilter, mergeRegisterEsmLoaderHooks } from '../../s const nodeInit = vi.spyOn(SentryNode, 'init'); -let passedEventProcessors: EventProcessor[] = []; -const addEventProcessor = vi - .spyOn(getGlobalScope(), 'addEventProcessor') - .mockImplementation((eventProcessor: EventProcessor) => { - passedEventProcessors = [...passedEventProcessors, eventProcessor]; - return new Scope(); - }); - describe('Nuxt Server SDK', () => { describe('init', () => { beforeEach(() => { @@ -96,6 +88,14 @@ describe('Nuxt Server SDK', () => { }); it('registers an event processor', async () => { + let passedEventProcessors: EventProcessor[] = []; + const addEventProcessor = vi + .spyOn(getGlobalScope(), 'addEventProcessor') + .mockImplementation((eventProcessor: EventProcessor) => { + passedEventProcessors = [...passedEventProcessors, eventProcessor]; + return new Scope(); + }); + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', });