diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 81ab93e6ccb2..6c4dd4712a1a 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -26,30 +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; +} + +/** + * 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..56888afc9a79 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -1,10 +1,13 @@ 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'); @@ -83,6 +86,88 @@ describe('Nuxt Server SDK', () => { expect.any(Object), ); }); + + 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', + }); + + 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', () => {