diff --git a/dev-packages/browser-integration-tests/suites/public-api/withScope/throwError/subject.js b/dev-packages/browser-integration-tests/suites/public-api/withScope/throwError/subject.js new file mode 100644 index 000000000000..67cc16af1d40 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/withScope/throwError/subject.js @@ -0,0 +1,7 @@ +Sentry.setTag('global', 'tag'); +setTimeout(() => { + Sentry.withScope(scope => { + scope.setTag('local', 'tag'); + throw new Error('test error'); + }); +}, 10); diff --git a/dev-packages/browser-integration-tests/suites/public-api/withScope/throwError/test.ts b/dev-packages/browser-integration-tests/suites/public-api/withScope/throwError/test.ts new file mode 100644 index 000000000000..cb21bebb8241 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/withScope/throwError/test.ts @@ -0,0 +1,33 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +/** + * Why does this test exist? + * + * We recently discovered that errors caught by global handlers will potentially loose scope data from the active scope + * where the error was thrown in. The simple example in this test (see subject.ts) demonstrates this behavior (in a + * browser environment but the same behavior applies to the server; see the test there). + * + * This test nevertheless covers the behavior so that we're aware. + */ +sentryTest( + 'withScope scope is NOT applied to thrown error caught by global handler', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + const ex = eventData.exception?.values ? eventData.exception.values[0] : undefined; + + // This tag is missing :( + expect(eventData.tags?.local).toBeUndefined(); + + expect(eventData.tags).toMatchObject({ + global: 'tag', + }); + expect(ex?.value).toBe('test error'); + }, +); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts new file mode 100644 index 000000000000..ad45cd5d6713 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts @@ -0,0 +1,31 @@ +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node-experimental'; +import express from 'express'; + +const app = express(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +app.use(Sentry.Handlers.requestHandler()); + +Sentry.setTag('global', 'tag'); + +app.get('/test/withScope', () => { + Sentry.withScope(scope => { + scope.setTag('local', 'tag'); + throw new Error('test_error'); + }); +}); + +app.get('/test/isolationScope', () => { + Sentry.getIsolationScope().setTag('isolation-scope', 'tag'); + throw new Error('isolation_test_error'); +}); + +app.use(Sentry.Handlers.errorHandler()); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts new file mode 100644 index 000000000000..f1bb9a1229b1 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts @@ -0,0 +1,89 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +/** + * Why does this test exist? + * + * We recently discovered that errors caught by global handlers will potentially loose scope data from the active scope + * where the error was originally thrown in. The simple example in this test (see subject.ts) demonstrates this behavior + * (in a Node environment but the same behavior applies to the browser; see the test there). + * + * This test nevertheless covers the behavior so that we're aware. + */ +test('withScope scope is NOT applied to thrown error caught by global handler', done => { + const runner = createRunner(__dirname, 'server.ts') + .ignore('session', 'sessions') + .expect({ + event: { + exception: { + values: [ + { + mechanism: { + type: 'middleware', + handled: false, + }, + type: 'Error', + value: 'test_error', + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + function: expect.any(String), + lineno: expect.any(Number), + colno: expect.any(Number), + }), + ]), + }, + }, + ], + }, + // 'local' tag is not applied to the event + tags: expect.not.objectContaining({ local: expect.anything() }), + }, + }) + .start(done); + + expect(() => runner.makeRequest('get', '/test/withScope')).rejects.toThrow(); +}); + +/** + * This test shows that the isolation scope set tags are applied correctly to the error. + */ +test('isolation scope is applied to thrown error caught by global handler', done => { + const runner = createRunner(__dirname, 'server.ts') + .ignore('session', 'sessions') + .expect({ + event: { + exception: { + values: [ + { + mechanism: { + type: 'middleware', + handled: false, + }, + type: 'Error', + value: 'isolation_test_error', + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + function: expect.any(String), + lineno: expect.any(Number), + colno: expect.any(Number), + }), + ]), + }, + }, + ], + }, + tags: { + global: 'tag', + 'isolation-scope': 'tag', + }, + }, + }) + .start(done); + + expect(() => runner.makeRequest('get', '/test/isolationScope')).rejects.toThrow(); +});