From 6d1a251ebc836accbedf2c408f11200674f65714 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 12 Nov 2024 14:36:45 -0500 Subject: [PATCH 01/29] fix(mongo): rewrite Buffer as ? during serialization (#14071) --- .../suites/tracing/mongodb/test.ts | 12 ++-- .../node/src/integrations/tracing/mongo.ts | 46 ++++++++++++ .../test/integrations/tracing/mongo.test.ts | 72 +++++++++++++++++++ 3 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 packages/node/test/integrations/tracing/mongo.test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts index 59c50d32ebdc..92fc857ed4e8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts @@ -71,12 +71,10 @@ describe('MongoDB experimental Test', () => { 'db.connection_string': expect.any(String), 'net.peer.name': expect.any(String), 'net.peer.port': expect.any(Number), - 'db.statement': - '{"title":"?","_id":{"_bsontype":"?","id":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?"}}}', + 'db.statement': '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', 'otel.kind': 'CLIENT', }, - description: - '{"title":"?","_id":{"_bsontype":"?","id":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?"}}}', + description: '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', op: 'db', origin: 'auto.db.otel.mongo', }), @@ -162,12 +160,10 @@ describe('MongoDB experimental Test', () => { 'db.connection_string': expect.any(String), 'net.peer.name': expect.any(String), 'net.peer.port': expect.any(Number), - 'db.statement': - '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?","12":"?","13":"?","14":"?","15":"?"}}}]}', + 'db.statement': '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', 'otel.kind': 'CLIENT', }, - description: - '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?","12":"?","13":"?","14":"?","15":"?"}}}]}', + description: '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', op: 'db', origin: 'auto.db.otel.mongo', }), diff --git a/packages/node/src/integrations/tracing/mongo.ts b/packages/node/src/integrations/tracing/mongo.ts index 5e42f5611db8..5f4d4e66a8a6 100644 --- a/packages/node/src/integrations/tracing/mongo.ts +++ b/packages/node/src/integrations/tracing/mongo.ts @@ -11,12 +11,58 @@ export const instrumentMongo = generateInstrumentOnce( INTEGRATION_NAME, () => new MongoDBInstrumentation({ + dbStatementSerializer: _defaultDbStatementSerializer, responseHook(span) { addOriginToSpan(span, 'auto.db.otel.mongo'); }, }), ); +/** + * Replaces values in document with '?', hiding PII and helping grouping. + */ +export function _defaultDbStatementSerializer(commandObj: Record): string { + const resultObj = _scrubStatement(commandObj); + return JSON.stringify(resultObj); +} + +function _scrubStatement(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(element => _scrubStatement(element)); + } + + if (isCommandObj(value)) { + const initial: Record = {}; + return Object.entries(value) + .map(([key, element]) => [key, _scrubStatement(element)]) + .reduce((prev, current) => { + if (isCommandEntry(current)) { + prev[current[0]] = current[1]; + } + return prev; + }, initial); + } + + // A value like string or number, possible contains PII, scrub it + return '?'; +} + +function isCommandObj(value: Record | unknown): value is Record { + return typeof value === 'object' && value !== null && !isBuffer(value); +} + +function isBuffer(value: unknown): boolean { + let isBuffer = false; + if (typeof Buffer !== 'undefined') { + isBuffer = Buffer.isBuffer(value); + } + return isBuffer; +} + +function isCommandEntry(value: [string, unknown] | unknown): value is [string, unknown] { + return Array.isArray(value); +} + const _mongoIntegration = (() => { return { name: INTEGRATION_NAME, diff --git a/packages/node/test/integrations/tracing/mongo.test.ts b/packages/node/test/integrations/tracing/mongo.test.ts new file mode 100644 index 000000000000..29571c07babe --- /dev/null +++ b/packages/node/test/integrations/tracing/mongo.test.ts @@ -0,0 +1,72 @@ +import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; + +import { + _defaultDbStatementSerializer, + instrumentMongo, + mongoIntegration, +} from '../../../src/integrations/tracing/mongo'; +import { INSTRUMENTED } from '../../../src/otel/instrument'; + +jest.mock('@opentelemetry/instrumentation-mongodb'); + +describe('Mongo', () => { + beforeEach(() => { + jest.clearAllMocks(); + delete INSTRUMENTED.Mongo; + + (MongoDBInstrumentation as unknown as jest.SpyInstance).mockImplementation(() => { + return { + setTracerProvider: () => undefined, + setMeterProvider: () => undefined, + getConfig: () => ({}), + setConfig: () => ({}), + enable: () => undefined, + }; + }); + }); + + it('defaults are correct for instrumentMongo', () => { + instrumentMongo(); + + expect(MongoDBInstrumentation).toHaveBeenCalledTimes(1); + expect(MongoDBInstrumentation).toHaveBeenCalledWith({ + dbStatementSerializer: expect.any(Function), + responseHook: expect.any(Function), + }); + }); + + it('defaults are correct for mongoIntegration', () => { + mongoIntegration().setupOnce!(); + + expect(MongoDBInstrumentation).toHaveBeenCalledTimes(1); + expect(MongoDBInstrumentation).toHaveBeenCalledWith({ + responseHook: expect.any(Function), + dbStatementSerializer: expect.any(Function), + }); + }); + + describe('_defaultDbStatementSerializer', () => { + it('rewrites strings as ?', () => { + const serialized = _defaultDbStatementSerializer({ + find: 'foo', + }); + expect(JSON.parse(serialized).find).toBe('?'); + }); + + it('rewrites nested strings as ?', () => { + const serialized = _defaultDbStatementSerializer({ + find: { + inner: 'foo', + }, + }); + expect(JSON.parse(serialized).find.inner).toBe('?'); + }); + + it('rewrites Buffer as ?', () => { + const serialized = _defaultDbStatementSerializer({ + find: Buffer.from('foo', 'utf8'), + }); + expect(JSON.parse(serialized).find).toBe('?'); + }); + }); +}); From 335da3705428869b647df9e87441fbad21975eef Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 13 Nov 2024 10:33:23 +0100 Subject: [PATCH 02/29] test: Make node integration test runner more resilient (#14245) Noticed while debugging some test problems, that if axios throws an error (e.g. you get a 500 error), jest cannot serialize the error because it contains recursive data, leading to super hard to debug error messages. This makes our node integration test runner more resilient by normalizing errors we get there to ensure circular references are resolved. ## Update While working on this, I found some further, actually more serious problems - some tests were simply not running at all. Especially all the session aggregrate tests were simply not being run and just "passed" accidentally. The core problem there was that the path to the scenario was incorrect, so nothing was even started, plus some things where we seemed to catch errors and still pass (?? I do not think I fixed all of the issues there, but at least some of them...) Now, we assert that the scenario file actually exists. Plus, instead of setting `.expectError()` on the whole runner, you now have to pass this to a specific `makeRequest` call as an optional option, and it will also assert if we expect an error _but do not get one_. This way, the tests can be more explicit and clear in what they do. --- dev-packages/node-integration-tests/README.md | 9 - .../handle-error-scope-data-loss/test.ts | 14 +- .../handle-error-tracesSampleRate-0/test.ts | 7 +- .../test.ts | 7 +- .../baggage-header-assign/test.ts | 30 +- .../test.ts | 6 +- .../baggage-other-vendors/test.ts | 6 +- .../sentry-trace/trace-header-assign/test.ts | 4 +- .../express/setupExpressErrorHandler/test.ts | 4 +- .../startSpan/with-nested-spans/test.ts | 3 +- .../crashed-session-aggregate/test.ts | 20 +- .../errored-session-aggregate/test.ts | 20 +- .../sessions/exited-session-aggregate/test.ts | 20 +- .../suites/sessions/server.ts | 18 +- .../suites/tracing/connect/test.ts | 2 - .../suites/tracing/hapi/test.ts | 12 +- .../suites/tracing/meta-tags/test.ts | 12 +- .../utils/assertions.ts | 78 +++++ .../utils/defaults/server.ts | 5 - .../node-integration-tests/utils/index.ts | 31 -- .../node-integration-tests/utils/runner.ts | 266 +++++++++--------- 21 files changed, 290 insertions(+), 284 deletions(-) create mode 100644 dev-packages/node-integration-tests/utils/assertions.ts delete mode 100644 dev-packages/node-integration-tests/utils/defaults/server.ts diff --git a/dev-packages/node-integration-tests/README.md b/dev-packages/node-integration-tests/README.md index ab1ce5e834de..3f7abd7b5727 100644 --- a/dev-packages/node-integration-tests/README.md +++ b/dev-packages/node-integration-tests/README.md @@ -14,20 +14,11 @@ suites/ |---- scenario_2.ts [optional extra test scenario] |---- server_with_mongo.ts [optional custom server] |---- server_with_postgres.ts [optional custom server] -utils/ -|---- defaults/ - |---- server.ts [default Express server configuration] ``` The tests are grouped by their scopes, such as `public-api` or `tracing`. In every group of tests, there are multiple folders containing test scenarios and assertions. -Tests run on Express servers (a server instance per test). By default, a simple server template inside -`utils/defaults/server.ts` is used. Every server instance runs on a different port. - -A custom server configuration can be used, supplying a script that exports a valid express server instance as default. -`runServer` utility function accepts an optional `serverPath` argument for this purpose. - `scenario.ts` contains the initialization logic and the test subject. By default, `{TEST_DIR}/scenario.ts` is used, but `runServer` also accepts an optional `scenarioPath` argument for non-standard usage. 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 index ecf69671b9f4..58d4a299174c 100644 --- 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 @@ -14,7 +14,7 @@ afterAll(() => { * 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') + createRunner(__dirname, 'server.ts') .expect({ event: { exception: { @@ -42,16 +42,15 @@ test('withScope scope is NOT applied to thrown error caught by global handler', tags: expect.not.objectContaining({ local: expect.anything() }), }, }) - .start(done); - - expect(() => runner.makeRequest('get', '/test/withScope')).rejects.toThrow(); + .start(done) + .makeRequest('get', '/test/withScope', { expectError: true }); }); /** * 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') + createRunner(__dirname, 'server.ts') .expect({ event: { exception: { @@ -81,7 +80,6 @@ test('isolation scope is applied to thrown error caught by global handler', done }, }, }) - .start(done); - - expect(() => runner.makeRequest('get', '/test/isolationScope')).rejects.toThrow(); + .start(done) + .makeRequest('get', '/test/isolationScope', { expectError: true }); }); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts index 955d725ae0c5..3ad6a3d2068f 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts @@ -5,7 +5,7 @@ afterAll(() => { }); test('should capture and send Express controller error with txn name if tracesSampleRate is 0', done => { - const runner = createRunner(__dirname, 'server.ts') + createRunner(__dirname, 'server.ts') .ignore('transaction') .expect({ event: { @@ -33,7 +33,6 @@ test('should capture and send Express controller error with txn name if tracesSa transaction: 'GET /test/express/:id', }, }) - .start(done); - - expect(() => runner.makeRequest('get', '/test/express/123')).rejects.toThrow(); + .start(done) + .makeRequest('get', '/test/express/123', { expectError: true }); }); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts index cb43073fa994..b02d74016ad4 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts @@ -5,7 +5,7 @@ afterAll(() => { }); test('should capture and send Express controller error if tracesSampleRate is not set.', done => { - const runner = createRunner(__dirname, 'server.ts') + createRunner(__dirname, 'server.ts') .ignore('transaction') .expect({ event: { @@ -32,7 +32,6 @@ test('should capture and send Express controller error if tracesSampleRate is no }, }, }) - .start(done); - - expect(() => runner.makeRequest('get', '/test/express/123')).rejects.toThrow(); + .start(done) + .makeRequest('get', '/test/express/123', { expectError: true }); }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts index 4ec29414868c..0ee5ca2204f5 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts @@ -9,7 +9,9 @@ test('Should overwrite baggage if the incoming request already has Sentry baggag const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + headers: { + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + }, }); expect(response).toBeDefined(); @@ -25,8 +27,10 @@ test('Should propagate sentry trace baggage data from an incoming to an outgoing const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + }, }); expect(response).toBeDefined(); @@ -42,8 +46,10 @@ test('Should not propagate baggage data from an incoming to an outgoing request const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '', - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + headers: { + 'sentry-trace': '', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + }, }); expect(response).toBeDefined(); @@ -59,7 +65,9 @@ test('Should not propagate baggage if sentry-trace header is present in incoming const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + }, }); expect(response).toBeDefined(); @@ -74,8 +82,10 @@ test('Should not propagate baggage and ignore original 3rd party baggage entries const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'foo=bar', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'foo=bar', + }, }); expect(response).toBeDefined(); @@ -107,7 +117,9 @@ test('Should populate Sentry and ignore 3rd party content if sentry-trace header const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - baggage: 'foo=bar,bar=baz', + headers: { + baggage: 'foo=bar,bar=baz', + }, }); expect(response).toBeDefined(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts index 9af5d4456c89..0e083f5c2dc6 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts @@ -9,8 +9,10 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an const runner = createRunner(__dirname, 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'sentry-release=2.1.0,sentry-environment=myEnv', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-release=2.1.0,sentry-environment=myEnv', + }, }); expect(response).toBeDefined(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts index dd3c0f8cddd7..2403da850d9d 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts @@ -9,8 +9,10 @@ test('should merge `baggage` header of a third party vendor with the Sentry DSC const runner = createRunner(__dirname, 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + }, }); expect(response).toBeDefined(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts index 071f02f83647..1ef9c11aff70 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts @@ -10,7 +10,9 @@ test('Should assign `sentry-trace` header which sets parent trace id of an outgo const runner = createRunner(__dirname, 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', + }, }); expect(response).toBeDefined(); diff --git a/dev-packages/node-integration-tests/suites/express/setupExpressErrorHandler/test.ts b/dev-packages/node-integration-tests/suites/express/setupExpressErrorHandler/test.ts index 97ff6e3fa769..ffc702d63057 100644 --- a/dev-packages/node-integration-tests/suites/express/setupExpressErrorHandler/test.ts +++ b/dev-packages/node-integration-tests/suites/express/setupExpressErrorHandler/test.ts @@ -22,9 +22,9 @@ describe('express setupExpressErrorHandler', () => { .start(done); // this error is filtered & ignored - expect(() => runner.makeRequest('get', '/test1')).rejects.toThrow(); + runner.makeRequest('get', '/test1', { expectError: true }); // this error is actually captured - expect(() => runner.makeRequest('get', '/test2')).rejects.toThrow(); + runner.makeRequest('get', '/test2', { expectError: true }); }); }); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts index f7a1678e1eb6..5cf4dc8c8c40 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts @@ -1,5 +1,6 @@ import type { SpanJSON } from '@sentry/types'; -import { assertSentryTransaction, cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import { assertSentryTransaction } from '../../../../utils/assertions'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); diff --git a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts index 6e8a86e627d9..0500c702189a 100644 --- a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts @@ -4,16 +4,10 @@ afterEach(() => { cleanupChildProcesses(); }); -test('should aggregate successful and crashed sessions', async () => { - let _done: undefined | (() => void); - const promise = new Promise(resolve => { - _done = resolve; - }); - - const runner = createRunner(__dirname, 'server.ts') +test('should aggregate successful and crashed sessions', done => { + const runner = createRunner(__dirname, '..', 'server.ts') .ignore('transaction', 'event') .unignore('sessions') - .expectError() .expect({ sessions: { aggregates: [ @@ -25,11 +19,9 @@ test('should aggregate successful and crashed sessions', async () => { ], }, }) - .start(_done); - - runner.makeRequest('get', '/success'); - runner.makeRequest('get', '/error_unhandled'); - runner.makeRequest('get', '/success_next'); + .start(done); - await promise; + runner.makeRequest('get', '/test/success'); + runner.makeRequest('get', '/test/error_unhandled', { expectError: true }); + runner.makeRequest('get', '/test/success_next'); }); diff --git a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts index 383bfca96062..1159d092fdd7 100644 --- a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts @@ -4,16 +4,10 @@ afterEach(() => { cleanupChildProcesses(); }); -test('should aggregate successful, crashed and erroneous sessions', async () => { - let _done: undefined | (() => void); - const promise = new Promise(resolve => { - _done = resolve; - }); - - const runner = createRunner(__dirname, 'server.ts') +test('should aggregate successful, crashed and erroneous sessions', done => { + const runner = createRunner(__dirname, '..', 'server.ts') .ignore('transaction', 'event') .unignore('sessions') - .expectError() .expect({ sessions: { aggregates: [ @@ -26,11 +20,9 @@ test('should aggregate successful, crashed and erroneous sessions', async () => ], }, }) - .start(_done); - - runner.makeRequest('get', '/success'); - runner.makeRequest('get', '/error_handled'); - runner.makeRequest('get', '/error_unhandled'); + .start(done); - await promise; + runner.makeRequest('get', '/test/success'); + runner.makeRequest('get', '/test/error_handled'); + runner.makeRequest('get', '/test/error_unhandled', { expectError: true }); }); diff --git a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts index 23b80109fa43..465761e76224 100644 --- a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts @@ -4,16 +4,10 @@ afterEach(() => { cleanupChildProcesses(); }); -test('should aggregate successful sessions', async () => { - let _done: undefined | (() => void); - const promise = new Promise(resolve => { - _done = resolve; - }); - - const runner = createRunner(__dirname, 'server.ts') +test('should aggregate successful sessions', done => { + const runner = createRunner(__dirname, '..', 'server.ts') .ignore('transaction', 'event') .unignore('sessions') - .expectError() .expect({ sessions: { aggregates: [ @@ -24,11 +18,9 @@ test('should aggregate successful sessions', async () => { ], }, }) - .start(_done); - - runner.makeRequest('get', '/success'); - runner.makeRequest('get', '/success_next'); - runner.makeRequest('get', '/success_slow'); + .start(done); - await promise; + runner.makeRequest('get', '/test/success'); + runner.makeRequest('get', '/test/success_next'); + runner.makeRequest('get', '/test/success_slow'); }); diff --git a/dev-packages/node-integration-tests/suites/sessions/server.ts b/dev-packages/node-integration-tests/suites/sessions/server.ts index e06f00ef486a..2415140b6140 100644 --- a/dev-packages/node-integration-tests/suites/sessions/server.ts +++ b/dev-packages/node-integration-tests/suites/sessions/server.ts @@ -1,26 +1,24 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import type { SessionFlusher } from '@sentry/core'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import express from 'express'; const app = express(); -// ### Taken from manual tests ### -// Hack that resets the 60s default flush interval, and replaces it with just a one second interval const flusher = (Sentry.getClient() as Sentry.NodeClient)['_sessionFlusher'] as SessionFlusher; -let flusherIntervalId = flusher && flusher['_intervalId']; - -clearInterval(flusherIntervalId); - -flusherIntervalId = flusher['_intervalId'] = setInterval(() => flusher?.flush(), 2000); - -setTimeout(() => clearInterval(flusherIntervalId), 4000); +// Flush after 2 seconds (to avoid waiting for the default 60s) +setTimeout(() => { + flusher?.flush(); +}, 2000); app.get('/test/success', (_req, res) => { res.send('Success!'); @@ -52,4 +50,4 @@ app.get('/test/error_handled', (_req, res) => { Sentry.setupExpressErrorHandler(app); -export default app; +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/connect/test.ts b/dev-packages/node-integration-tests/suites/tracing/connect/test.ts index dd14c2277f7b..a416656f6355 100644 --- a/dev-packages/node-integration-tests/suites/tracing/connect/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/connect/test.ts @@ -45,7 +45,6 @@ describe('connect auto-instrumentation', () => { test('CJS - should capture errors in `connect` middleware.', done => { createRunner(__dirname, 'scenario.js') .ignore('transaction') - .expectError() .expect({ event: EXPECTED_EVENT }) .start(done) .makeRequest('get', '/error'); @@ -55,7 +54,6 @@ describe('connect auto-instrumentation', () => { createRunner(__dirname, 'scenario.js') .ignore('event') .expect({ transaction: { transaction: 'GET /error' } }) - .expectError() .start(done) .makeRequest('get', '/error'); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/hapi/test.ts b/dev-packages/node-integration-tests/suites/tracing/hapi/test.ts index 8bb3bfdb0796..4bd995777248 100644 --- a/dev-packages/node-integration-tests/suites/tracing/hapi/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/hapi/test.ts @@ -50,9 +50,8 @@ describe('hapi auto-instrumentation', () => { }, }) .expect({ event: EXPECTED_ERROR_EVENT }) - .expectError() .start(done) - .makeRequest('get', '/error'); + .makeRequest('get', '/error', { expectError: true }); }); test('CJS - should assign parameterized transactionName to error.', done => { @@ -64,9 +63,8 @@ describe('hapi auto-instrumentation', () => { }, }) .ignore('transaction') - .expectError() .start(done) - .makeRequest('get', '/error/123'); + .makeRequest('get', '/error/123', { expectError: true }); }); test('CJS - should handle returned Boom errors in routes.', done => { @@ -77,9 +75,8 @@ describe('hapi auto-instrumentation', () => { }, }) .expect({ event: EXPECTED_ERROR_EVENT }) - .expectError() .start(done) - .makeRequest('get', '/boom-error'); + .makeRequest('get', '/boom-error', { expectError: true }); }); test('CJS - should handle promise rejections in routes.', done => { @@ -90,8 +87,7 @@ describe('hapi auto-instrumentation', () => { }, }) .expect({ event: EXPECTED_ERROR_EVENT }) - .expectError() .start(done) - .makeRequest('get', '/promise-error'); + .makeRequest('get', '/promise-error', { expectError: true }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts b/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts index ab63b1c9cb35..7c94d30b686a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts @@ -12,8 +12,10 @@ describe('getTraceMetaTags', () => { const runner = createRunner(__dirname, 'server.js').start(); const response = await runner.makeRequest('get', '/test', { - 'sentry-trace': `${traceId}-${parentSpanId}-1`, - baggage: 'sentry-environment=production', + headers: { + 'sentry-trace': `${traceId}-${parentSpanId}-1`, + baggage: 'sentry-environment=production', + }, }); // @ts-ignore - response is defined, types just don't reflect it @@ -61,8 +63,10 @@ describe('getTraceMetaTags', () => { const runner = createRunner(__dirname, 'server-sdk-disabled.js').start(); const response = await runner.makeRequest('get', '/test', { - 'sentry-trace': `${traceId}-${parentSpanId}-1`, - baggage: 'sentry-environment=production', + headers: { + 'sentry-trace': `${traceId}-${parentSpanId}-1`, + baggage: 'sentry-environment=production', + }, }); // @ts-ignore - response is defined, types just don't reflect it diff --git a/dev-packages/node-integration-tests/utils/assertions.ts b/dev-packages/node-integration-tests/utils/assertions.ts new file mode 100644 index 000000000000..68ce3941ff92 --- /dev/null +++ b/dev-packages/node-integration-tests/utils/assertions.ts @@ -0,0 +1,78 @@ +import type { + ClientReport, + Envelope, + Event, + SerializedCheckIn, + SerializedSession, + SessionAggregates, + TransactionEvent, +} from '@sentry/types'; +import { SDK_VERSION } from '@sentry/utils'; + +/** + * Asserts against a Sentry Event ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryEvent = (actual: Event, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + ...expected, + }); +}; + +/** + * Asserts against a Sentry Transaction ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryTransaction = (actual: TransactionEvent, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + timestamp: expect.anything(), + start_timestamp: expect.anything(), + spans: expect.any(Array), + type: 'transaction', + ...expected, + }); +}; + +export function assertSentrySession(actual: SerializedSession, expected: Partial): void { + expect(actual).toMatchObject({ + sid: expect.any(String), + ...expected, + }); +} + +export function assertSentrySessions(actual: SessionAggregates, expected: Partial): void { + expect(actual).toMatchObject({ + ...expected, + }); +} + +export function assertSentryCheckIn(actual: SerializedCheckIn, expected: Partial): void { + expect(actual).toMatchObject({ + check_in_id: expect.any(String), + ...expected, + }); +} + +export function assertSentryClientReport(actual: ClientReport, expected: Partial): void { + expect(actual).toMatchObject({ + ...expected, + }); +} + +export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { + expect(actual).toEqual({ + event_id: expect.any(String), + sent_at: expect.any(String), + sdk: { + name: 'sentry.javascript.node', + version: SDK_VERSION, + }, + ...expected, + }); +} diff --git a/dev-packages/node-integration-tests/utils/defaults/server.ts b/dev-packages/node-integration-tests/utils/defaults/server.ts deleted file mode 100644 index 3cf8cadab65a..000000000000 --- a/dev-packages/node-integration-tests/utils/defaults/server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import express from 'express'; - -const app = express(); - -export default app; diff --git a/dev-packages/node-integration-tests/utils/index.ts b/dev-packages/node-integration-tests/utils/index.ts index 8c12ec72e0d2..0beedd250980 100644 --- a/dev-packages/node-integration-tests/utils/index.ts +++ b/dev-packages/node-integration-tests/utils/index.ts @@ -43,37 +43,6 @@ export const conditionalTest = (allowedVersion: { min?: number; max?: number }): : describe; }; -/** - * Asserts against a Sentry Event ignoring non-deterministic properties - * - * @param {Record} actual - * @param {Record} expected - */ -export const assertSentryEvent = (actual: Record, expected: Record): void => { - expect(actual).toMatchObject({ - event_id: expect.any(String), - timestamp: expect.anything(), - ...expected, - }); -}; - -/** - * Asserts against a Sentry Transaction ignoring non-deterministic properties - * - * @param {Record} actual - * @param {Record} expected - */ -export const assertSentryTransaction = (actual: Record, expected: Record): void => { - expect(actual).toMatchObject({ - event_id: expect.any(String), - timestamp: expect.anything(), - start_timestamp: expect.anything(), - spans: expect.any(Array), - type: 'transaction', - ...expected, - }); -}; - /** * Parses response body containing an Envelope * diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index bde5bd06cd21..1cbd9ade2e67 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import { spawn, spawnSync } from 'child_process'; +import { existsSync } from 'fs'; import { join } from 'path'; -import { SDK_VERSION } from '@sentry/node'; import type { ClientReport, Envelope, @@ -13,59 +13,19 @@ import type { SessionAggregates, TransactionEvent, } from '@sentry/types'; +import { normalize } from '@sentry/utils'; import axios from 'axios'; +import { + assertEnvelopeHeader, + assertSentryCheckIn, + assertSentryClientReport, + assertSentryEvent, + assertSentrySession, + assertSentrySessions, + assertSentryTransaction, +} from './assertions'; import { createBasicSentryServer } from './server'; -export function assertSentryEvent(actual: Event, expected: Event): void { - expect(actual).toMatchObject({ - event_id: expect.any(String), - ...expected, - }); -} - -export function assertSentrySession(actual: SerializedSession, expected: Partial): void { - expect(actual).toMatchObject({ - sid: expect.any(String), - ...expected, - }); -} - -export function assertSentryTransaction(actual: Event, expected: Partial): void { - expect(actual).toMatchObject({ - event_id: expect.any(String), - timestamp: expect.anything(), - start_timestamp: expect.anything(), - spans: expect.any(Array), - type: 'transaction', - ...expected, - }); -} - -export function assertSentryCheckIn(actual: SerializedCheckIn, expected: Partial): void { - expect(actual).toMatchObject({ - check_in_id: expect.any(String), - ...expected, - }); -} - -export function assertSentryClientReport(actual: ClientReport, expected: Partial): void { - expect(actual).toMatchObject({ - ...expected, - }); -} - -export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { - expect(actual).toEqual({ - event_id: expect.any(String), - sent_at: expect.any(String), - sdk: { - name: 'sentry.javascript.node', - version: SDK_VERSION, - }, - ...expected, - }); -} - const CLEANUP_STEPS = new Set(); export function cleanupChildProcesses(): void { @@ -130,8 +90,7 @@ async function runDockerCompose(options: DockerOptions): Promise { function newData(data: Buffer): void { const text = data.toString('utf8'); - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log(text); + if (process.env.DEBUG) log(text); for (const match of options.readyMatches) { if (text.includes(match)) { @@ -147,24 +106,31 @@ async function runDockerCompose(options: DockerOptions): Promise { }); } +type ExpectedEvent = Partial | ((event: Event) => void); +type ExpectedTransaction = Partial | ((event: TransactionEvent) => void); +type ExpectedSession = Partial | ((event: SerializedSession) => void); +type ExpectedSessions = Partial | ((event: SessionAggregates) => void); +type ExpectedCheckIn = Partial | ((event: SerializedCheckIn) => void); +type ExpectedClientReport = Partial | ((event: ClientReport) => void); + type Expected = | { - event: Partial | ((event: Event) => void); + event: ExpectedEvent; } | { - transaction: Partial | ((event: TransactionEvent) => void); + transaction: ExpectedTransaction; } | { - session: Partial | ((event: SerializedSession) => void); + session: ExpectedSession; } | { - sessions: Partial | ((event: SessionAggregates) => void); + sessions: ExpectedSessions; } | { - check_in: Partial | ((event: SerializedCheckIn) => void); + check_in: ExpectedCheckIn; } | { - client_report: Partial | ((event: ClientReport) => void); + client_report: ExpectedClientReport; }; type ExpectedEnvelopeHeader = @@ -178,16 +144,19 @@ type ExpectedEnvelopeHeader = export function createRunner(...paths: string[]) { const testPath = join(...paths); + if (!existsSync(testPath)) { + throw new Error(`Test scenario not found: ${testPath}`); + } + const expectedEnvelopes: Expected[] = []; let expectedEnvelopeHeaders: ExpectedEnvelopeHeader[] | undefined = undefined; const flags: string[] = []; // By default, we ignore session & sessions - const ignored: EnvelopeItemType[] = ['session', 'sessions']; + const ignored: Set = new Set(['session', 'sessions']); let withEnv: Record = {}; let withSentryServer = false; let dockerOptions: DockerOptions | undefined; let ensureNoErrorOutput = false; - let expectError = false; const logs: string[] = []; if (testPath.endsWith('.ts')) { @@ -207,10 +176,6 @@ export function createRunner(...paths: string[]) { expectedEnvelopeHeaders.push(expected); return this; }, - expectError: function () { - expectError = true; - return this; - }, withEnv: function (env: Record) { withEnv = env; return this; @@ -224,15 +189,12 @@ export function createRunner(...paths: string[]) { return this; }, ignore: function (...types: EnvelopeItemType[]) { - ignored.push(...types); + types.forEach(t => ignored.add(t)); return this; }, unignore: function (...types: EnvelopeItemType[]) { for (const t of types) { - const pos = ignored.indexOf(t); - if (pos > -1) { - ignored.splice(pos, 1); - } + ignored.delete(t); } return this; }, @@ -254,7 +216,7 @@ export function createRunner(...paths: string[]) { function complete(error?: Error): void { child?.kill(); - done?.(error); + done?.(normalize(error)); } /** Called after each expect callback to check if we're complete */ @@ -269,7 +231,7 @@ export function createRunner(...paths: string[]) { for (const item of envelope[1]) { const envelopeItemType = item[0].type; - if (ignored.includes(envelopeItemType)) { + if (ignored.has(envelopeItemType)) { continue; } @@ -307,58 +269,25 @@ export function createRunner(...paths: string[]) { } if ('event' in expected) { - const event = item[1] as Event; - if (typeof expected.event === 'function') { - expected.event(event); - } else { - assertSentryEvent(event, expected.event); - } - + expectErrorEvent(item[1] as Event, expected.event); expectCallbackCalled(); - } - - if ('transaction' in expected) { - const event = item[1] as TransactionEvent; - if (typeof expected.transaction === 'function') { - expected.transaction(event); - } else { - assertSentryTransaction(event, expected.transaction); - } - + } else if ('transaction' in expected) { + expectTransactionEvent(item[1] as TransactionEvent, expected.transaction); expectCallbackCalled(); - } - - if ('session' in expected) { - const session = item[1] as SerializedSession; - if (typeof expected.session === 'function') { - expected.session(session); - } else { - assertSentrySession(session, expected.session); - } - + } else if ('session' in expected) { + expectSessionEvent(item[1] as SerializedSession, expected.session); expectCallbackCalled(); - } - - if ('check_in' in expected) { - const checkIn = item[1] as SerializedCheckIn; - if (typeof expected.check_in === 'function') { - expected.check_in(checkIn); - } else { - assertSentryCheckIn(checkIn, expected.check_in); - } - + } else if ('sessions' in expected) { + expectSessionsEvent(item[1] as SessionAggregates, expected.sessions); expectCallbackCalled(); - } - - if ('client_report' in expected) { - const clientReport = item[1] as ClientReport; - if (typeof expected.client_report === 'function') { - expected.client_report(clientReport); - } else { - assertSentryClientReport(clientReport, expected.client_report); - } - + } else if ('check_in' in expected) { + expectCheckInEvent(item[1] as SerializedCheckIn, expected.check_in); + expectCallbackCalled(); + } else if ('client_report' in expected) { + expectClientReport(item[1] as ClientReport, expected.client_report); expectCallbackCalled(); + } else { + throw new Error(`Unhandled expected envelope item type: ${JSON.stringify(expected)}`); } } catch (e) { complete(e as Error); @@ -397,8 +326,7 @@ export function createRunner(...paths: string[]) { ? { ...process.env, ...withEnv, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` } : { ...process.env, ...withEnv }; - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN); + if (process.env.DEBUG) log('starting scenario', testPath, flags, env.SENTRY_DSN); child = spawn('node', [...flags, testPath], { env }); @@ -425,8 +353,7 @@ export function createRunner(...paths: string[]) { // Pass error to done to end the test quickly child.on('error', e => { - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('scenario error', e); + if (process.env.DEBUG) log('scenario error', e); complete(e); }); @@ -465,8 +392,7 @@ export function createRunner(...paths: string[]) { logs.push(line.trim()); buffer = Buffer.from(buffer.subarray(splitIndex + 1)); - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('line', line); + if (process.env.DEBUG) log('line', line); tryParseEnvelopeFromStdoutLine(line); } }); @@ -483,35 +409,95 @@ export function createRunner(...paths: string[]) { makeRequest: async function ( method: 'get' | 'post', path: string, - headers: Record = {}, - data?: any, // axios accept any as data + options: { headers?: Record; data?: unknown; expectError?: boolean } = {}, ): Promise { try { await waitFor(() => scenarioServerPort !== undefined); } catch (e) { complete(e as Error); - return undefined; + return; } const url = `http://localhost:${scenarioServerPort}${path}`; - if (expectError) { - try { - if (method === 'get') { - await axios.get(url, { headers }); - } else { - await axios.post(url, data, { headers }); - } - } catch (e) { + const data = options.data; + const headers = options.headers || {}; + const expectError = options.expectError || false; + + if (process.env.DEBUG) log('making request', method, url, headers, data); + + try { + const res = + method === 'post' ? await axios.post(url, data, { headers }) : await axios.get(url, { headers }); + + if (expectError) { + complete(new Error(`Expected request to "${path}" to fail, but got a ${res.status} response`)); + return; + } + + return res.data; + } catch (e) { + if (expectError) { return; } + + complete(e as Error); return; - } else if (method === 'get') { - return (await axios.get(url, { headers })).data; - } else { - return (await axios.post(url, data, { headers })).data; } }, }; }, }; } + +function log(...args: unknown[]): void { + // eslint-disable-next-line no-console + console.log(...args.map(arg => normalize(arg))); +} + +function expectErrorEvent(item: Event, expected: ExpectedEvent): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryEvent(item, expected); + } +} + +function expectTransactionEvent(item: TransactionEvent, expected: ExpectedTransaction): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryTransaction(item, expected); + } +} + +function expectSessionEvent(item: SerializedSession, expected: ExpectedSession): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentrySession(item, expected); + } +} + +function expectSessionsEvent(item: SessionAggregates, expected: ExpectedSessions): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentrySessions(item, expected); + } +} + +function expectCheckInEvent(item: SerializedCheckIn, expected: ExpectedCheckIn): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryCheckIn(item, expected); + } +} + +function expectClientReport(item: ClientReport, expected: ExpectedClientReport): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryClientReport(item, expected); + } +} From 9e37c316509da54ba0917bf1c54568a8e408eb10 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 13 Nov 2024 10:51:34 +0100 Subject: [PATCH 03/29] chore(deps): Bump @rollup/plugin-commonjs (#14250) --- packages/nextjs/package.json | 2 +- yarn.lock | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 5ee59e1dae29..086ec4ecf2b3 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -79,7 +79,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation-http": "0.53.0", "@opentelemetry/semantic-conventions": "^1.27.0", - "@rollup/plugin-commonjs": "26.0.1", + "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "8.38.0", "@sentry/core": "8.38.0", "@sentry/node": "8.38.0", diff --git a/yarn.lock b/yarn.lock index da1c9aa37efc..53acaa3dc244 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8163,17 +8163,18 @@ dependencies: slash "^4.0.0" -"@rollup/plugin-commonjs@26.0.1": - version "26.0.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz#16d4d6e54fa63021249a292b50f27c0b0f1a30d8" - integrity sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ== +"@rollup/plugin-commonjs@28.0.1": + version "28.0.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz#e2138e31cc0637676dc3d5cae7739131f7cd565e" + integrity sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA== dependencies: "@rollup/pluginutils" "^5.0.1" commondir "^1.0.1" estree-walker "^2.0.2" - glob "^10.4.1" + fdir "^6.2.0" is-reference "1.2.1" magic-string "^0.30.3" + picomatch "^4.0.2" "@rollup/plugin-commonjs@^25.0.4", "@rollup/plugin-commonjs@^25.0.8": version "25.0.8" @@ -18433,16 +18434,16 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fdir@^6.2.0, fdir@^6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" + integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== + fdir@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.3.0.tgz#fcca5a23ea20e767b15e081ee13b3e6488ee0bb0" integrity sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ== -fdir@^6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" - integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== - fflate@0.8.1, fflate@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.1.tgz#1ed92270674d2ad3c73f077cd0acf26486dae6c9" @@ -19372,7 +19373,7 @@ glob@^10.2.2: minipass "^5.0.0 || ^6.0.2" path-scurry "^1.10.0" -glob@^10.3.10, glob@^10.4.1: +glob@^10.3.10: version "10.4.1" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== From e0282b6c40d0416a8c724c5412aed4d51c68484f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 13 Nov 2024 11:49:16 +0100 Subject: [PATCH 04/29] test(remix): Stop relying on node-integration-tests in remix (#14253) Oops, https://github.com/getsentry/sentry-javascript/pull/14245 broke our remix integration tests. Instead of re-exporting this from a path (which is not resolved nicely etc. and not reflected by our dependency graph) we can just inline the two methods we need here, they are not too complicated. --- .../integration/test/server/utils/helpers.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/remix/test/integration/test/server/utils/helpers.ts b/packages/remix/test/integration/test/server/utils/helpers.ts index 981be12f314a..909d8d1671ae 100644 --- a/packages/remix/test/integration/test/server/utils/helpers.ts +++ b/packages/remix/test/integration/test/server/utils/helpers.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { createRequestHandler } from '@remix-run/express'; /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import * as Sentry from '@sentry/node'; -import type { EnvelopeItemType } from '@sentry/types'; +import type { EnvelopeItemType, Event, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; import type { AxiosRequestConfig } from 'axios'; import axios from 'axios'; @@ -14,8 +14,6 @@ import type { HttpTerminator } from 'http-terminator'; import { createHttpTerminator } from 'http-terminator'; import nock from 'nock'; -export * from '../../../../../../../dev-packages/node-integration-tests/utils'; - type DataCollectorOptions = { // Optional custom URL url?: string; @@ -284,3 +282,33 @@ export class RemixTestEnv extends TestEnv { const parseEnvelope = (body: string): Array> => { return body.split('\n').map(e => JSON.parse(e)); }; + +/** + * Asserts against a Sentry Event ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryEvent = (actual: Event, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + ...expected, + }); +}; + +/** + * Asserts against a Sentry Transaction ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryTransaction = (actual: TransactionEvent, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + timestamp: expect.anything(), + start_timestamp: expect.anything(), + spans: expect.any(Array), + type: 'transaction', + ...expected, + }); +}; From 9a844847ed1be1171afbb6f53e7527b61f894f93 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 13 Nov 2024 12:23:46 +0100 Subject: [PATCH 05/29] chore(dev-deps): Bump nestjs deps & add nestjs v8 E2E test (#14148) We support v8 but only test v10. By adding a (very basic) e2e test app for v8 of nestjs we can ensure this continues to work (as long as we want to support it). Also bumps nestjs dev deps to hopefully fix some security warnings for outdates deps (e.g. https://github.com/getsentry/sentry-javascript/security/dependabot/376). Note for the future: There seems to be some problem with the node-integration-tests and module resolution. As soon as the nestjs version there matches the one from the nestjs package - which means it will hoist the dependency to the workspace-level node_modules folder - the tests start failing weirdly. For now, I "fixed" this by using a different version of nestjs in node-integration-tests from nestjs. Since we want to get rid of the node-specific part there anyhow in v9, I figured it's not worth it to put more work into fixing this properly... --- .github/workflows/build.yml | 1 + .../test-applications/nestjs-8/.gitignore | 56 ++ .../test-applications/nestjs-8/.npmrc | 2 + .../test-applications/nestjs-8/nest-cli.json | 8 + .../test-applications/nestjs-8/package.json | 49 ++ .../nestjs-8/playwright.config.mjs | 7 + .../nestjs-8/src/app.controller.ts | 124 +++ .../nestjs-8/src/app.module.ts | 29 + .../nestjs-8/src/app.service.ts | 102 +++ .../nestjs-8/src/async-example.interceptor.ts | 17 + .../nestjs-8/src/example-1.interceptor.ts | 15 + .../nestjs-8/src/example-2.interceptor.ts | 10 + .../src/example-global-filter.exception.ts | 5 + .../nestjs-8/src/example-global.filter.ts | 19 + .../src/example-local-filter.exception.ts | 5 + .../nestjs-8/src/example-local.filter.ts | 19 + .../nestjs-8/src/example.guard.ts | 10 + .../nestjs-8/src/example.middleware.ts | 12 + .../nestjs-8/src/instrument.ts | 12 + .../test-applications/nestjs-8/src/main.ts | 15 + .../nestjs-8/start-event-proxy.mjs | 6 + .../nestjs-8/tests/cron-decorator.test.ts | 55 ++ .../nestjs-8/tests/errors.test.ts | 166 ++++ .../nestjs-8/tests/span-decorator.test.ts | 79 ++ .../nestjs-8/tests/transactions.test.ts | 729 ++++++++++++++++++ .../nestjs-8/tsconfig.build.json | 4 + .../test-applications/nestjs-8/tsconfig.json | 22 + .../node-integration-tests/package.json | 6 +- .../nestjs-errors-no-express/scenario.ts | 4 +- .../suites/tracing/nestjs-errors/scenario.ts | 4 +- .../tracing/nestjs-no-express/scenario.ts | 4 +- .../suites/tracing/nestjs/scenario.ts | 2 - packages/nestjs/package.json | 4 +- yarn.lock | 74 +- 34 files changed, 1623 insertions(+), 53 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/nest-cli.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/app.controller.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/app.module.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/async-example.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/example-1.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/example-2.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global-filter.exception.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global.filter.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local-filter.exception.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local.filter.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/example.guard.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/example.middleware.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/instrument.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/tests/cron-decorator.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/tests/span-decorator.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.build.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c17b81246e79..79fc0ff4a73c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -928,6 +928,7 @@ jobs: 'node-nestjs-basic', 'node-nestjs-distributed-tracing', 'nestjs-basic', + 'nestjs-8', 'nestjs-distributed-tracing', 'nestjs-with-submodules', 'nestjs-with-submodules-decorator', diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/.gitignore b/dev-packages/e2e-tests/test-applications/nestjs-8/.gitignore new file mode 100644 index 000000000000..4b56acfbebf4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/.gitignore @@ -0,0 +1,56 @@ +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-8/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/nest-cli.json b/dev-packages/e2e-tests/test-applications/nestjs-8/nest-cli.json new file mode 100644 index 000000000000..f9aa683b1ad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json new file mode 100644 index 000000000000..9cf681c33ef5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json @@ -0,0 +1,49 @@ +{ + "name": "nestjs-8", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test": "playwright test", + "test:build": "pnpm install", + "test:assert": "pnpm test" + }, + "dependencies": { + "@nestjs/common": "^8.0.0", + "@nestjs/core": "^8.0.0", + "@nestjs/microservices": "^8.0.0", + "@nestjs/schedule": "^4.1.0", + "@nestjs/platform-express": "^8.0.0", + "@sentry/nestjs": "latest || *", + "@sentry/types": "latest || *", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/express": "^4.17.17", + "@types/node": "18.15.1", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-loader": "^9.4.3", + "tsconfig-paths": "^4.2.0", + "typescript": "^4.9.5" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nestjs-8/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.controller.ts new file mode 100644 index 000000000000..77e25a72dad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.controller.ts @@ -0,0 +1,124 @@ +import { Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common'; +import { flush } from '@sentry/nestjs'; +import { AppService } from './app.service'; +import { AsyncInterceptor } from './async-example.interceptor'; +import { ExampleInterceptor1 } from './example-1.interceptor'; +import { ExampleInterceptor2 } from './example-2.interceptor'; +import { ExampleExceptionGlobalFilter } from './example-global-filter.exception'; +import { ExampleExceptionLocalFilter } from './example-local-filter.exception'; +import { ExampleLocalFilter } from './example-local.filter'; +import { ExampleGuard } from './example.guard'; + +@Controller() +@UseFilters(ExampleLocalFilter) +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get('test-transaction') + testTransaction() { + return this.appService.testTransaction(); + } + + @Get('test-middleware-instrumentation') + testMiddlewareInstrumentation() { + return this.appService.testSpan(); + } + + @Get('test-guard-instrumentation') + @UseGuards(ExampleGuard) + testGuardInstrumentation() { + return {}; + } + + @Get('test-interceptor-instrumentation') + @UseInterceptors(ExampleInterceptor1, ExampleInterceptor2) + testInterceptorInstrumentation() { + return this.appService.testSpan(); + } + + @Get('test-async-interceptor-instrumentation') + @UseInterceptors(AsyncInterceptor) + testAsyncInterceptorInstrumentation() { + return this.appService.testSpan(); + } + + @Get('test-pipe-instrumentation/:id') + testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { + return { value: id }; + } + + @Get('test-exception/:id') + async testException(@Param('id') id: string) { + return this.appService.testException(id); + } + + @Get('test-expected-400-exception/:id') + async testExpected400Exception(@Param('id') id: string) { + return this.appService.testExpected400Exception(id); + } + + @Get('test-expected-500-exception/:id') + async testExpected500Exception(@Param('id') id: string) { + return this.appService.testExpected500Exception(id); + } + + @Get('test-expected-rpc-exception/:id') + async testExpectedRpcException(@Param('id') id: string) { + return this.appService.testExpectedRpcException(id); + } + + @Get('test-span-decorator-async') + async testSpanDecoratorAsync() { + return { result: await this.appService.testSpanDecoratorAsync() }; + } + + @Get('test-span-decorator-sync') + async testSpanDecoratorSync() { + return { result: await this.appService.testSpanDecoratorSync() }; + } + + @Get('kill-test-cron') + async killTestCron() { + this.appService.killTestCron(); + } + + @Get('flush') + async flush() { + await flush(); + } + + @Get('example-exception-global-filter') + async exampleExceptionGlobalFilter() { + throw new ExampleExceptionGlobalFilter(); + } + + @Get('example-exception-local-filter') + async exampleExceptionLocalFilter() { + throw new ExampleExceptionLocalFilter(); + } + + @Get('test-service-use') + testServiceWithUseMethod() { + return this.appService.use(); + } + + @Get('test-service-transform') + testServiceWithTransform() { + return this.appService.transform(); + } + + @Get('test-service-intercept') + testServiceWithIntercept() { + return this.appService.intercept(); + } + + @Get('test-service-canActivate') + testServiceWithCanActivate() { + return this.appService.canActivate(); + } + + @Get('test-function-name') + testFunctionName() { + return this.appService.getFunctionName(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.module.ts new file mode 100644 index 000000000000..3de3c82dc925 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.module.ts @@ -0,0 +1,29 @@ +import { MiddlewareConsumer, Module } from '@nestjs/common'; +import { APP_FILTER } from '@nestjs/core'; +import { ScheduleModule } from '@nestjs/schedule'; +import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { ExampleGlobalFilter } from './example-global.filter'; +import { ExampleMiddleware } from './example.middleware'; + +@Module({ + imports: [SentryModule.forRoot(), ScheduleModule.forRoot()], + controllers: [AppController], + providers: [ + AppService, + { + provide: APP_FILTER, + useClass: SentryGlobalFilter, + }, + { + provide: APP_FILTER, + useClass: ExampleGlobalFilter, + }, + ], +}) +export class AppModule { + configure(consumer: MiddlewareConsumer): void { + consumer.apply(ExampleMiddleware).forRoutes('test-middleware-instrumentation'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts new file mode 100644 index 000000000000..72aef6947a6c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts @@ -0,0 +1,102 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; +import { Cron, SchedulerRegistry } from '@nestjs/schedule'; +import * as Sentry from '@sentry/nestjs'; +import { SentryCron, SentryTraced } from '@sentry/nestjs'; +import type { MonitorConfig } from '@sentry/types'; + +const monitorConfig: MonitorConfig = { + schedule: { + type: 'crontab', + value: '* * * * *', + }, +}; + +@Injectable() +export class AppService { + constructor(private schedulerRegistry: SchedulerRegistry) {} + + testTransaction() { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + } + + testSpan() { + // span that should not be a child span of the middleware span + Sentry.startSpan({ name: 'test-controller-span' }, () => {}); + } + + testException(id: string) { + throw new Error(`This is an exception with id ${id}`); + } + + testExpected400Exception(id: string) { + throw new HttpException(`This is an expected 400 exception with id ${id}`, HttpStatus.BAD_REQUEST); + } + + testExpected500Exception(id: string) { + throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + + testExpectedRpcException(id: string) { + throw new RpcException(`This is an expected RPC exception with id ${id}`); + } + + @SentryTraced('wait and return a string') + async wait() { + await new Promise(resolve => setTimeout(resolve, 500)); + return 'test'; + } + + async testSpanDecoratorAsync() { + return await this.wait(); + } + + @SentryTraced('return a string') + getString(): { result: string } { + return { result: 'test' }; + } + + @SentryTraced('return the function name') + getFunctionName(): { result: string } { + return { result: this.getFunctionName.name }; + } + + async testSpanDecoratorSync() { + const returned = this.getString(); + // Will fail if getString() is async, because returned will be a Promise<> + return returned.result; + } + + /* + Actual cron schedule differs from schedule defined in config because Sentry + only supports minute granularity, but we don't want to wait (worst case) a + full minute for the tests to finish. + */ + @Cron('*/5 * * * * *', { name: 'test-cron-job' }) + @SentryCron('test-cron-slug', monitorConfig) + async testCron() { + console.log('Test cron!'); + } + + async killTestCron() { + this.schedulerRegistry.deleteCronJob('test-cron-job'); + } + + use() { + console.log('Test use!'); + } + + transform() { + console.log('Test transform!'); + } + + intercept() { + console.log('Test intercept!'); + } + + canActivate() { + console.log('Test canActivate!'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/async-example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/async-example.interceptor.ts new file mode 100644 index 000000000000..ac0ee60acc51 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/async-example.interceptor.ts @@ -0,0 +1,17 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; +import { tap } from 'rxjs'; + +@Injectable() +export class AsyncInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-async-interceptor-span' }, () => {}); + return Promise.resolve( + next.handle().pipe( + tap(() => { + Sentry.startSpan({ name: 'test-async-interceptor-span-after-route' }, () => {}); + }), + ), + ); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-1.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-1.interceptor.ts new file mode 100644 index 000000000000..81c9f70d30e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-1.interceptor.ts @@ -0,0 +1,15 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; +import { tap } from 'rxjs'; + +@Injectable() +export class ExampleInterceptor1 implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-interceptor-span-1' }, () => {}); + return next.handle().pipe( + tap(() => { + Sentry.startSpan({ name: 'test-interceptor-span-after-route' }, () => {}); + }), + ); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-2.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-2.interceptor.ts new file mode 100644 index 000000000000..2cf9dfb9e043 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-2.interceptor.ts @@ -0,0 +1,10 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; + +@Injectable() +export class ExampleInterceptor2 implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-interceptor-span-2' }, () => {}); + return next.handle().pipe(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global-filter.exception.ts new file mode 100644 index 000000000000..41981ba748fe --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global-filter.exception.ts @@ -0,0 +1,5 @@ +export class ExampleExceptionGlobalFilter extends Error { + constructor() { + super('Original global example exception!'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global.filter.ts new file mode 100644 index 000000000000..988696d0e13d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global.filter.ts @@ -0,0 +1,19 @@ +import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; +import { Request, Response } from 'express'; +import { ExampleExceptionGlobalFilter } from './example-global-filter.exception'; + +@Catch(ExampleExceptionGlobalFilter) +export class ExampleGlobalFilter implements ExceptionFilter { + catch(exception: BadRequestException, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + response.status(400).json({ + statusCode: 400, + timestamp: new Date().toISOString(), + path: request.url, + message: 'Example exception was handled by global filter!', + }); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local-filter.exception.ts new file mode 100644 index 000000000000..8f76520a3b94 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local-filter.exception.ts @@ -0,0 +1,5 @@ +export class ExampleExceptionLocalFilter extends Error { + constructor() { + super('Original local example exception!'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local.filter.ts new file mode 100644 index 000000000000..505217f5dcbd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local.filter.ts @@ -0,0 +1,19 @@ +import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; +import { Request, Response } from 'express'; +import { ExampleExceptionLocalFilter } from './example-local-filter.exception'; + +@Catch(ExampleExceptionLocalFilter) +export class ExampleLocalFilter implements ExceptionFilter { + catch(exception: BadRequestException, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + response.status(400).json({ + statusCode: 400, + timestamp: new Date().toISOString(), + path: request.url, + message: 'Example exception was handled by local filter!', + }); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/example.guard.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example.guard.ts new file mode 100644 index 000000000000..e12bbdc4e994 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example.guard.ts @@ -0,0 +1,10 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; + +@Injectable() +export class ExampleGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + Sentry.startSpan({ name: 'test-guard-span' }, () => {}); + return true; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/example.middleware.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example.middleware.ts new file mode 100644 index 000000000000..31d15c9372ea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example.middleware.ts @@ -0,0 +1,12 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; +import { NextFunction, Request, Response } from 'express'; + +@Injectable() +export class ExampleMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + // span that should be a child span of the middleware span + Sentry.startSpan({ name: 'test-middleware-span' }, () => {}); + next(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/instrument.ts new file mode 100644 index 000000000000..4f16ebb36d11 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/instrument.ts @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/nestjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/main.ts new file mode 100644 index 000000000000..71ce685f4d61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/main.ts @@ -0,0 +1,15 @@ +// Import this first +import './instrument'; + +// Import other modules +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +const PORT = 3030; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(PORT); +} + +bootstrap(); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-8/start-event-proxy.mjs new file mode 100644 index 000000000000..e771eb5dbc4b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nestjs-8', +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/cron-decorator.test.ts new file mode 100644 index 000000000000..dee95c1f1b01 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/cron-decorator.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; + +test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { + const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs-8', envelope => { + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'in_progress'; + }); + + const okEnvelopePromise = waitForEnvelopeItem('nestjs-8', envelope => { + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'ok'; + }); + + const inProgressEnvelope = await inProgressEnvelopePromise; + const okEnvelope = await okEnvelopePromise; + + expect(inProgressEnvelope[1]).toEqual( + expect.objectContaining({ + check_in_id: expect.any(String), + monitor_slug: 'test-cron-slug', + status: 'in_progress', + environment: 'qa', + monitor_config: { + schedule: { + type: 'crontab', + value: '* * * * *', + }, + }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + }), + ); + + expect(okEnvelope[1]).toEqual( + expect.objectContaining({ + check_in_id: expect.any(String), + monitor_slug: 'test-cron-slug', + status: 'ok', + environment: 'qa', + duration: expect.any(Number), + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + }), + ); + + // kill cron so tests don't get stuck + await fetch(`${baseURL}/kill-test-cron`); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/errors.test.ts new file mode 100644 index 000000000000..72570f43efc4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/errors.test.ts @@ -0,0 +1,166 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends exception to Sentry', async ({ baseURL }) => { + const errorEventPromise = waitForError('nestjs-8', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; + }); + + const response = await fetch(`${baseURL}/test-exception/123`); + expect(response.status).toBe(500); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception/123', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception/:id'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + span_id: expect.any(String), + }); +}); + +test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('nestjs-8', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /test-expected-400-exception/:id'; + }); + + waitForError('nestjs-8', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /test-expected-500-exception/:id'; + }); + + const transactionEventPromise400 = waitForTransaction('nestjs-8', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; + }); + + const transactionEventPromise500 = waitForTransaction('nestjs-8', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; + }); + + const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`); + expect(response400.status).toBe(400); + + const response500 = await fetch(`${baseURL}/test-expected-500-exception/123`); + expect(response500.status).toBe(500); + + await transactionEventPromise400; + await transactionEventPromise500; + + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); + +test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('nestjs-8', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /test-expected-rpc-exception/:id'; + }); + + const transactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id'; + }); + + const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`); + expect(response.status).toBe(500); + + await transactionEventPromise; + + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); + +test('Global exception filter registered in main module is applied and exception is not sent to Sentry', async ({ + baseURL, +}) => { + let errorEventOccurred = false; + + waitForError('nestjs-8', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by global filter!') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /example-exception-global-filter'; + }); + + const transactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return transactionEvent?.transaction === 'GET /example-exception-global-filter'; + }); + + const response = await fetch(`${baseURL}/example-exception-global-filter`); + const responseBody = await response.json(); + + expect(response.status).toBe(400); + expect(responseBody).toEqual({ + statusCode: 400, + timestamp: expect.any(String), + path: '/example-exception-global-filter', + message: 'Example exception was handled by global filter!', + }); + + await transactionEventPromise; + + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); + +test('Local exception filter registered in main module is applied and exception is not sent to Sentry', async ({ + baseURL, +}) => { + let errorEventOccurred = false; + + waitForError('nestjs-8', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by local filter!') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /example-exception-local-filter'; + }); + + const transactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return transactionEvent?.transaction === 'GET /example-exception-local-filter'; + }); + + const response = await fetch(`${baseURL}/example-exception-local-filter`); + const responseBody = await response.json(); + + expect(response.status).toBe(400); + expect(responseBody).toEqual({ + statusCode: 400, + timestamp: expect.any(String), + path: '/example-exception-local-filter', + message: 'Example exception was handled by local filter!', + }); + + await transactionEventPromise; + + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/span-decorator.test.ts new file mode 100644 index 000000000000..2aa097d5262c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/span-decorator.test.ts @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-span-decorator-async' + ); + }); + + const response = await fetch(`${baseURL}/test-span-decorator-async`); + const body = await response.json(); + + expect(body.result).toEqual('test'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'wait and return a string', + }, + description: 'wait', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + op: 'wait and return a string', + origin: 'manual', + }), + ]), + ); +}); + +test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-span-decorator-sync' + ); + }); + + const response = await fetch(`${baseURL}/test-span-decorator-sync`); + const body = await response.json(); + + expect(body.result).toEqual('test'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'return a string', + }, + description: 'getString', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + op: 'return a string', + origin: 'manual', + }), + ]), + ); +}); + +test('preserves original function name on decorated functions', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-function-name`); + const body = await response.json(); + + expect(body.result).toEqual('getFunctionName'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts new file mode 100644 index 000000000000..dc72d6c639e8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts @@ -0,0 +1,729 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + await fetch(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': 'http://localhost:3030/test-transaction', + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': '/test-transaction', + 'http.user_agent': 'node', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-transaction', + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + origin: 'auto.http.otel.http', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + data: { + 'express.name': '/test-transaction', + 'express.type': 'request_handler', + 'http.route': '/test-transaction', + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'request_handler.express', + }, + op: 'request_handler.express', + description: '/test-transaction', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.http.otel.express', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'test-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'child-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'auto.http.otel.nestjs', + 'sentry.op': 'handler.nestjs', + component: '@nestjs/core', + 'nestjs.version': expect.any(String), + 'nestjs.type': 'handler', + 'nestjs.callback': 'testTransaction', + }, + description: 'testTransaction', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'auto.http.otel.nestjs', + op: 'handler.nestjs', + }, + ]), + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); +}); + +test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-middleware-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-middleware-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleMiddleware', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + const exampleMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'ExampleMiddleware'); + const exampleMiddlewareSpanId = exampleMiddlewareSpan?.span_id; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-middleware-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'test-middleware-span'); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'ExampleMiddleware' is the parent of 'test-middleware-span' + expect(testMiddlewareSpan.parent_span_id).toBe(exampleMiddlewareSpanId); + + // 'ExampleMiddleware' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId); +}); + +test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({ + baseURL, +}) => { + const transactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-guard-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-guard-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleGuard', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + const exampleGuardSpan = transactionEvent.spans.find(span => span.description === 'ExampleGuard'); + const exampleGuardSpanId = exampleGuardSpan?.span_id; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-guard-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testGuardSpan = transactionEvent.spans.find(span => span.description === 'test-guard-span'); + + // 'ExampleGuard' is the parent of 'test-guard-span' + expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId); +}); + +test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/123') + ); + }); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/123`); + expect(response.status).toBe(200); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); + +test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/abc') + ); + }); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/abc`); + expect(response.status).toBe(400); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'unknown_error', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); + +test('API route transaction includes nest interceptor spans before route execution. Spans created in and after interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + // check if interceptor spans before route execution exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor1', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor2', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + // get interceptor spans + const exampleInterceptor1Span = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor1'); + const exampleInterceptor1SpanId = exampleInterceptor1Span?.span_id; + const exampleInterceptor2Span = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor2'); + const exampleInterceptor2SpanId = exampleInterceptor2Span?.span_id; + + // check if manually started spans exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span-1', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span-2', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptor1Span = transactionEvent.spans.find(span => span.description === 'test-interceptor-span-1'); + const testInterceptor2Span = transactionEvent.spans.find(span => span.description === 'test-interceptor-span-2'); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'ExampleInterceptor1' is the parent of 'test-interceptor-span-1' + expect(testInterceptor1Span.parent_span_id).toBe(exampleInterceptor1SpanId); + + // 'ExampleInterceptor1' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptor1SpanId); + + // 'ExampleInterceptor2' is the parent of 'test-interceptor-span-2' + expect(testInterceptor2Span.parent_span_id).toBe(exampleInterceptor2SpanId); + + // 'ExampleInterceptor2' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptor2SpanId); +}); + +test('API route transaction includes exactly one nest interceptor span after route execution. Spans created in controller and in interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + // check if interceptor spans after route execution exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'Interceptors - After Route', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + // check that exactly one after route span is sent + const allInterceptorSpansAfterRoute = transactionEvent.spans.filter( + span => span.description === 'Interceptors - After Route', + ); + expect(allInterceptorSpansAfterRoute.length).toBe(1); + + // get interceptor span + const exampleInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'Interceptors - After Route', + ); + const exampleInterceptorSpanAfterRouteId = exampleInterceptorSpanAfterRoute?.span_id; + + // check if manually started span in interceptor after route exists + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span-after-route', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'test-interceptor-span-after-route', + ); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'Interceptor - After Route' is the parent of 'test-interceptor-span-after-route' + expect(testInterceptorSpanAfterRoute.parent_span_id).toBe(exampleInterceptorSpanAfterRouteId); + + // 'Interceptor - After Route' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanAfterRouteId); +}); + +test('API route transaction includes nest async interceptor spans before route execution. Spans created in and after async interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + // check if interceptor spans before route execution exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'AsyncInterceptor', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + // get interceptor spans + const exampleAsyncInterceptor = transactionEvent.spans.find(span => span.description === 'AsyncInterceptor'); + const exampleAsyncInterceptorSpanId = exampleAsyncInterceptor?.span_id; + + // check if manually started spans exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-async-interceptor-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testAsyncInterceptorSpan = transactionEvent.spans.find( + span => span.description === 'test-async-interceptor-span', + ); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'AsyncInterceptor' is the parent of 'test-async-interceptor-span' + expect(testAsyncInterceptorSpan.parent_span_id).toBe(exampleAsyncInterceptorSpanId); + + // 'AsyncInterceptor' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleAsyncInterceptorSpanId); +}); + +test('API route transaction includes exactly one nest async interceptor span after route execution. Spans created in controller and in async interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-8', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + // check if interceptor spans after route execution exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'Interceptors - After Route', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + // check that exactly one after route span is sent + const allInterceptorSpansAfterRoute = transactionEvent.spans.filter( + span => span.description === 'Interceptors - After Route', + ); + expect(allInterceptorSpansAfterRoute.length).toBe(1); + + // get interceptor span + const exampleInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'Interceptors - After Route', + ); + const exampleInterceptorSpanAfterRouteId = exampleInterceptorSpanAfterRoute?.span_id; + + // check if manually started span in interceptor after route exists + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-async-interceptor-span-after-route', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'test-async-interceptor-span-after-route', + ); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'Interceptor - After Route' is the parent of 'test-interceptor-span-after-route' + expect(testInterceptorSpanAfterRoute.parent_span_id).toBe(exampleInterceptorSpanAfterRouteId); + + // 'Interceptor - After Route' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanAfterRouteId); +}); + +test('Calling use method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-use`); + expect(response.status).toBe(200); +}); + +test('Calling transform method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-transform`); + expect(response.status).toBe(200); +}); + +test('Calling intercept method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-intercept`); + expect(response.status).toBe(200); +}); + +test('Calling canActivate method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-canActivate`); + expect(response.status).toBe(200); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.build.json b/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.build.json new file mode 100644 index 000000000000..26c30d4eddf2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist"] +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.json b/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.json new file mode 100644 index 000000000000..cf79f029c781 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "moduleResolution": "Node16" + } +} diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 0690206e6b51..e016bf6ee93b 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -27,9 +27,9 @@ "dependencies": { "@aws-sdk/client-s3": "^3.552.0", "@hapi/hapi": "^21.3.10", - "@nestjs/common": "^10.3.7", - "@nestjs/core": "^10.3.3", - "@nestjs/platform-express": "^10.4.6", + "@nestjs/common": "10.4.6", + "@nestjs/core": "10.4.6", + "@nestjs/platform-express": "10.4.6", "@prisma/client": "5.9.1", "@sentry/aws-serverless": "8.38.0", "@sentry/node": "8.38.0", diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts index 51173004b2f8..e4f15f80fe70 100644 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck These are only tests /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; @@ -35,7 +33,7 @@ class AppController { constructor(private readonly appService: AppService) {} @Get('test-exception/:id') - async testException(@Param('id') id: string): void { + async testException(@Param('id') id: string): Promise { Sentry.captureException(new Error(`error with id ${id}`)); } } diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts index 11a0bb831c36..7cf65cbbbb1c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck These are only tests /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; @@ -33,7 +31,7 @@ class AppController { constructor(private readonly appService: AppService) {} @Get('test-exception/:id') - async testException(@Param('id') id: string): void { + async testException(@Param('id') id: string): Promise { Sentry.captureException(new Error(`error with id ${id}`)); } } diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts index b6a6e4c0dca7..e77888ded6a3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck These are only tests /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; @@ -35,7 +33,7 @@ class AppController { constructor(private readonly appService: AppService) {} @Get('test-exception/:id') - async testException(@Param('id') id: string): void { + async testException(@Param('id') id: string): Promise { Sentry.captureException(new Error(`error with id ${id}`)); } } diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts index 953619d8d437..2d4ac4e534cd 100644 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck These are only tests /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 76028b27e40f..9b7360a45706 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -50,8 +50,8 @@ "@sentry/utils": "8.38.0" }, "devDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + "@nestjs/common": "10.4.7", + "@nestjs/core": "10.4.7" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", diff --git a/yarn.lock b/yarn.lock index 53acaa3dc244..6b1ae8520693 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6623,49 +6623,49 @@ semver "^7.3.5" tar "^6.1.11" -"@nestjs/common@^10.3.7": - version "10.3.7" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.3.7.tgz#38ab5ff92277cf1f26f4749c264524e76962cfff" - integrity sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw== +"@nestjs/common@10.4.6": + version "10.4.6" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.6.tgz#952e8fd0ceafeffcc4eaf47effd67fb395844ae0" + integrity sha512-KkezkZvU9poWaNq4L+lNvx+386hpOxPJkfXBBeSMrcqBOx8kVr36TGN2uYkF4Ta4zNu1KbCjmZbc0rhHSg296g== dependencies: uid "2.0.2" iterare "1.2.1" - tslib "2.6.2" + tslib "2.7.0" -"@nestjs/common@^8.0.0 || ^9.0.0 || ^10.0.0": - version "10.3.10" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.3.10.tgz#d8825d55a50a04e33080c9188e6a5b03235d19f2" - integrity sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg== +"@nestjs/common@10.4.7": + version "10.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.7.tgz#076cb77c06149805cb1e193d8cdc69bbe8446c75" + integrity sha512-gIOpjD3Mx8gfYGxYm/RHPcJzqdknNNFCyY+AxzBT3gc5Xvvik1Dn5OxaMGw5EbVfhZgJKVP0n83giUOAlZQe7w== dependencies: uid "2.0.2" iterare "1.2.1" - tslib "2.6.3" + tslib "2.7.0" -"@nestjs/core@^10.3.3": - version "10.3.3" - resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.3.3.tgz#f957068ddda59252b7c36fcdb07a0fb323b52bcf" - integrity sha512-kxJWggQAPX3RuZx9JVec69eSLaYLNIox2emkZJpfBJ5Qq7cAq7edQIt1r4LGjTKq6kFubNTPsqhWf5y7yFRBPw== +"@nestjs/core@10.4.6": + version "10.4.6" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.6.tgz#797b381f12bd62d2e425897058fa219da4c3689d" + integrity sha512-zXVPxCNRfO6gAy0yvEDjUxE/8gfZICJFpsl2lZAUH31bPb6m+tXuhUq2mVCTEltyMYQ+DYtRe+fEYM2v152N1g== dependencies: uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" - path-to-regexp "3.2.0" - tslib "2.6.2" + path-to-regexp "3.3.0" + tslib "2.7.0" -"@nestjs/core@^8.0.0 || ^9.0.0 || ^10.0.0": - version "10.3.10" - resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.3.10.tgz#508090c3ca36488a8e24a9e5939c2f37426e48f4" - integrity sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ== +"@nestjs/core@10.4.7": + version "10.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.7.tgz#adb27067a8c40b79f0713b417457fdfc6cf3406a" + integrity sha512-AIpQzW/vGGqSLkKvll1R7uaSNv99AxZI2EFyVJPNGDgFsfXaohfV1Ukl6f+s75Km+6Fj/7aNl80EqzNWQCS8Ig== dependencies: uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" - path-to-regexp "3.2.0" - tslib "2.6.3" + path-to-regexp "3.3.0" + tslib "2.7.0" -"@nestjs/platform-express@^10.4.6": +"@nestjs/platform-express@10.4.6": version "10.4.6" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.6.tgz#6c39c522fa66036b4256714fea203fbeb49fc4de" integrity sha512-HcyCpAKccAasrLSGRTGWv5BKRs0rwTIFOSsk6laNyqfqvgvYcJQAedarnm4jmaemtmSJ0PFI9PmtEZADd2ahCg== @@ -26780,10 +26780,10 @@ path-to-regexp@0.1.10: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== -path-to-regexp@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" - integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== path-to-regexp@^1.5.3, path-to-regexp@^1.7.0: version "1.9.0" @@ -32111,16 +32111,6 @@ tslib@2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslib@2.6.2, tslib@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - -tslib@2.6.3, tslib@^2.2.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== - tslib@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" @@ -32136,6 +32126,16 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== +tslib@^2.2.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From a55e2b0df8351795cf060eb1cfddae48c068d7ff Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 13 Nov 2024 14:43:40 +0100 Subject: [PATCH 06/29] feat(node): Ensure request bodies are reliably captured for http requests (#13746) This PR started out as trying to fix capturing request bodies for Koa. Some investigation later, we found out that the fundamental problem was that we relied on the request body being on `request.body`, which is non-standard and thus does not necessarily works. It seems that in express this works because it under the hood writes the body there, but this is non-standard and rather undefined behavior. For other frameworks (e.g. Koa and probably more) this did not work, the body was not on the request and thus never captured. We also had no test coverage for this overall. This PR ended up doing a few things: * Add tests for this for express and koa * Streamline types for `sdkProcessingMetadata` - this used to be `any`, which lead to any usage of this not really being typed at all. I added proper types for this now. * Generic extraction of the http request body in the http instrumentation - this should now work for any node framework Most importantly, I opted to not force this into the existing, rather complicated and hard to follow request data integration flow. This used to take an IsomorphicRequest and then did a bunch of conversion etc. Since now in Node, we always have the same, proper http request (for any framework, because this always goes through http instrumentation), we can actually streamline this and normalize this properly at the time where we set this. So with this PR, we set a `normalizedRequest` which already has the url, headers etc. set in a way that we need it/it makes sense. Additionally, the parsed & stringified request body will be set on this too. If this normalized request is set in sdkProcessingMetadata, we will use it as source of truth instead of the plain `request`. (Note that we still need the plain request for some auxiliary data that is non-standard, e.g. `request.user`). For the body parsing itself, we monkey-patch `req.on('data')`. this way, we ensure to not add more handlers than a user has, and we only extract the body if the user is extracting it anyhow, ensuring we do not alter behavior. Closes https://github.com/getsentry/sentry-javascript/issues/13722 --------- Co-authored-by: Luca Forstner --- .../test-applications/node-koa/index.js | 6 + .../test-applications/node-koa/package.json | 1 + .../node-koa/tests/transactions.test.ts | 193 +++++++++++------- .../node-integration-tests/package.json | 1 + .../suites/express/tracing/server.js | 8 + .../suites/express/tracing/test.ts | 101 +++++++++ .../suites/express/without-tracing/server.ts | 11 + .../suites/express/without-tracing/test.ts | 139 +++++++++++-- packages/core/src/integrations/requestdata.ts | 20 +- .../http/SentryHttpInstrumentation.ts | 162 ++++++++++++++- packages/node/src/transports/http-module.ts | 3 +- packages/types/src/envelope.ts | 2 +- packages/types/src/event.ts | 14 +- packages/types/src/request.ts | 5 +- packages/utils/src/requestdata.ts | 122 ++++++++++- yarn.lock | 25 +++ 16 files changed, 711 insertions(+), 102 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-koa/index.js b/dev-packages/e2e-tests/test-applications/node-koa/index.js index ddc17f62e6f7..9e800a4fcc99 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/index.js +++ b/dev-packages/e2e-tests/test-applications/node-koa/index.js @@ -14,10 +14,12 @@ const port1 = 3030; const port2 = 3040; const Koa = require('koa'); +const { bodyParser } = require('@koa/bodyparser'); const Router = require('@koa/router'); const http = require('http'); const app1 = new Koa(); +app1.use(bodyParser()); Sentry.setupKoaErrorHandler(app1); @@ -109,6 +111,10 @@ router1.get('/test-assert/:condition', async ctx => { ctx.assert(condition, 400, 'ctx.assert failed'); }); +router1.post('/test-post', async ctx => { + ctx.body = { status: 'ok', body: ctx.request.body }; +}); + app1.use(router1.routes()).use(router1.allowedMethods()); app1.listen(port1); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/package.json b/dev-packages/e2e-tests/test-applications/node-koa/package.json index 79a4e540c089..dd8a17d0f4b5 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/package.json +++ b/dev-packages/e2e-tests/test-applications/node-koa/package.json @@ -10,6 +10,7 @@ "test:assert": "pnpm test" }, "dependencies": { + "@koa/bodyparser": "^5.1.1", "@koa/router": "^12.0.1", "@sentry/node": "latest || *", "@sentry/types": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts index 4c52c932e7b4..1197575d1a96 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts @@ -46,76 +46,129 @@ test('Sends an API route transaction', async ({ baseURL }) => { origin: 'auto.http.otel.http', }); - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: [ - { - data: { - 'koa.name': '', - 'koa.type': 'middleware', - 'sentry.origin': 'auto.http.otel.koa', - 'sentry.op': 'middleware.koa', - }, - op: 'middleware.koa', - origin: 'auto.http.otel.koa', - description: '< unknown >', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.any(String), - }, - { - data: { - 'http.route': '/test-transaction', - 'koa.name': '/test-transaction', - 'koa.type': 'router', - 'sentry.origin': 'auto.http.otel.koa', - 'sentry.op': 'router.koa', - }, - op: 'router.koa', - description: '/test-transaction', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.any(String), - origin: 'auto.http.otel.koa', - }, - { - data: { - 'sentry.origin': 'manual', - }, - description: 'test-span', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.any(String), - origin: 'manual', - }, - { - data: { - 'sentry.origin': 'manual', - }, - description: 'child-span', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.any(String), - origin: 'manual', - }, - ], - transaction: 'GET /test-transaction', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }); + + expect(transactionEvent.spans).toEqual([ + { + data: { + 'koa.name': 'bodyParser', + 'koa.type': 'middleware', + 'sentry.op': 'middleware.koa', + 'sentry.origin': 'auto.http.otel.koa', + }, + description: 'bodyParser', + op: 'middleware.koa', + origin: 'auto.http.otel.koa', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + { + data: { + 'koa.name': '', + 'koa.type': 'middleware', + 'sentry.origin': 'auto.http.otel.koa', + 'sentry.op': 'middleware.koa', + }, + op: 'middleware.koa', + origin: 'auto.http.otel.koa', + description: '< unknown >', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + { + data: { + 'http.route': '/test-transaction', + 'koa.name': '/test-transaction', + 'koa.type': 'router', + 'sentry.origin': 'auto.http.otel.koa', + 'sentry.op': 'router.koa', }, + op: 'router.koa', + description: '/test-transaction', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.http.otel.koa', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'test-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'child-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + ]); +}); + +test('Captures request metadata', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-koa', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'POST /test-post' + ); + }); + + const res = await fetch(`${baseURL}/test-post`, { + method: 'POST', + body: JSON.stringify({ foo: 'bar', other: 1 }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const resBody = await res.json(); + + expect(resBody).toEqual({ status: 'ok', body: { foo: 'bar', other: 1 } }); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.request).toEqual({ + cookies: {}, + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: expect.objectContaining({ + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', }), - ); + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }); + + expect(transactionEvent.user).toEqual(undefined); }); diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index e016bf6ee93b..5b62e3a8e996 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -41,6 +41,7 @@ "amqplib": "^0.10.4", "apollo-server": "^3.11.1", "axios": "^1.7.7", + "body-parser": "^1.20.3", "connect": "^3.7.0", "cors": "^2.8.5", "cron": "^3.1.6", diff --git a/dev-packages/node-integration-tests/suites/express/tracing/server.js b/dev-packages/node-integration-tests/suites/express/tracing/server.js index 81560806097e..f9b4ae24b339 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/server.js +++ b/dev-packages/node-integration-tests/suites/express/tracing/server.js @@ -13,11 +13,15 @@ Sentry.init({ // express must be required after Sentry is initialized const express = require('express'); const cors = require('cors'); +const bodyParser = require('body-parser'); const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); const app = express(); app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); app.get('/test/express', (_req, res) => { res.send({ response: 'response 1' }); @@ -35,6 +39,10 @@ app.get(['/test/arr/:id', /\/test\/arr[0-9]*\/required(path)?(\/optionalPath)?\/ res.send({ response: 'response 4' }); }); +app.post('/test-post', function (req, res) { + res.send({ status: 'ok', body: req.body }); +}); + Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/test.ts index 44852233ed67..0b56d354759c 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/test.ts @@ -137,5 +137,106 @@ describe('express tracing', () => { .start(done) .makeRequest('get', `/test/${segment}`); }) as any); + + describe('request data', () => { + test('correctly captures JSON request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', + }, + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { data: { foo: 'bar', other: 1 } }); + }); + + test('correctly captures plain text request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'text/plain', + }, + data: 'some plain text', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'text/plain' }, + data: 'some plain text', + }); + }); + + test('correctly captures text buffer request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + data: 'some plain text in buffer', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'application/octet-stream' }, + data: Buffer.from('some plain text in buffer'), + }); + }); + + test('correctly captures non-text buffer request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + // This is some non-ascii string representation + data: expect.any(String), + }, + }, + }) + .start(done); + + const body = new Uint8Array([1, 2, 3, 4, 5]).buffer; + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'application/octet-stream' }, + data: body, + }); + }); + }); }); }); diff --git a/dev-packages/node-integration-tests/suites/express/without-tracing/server.ts b/dev-packages/node-integration-tests/suites/express/without-tracing/server.ts index 2a85d39b83b8..5b96e8b1a2a3 100644 --- a/dev-packages/node-integration-tests/suites/express/without-tracing/server.ts +++ b/dev-packages/node-integration-tests/suites/express/without-tracing/server.ts @@ -8,10 +8,15 @@ Sentry.init({ }); import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import bodyParser from 'body-parser'; import express from 'express'; const app = express(); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); + Sentry.setTag('global', 'tag'); app.get('/test/isolationScope/:id', (req, res) => { @@ -24,6 +29,12 @@ app.get('/test/isolationScope/:id', (req, res) => { res.send({}); }); +app.post('/test-post', function (req, res) { + Sentry.captureException(new Error('This is an exception')); + + res.send({ status: 'ok', body: req.body }); +}); + Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts index 7c304062bc22..fdd63ad4aa4b 100644 --- a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts @@ -4,26 +4,129 @@ afterAll(() => { cleanupChildProcesses(); }); -test('correctly applies isolation scope even without tracing', done => { - const runner = createRunner(__dirname, 'server.ts') - .expect({ - event: { - transaction: 'GET /test/isolationScope/1', - tags: { - global: 'tag', - 'isolation-scope': 'tag', - 'isolation-scope-1': '1', +describe('express without tracing', () => { + test('correctly applies isolation scope even without tracing', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'GET /test/isolationScope/1', + tags: { + global: 'tag', + 'isolation-scope': 'tag', + 'isolation-scope-1': '1', + }, + // Request is correctly set + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test\/isolationScope\/1$/), + method: 'GET', + headers: { + 'user-agent': expect.stringContaining(''), + }, + }, }, - // Request is correctly set - request: { - url: expect.stringContaining('/test/isolationScope/1'), - headers: { - 'user-agent': expect.stringContaining(''), + }) + .start(done); + + runner.makeRequest('get', '/test/isolationScope/1'); + }); + + describe('request data', () => { + test('correctly captures JSON request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', + }, + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { data: { foo: 'bar', other: 1 } }); + }); + + test('correctly captures plain text request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'text/plain', + }, + data: 'some plain text', + }, }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { + 'Content-Type': 'text/plain', }, - }, - }) - .start(done); + data: 'some plain text', + }); + }); + + test('correctly captures text buffer request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + data: 'some plain text in buffer', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'application/octet-stream' }, + data: Buffer.from('some plain text in buffer'), + }); + }); + + test('correctly captures non-text buffer request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + // This is some non-ascii string representation + data: expect.any(String), + }, + }, + }) + .start(done); + + const body = new Uint8Array([1, 2, 3, 4, 5]).buffer; - runner.makeRequest('get', '/test/isolationScope/1'); + runner.makeRequest('post', '/test-post', { headers: { 'Content-Type': 'application/octet-stream' }, data: body }); + }); + }); }); diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index f7846dec6fea..9475973d8e2c 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -1,5 +1,6 @@ import type { IntegrationFn } from '@sentry/types'; import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; +import { addNormalizedRequestDataToEvent } from '@sentry/utils'; import { addRequestDataToEvent } from '@sentry/utils'; import { defineIntegration } from '../integration'; @@ -73,15 +74,26 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) = // that's happened, it will be easier to add this logic in without worrying about unexpected side effects.) const { sdkProcessingMetadata = {} } = event; - const req = sdkProcessingMetadata.request; + const { request, normalizedRequest } = sdkProcessingMetadata; - if (!req) { + const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options); + + // If this is set, it takes precedence over the plain request object + if (normalizedRequest) { + // Some other data is not available in standard HTTP requests, but can sometimes be augmented by e.g. Express or Next.js + const ipAddress = request ? request.ip || (request.socket && request.socket.remoteAddress) : undefined; + const user = request ? request.user : undefined; + + addNormalizedRequestDataToEvent(event, normalizedRequest, { ipAddress, user }, addRequestDataOptions); return event; } - const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options); + // TODO(v9): Eventually we can remove this fallback branch and only rely on the normalizedRequest above + if (!request) { + return event; + } - return addRequestDataToEvent(event, req, addRequestDataOptions); + return addRequestDataToEvent(event, request, addRequestDataOptions); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 090c0783507a..6b6fe8aaad40 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -1,18 +1,20 @@ import type * as http from 'node:http'; -import type { RequestOptions } from 'node:http'; +import type { IncomingMessage, RequestOptions } from 'node:http'; import type * as https from 'node:https'; import { VERSION } from '@opentelemetry/core'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; import { getRequestInfo } from '@opentelemetry/instrumentation-http'; import { addBreadcrumb, getClient, getIsolationScope, withIsolationScope } from '@sentry/core'; -import type { SanitizedRequestData } from '@sentry/types'; +import type { PolymorphicRequest, Request, SanitizedRequestData } from '@sentry/types'; import { getBreadcrumbLogLevelFromHttpStatusCode, getSanitizedUrlString, + logger, parseUrl, stripUrlQueryAndFragment, } from '@sentry/utils'; +import { DEBUG_BUILD } from '../../debug-build'; import type { NodeClient } from '../../sdk/client'; import { getRequestUrl } from '../../utils/getRequestUrl'; @@ -39,6 +41,9 @@ type SentryHttpInstrumentationOptions = InstrumentationConfig & { ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean; }; +// We only want to capture request bodies up to 1mb. +const MAX_BODY_BYTE_LENGTH = 1024 * 1024; + /** * This custom HTTP instrumentation is used to isolate incoming requests and annotate them with additional information. * It does not emit any spans. @@ -128,8 +133,27 @@ export class SentryHttpInstrumentation extends InstrumentationBase'; + const protocol = request.socket && (request.socket as { encrypted?: boolean }).encrypted ? 'https' : 'http'; + const originalUrl = request.url || ''; + const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`; + + // This is non-standard, but may be set on e.g. Next.js or Express requests + const cookies = (request as PolymorphicRequest).cookies; + + const normalizedRequest: Request = { + url: absoluteUrl, + method: request.method, + query_string: extractQueryParams(request), + headers: headersToDict(request.headers), + cookies, + }; + + patchRequestToCaptureBody(request, normalizedRequest); + // Update the isolation scope, isolate this request - isolationScope.setSDKProcessingMetadata({ request }); + isolationScope.setSDKProcessingMetadata({ request, normalizedRequest }); const client = getClient(); if (client && client.getOptions().autoSessionTracking) { @@ -316,3 +340,135 @@ function getBreadcrumbData(request: http.ClientRequest): Partial acc + chunk.byteLength, 0); + } + + /** + * We need to keep track of the original callbacks, in order to be able to remove listeners again. + * Since `off` depends on having the exact same function reference passed in, we need to be able to map + * original listeners to our wrapped ones. + */ + const callbackMap = new WeakMap(); + + try { + // eslint-disable-next-line @typescript-eslint/unbound-method + req.on = new Proxy(req.on, { + apply: (target, thisArg, args: Parameters) => { + const [event, listener, ...restArgs] = args; + + if (event === 'data') { + const callback = new Proxy(listener, { + apply: (target, thisArg, args: Parameters) => { + // If we have already read more than the max body length, we stop addiing chunks + // To avoid growing the memory indefinitely if a respons is e.g. streamed + if (getChunksSize() < MAX_BODY_BYTE_LENGTH) { + const chunk = args[0] as Buffer; + chunks.push(chunk); + } else if (DEBUG_BUILD) { + logger.log( + `Dropping request body chunk because it maximum body length of ${MAX_BODY_BYTE_LENGTH}b is exceeded.`, + ); + } + + return Reflect.apply(target, thisArg, args); + }, + }); + + callbackMap.set(listener, callback); + + return Reflect.apply(target, thisArg, [event, callback, ...restArgs]); + } + + if (event === 'end') { + const callback = new Proxy(listener, { + apply: (target, thisArg, args) => { + try { + const body = Buffer.concat(chunks).toString('utf-8'); + + // We mutate the passed in normalizedRequest and add the body to it + if (body) { + normalizedRequest.data = body; + } + } catch { + // ignore errors here + } + + return Reflect.apply(target, thisArg, args); + }, + }); + + callbackMap.set(listener, callback); + + return Reflect.apply(target, thisArg, [event, callback, ...restArgs]); + } + + return Reflect.apply(target, thisArg, args); + }, + }); + + // Ensure we also remove callbacks correctly + // eslint-disable-next-line @typescript-eslint/unbound-method + req.off = new Proxy(req.off, { + apply: (target, thisArg, args: Parameters) => { + const [, listener] = args; + + const callback = callbackMap.get(listener); + if (callback) { + callbackMap.delete(listener); + + const modifiedArgs = args.slice(); + modifiedArgs[1] = callback; + return Reflect.apply(target, thisArg, modifiedArgs); + } + + return Reflect.apply(target, thisArg, args); + }, + }); + } catch { + // ignore errors if we can't patch stuff + } +} + +function extractQueryParams(req: IncomingMessage): string | undefined { + // req.url is path and query string + if (!req.url) { + return; + } + + try { + // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and + // hostname as the base. Since the point here is just to grab the query string, it doesn't matter what we use. + const queryParams = new URL(req.url, 'http://dogs.are.great').search.slice(1); + return queryParams.length ? queryParams : undefined; + } catch { + return undefined; + } +} + +function headersToDict(reqHeaders: Record): Record { + const headers: Record = Object.create(null); + + try { + Object.entries(reqHeaders).forEach(([key, value]) => { + if (typeof value === 'string') { + headers[key] = value; + } + }); + } catch (e) { + DEBUG_BUILD && + logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.'); + } + + return headers; +} diff --git a/packages/node/src/transports/http-module.ts b/packages/node/src/transports/http-module.ts index f5cbe6fd35f9..65bf99349b10 100644 --- a/packages/node/src/transports/http-module.ts +++ b/packages/node/src/transports/http-module.ts @@ -10,7 +10,8 @@ export type HTTPModuleRequestOptions = HTTPRequestOptions | HTTPSRequestOptions export interface HTTPModuleRequestIncomingMessage { headers: IncomingHttpHeaders; statusCode?: number; - on(event: 'data' | 'end', listener: () => void): void; + on(event: 'data' | 'end', listener: (chunk: Buffer) => void): void; + off(event: 'data' | 'end', listener: (chunk: Buffer) => void): void; setEncoding(encoding: string): void; } diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 29e8fc123b16..20c67b8857fe 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -101,7 +101,7 @@ export type ProfileItem = BaseEnvelopeItem; export type ProfileChunkItem = BaseEnvelopeItem; export type SpanItem = BaseEnvelopeItem>; -export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext }; +export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: Partial }; type SessionEnvelopeHeaders = { sent_at: string }; type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext }; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index c8c16b8ce514..bdccaa2b58dd 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -2,13 +2,15 @@ import type { Attachment } from './attachment'; import type { Breadcrumb } from './breadcrumb'; import type { Contexts } from './context'; import type { DebugMeta } from './debugMeta'; +import type { DynamicSamplingContext } from './envelope'; import type { Exception } from './exception'; import type { Extras } from './extra'; import type { Measurements } from './measurement'; import type { Mechanism } from './mechanism'; import type { Primitive } from './misc'; +import type { PolymorphicRequest } from './polymorphics'; import type { Request } from './request'; -import type { CaptureContext } from './scope'; +import type { CaptureContext, Scope } from './scope'; import type { SdkInfo } from './sdkinfo'; import type { SeverityLevel } from './severity'; import type { MetricSummary, SpanJSON } from './span'; @@ -51,7 +53,15 @@ export interface Event { measurements?: Measurements; debug_meta?: DebugMeta; // A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get sent to Sentry - sdkProcessingMetadata?: { [key: string]: any }; + // Note: This is considered internal and is subject to change in minors + sdkProcessingMetadata?: { [key: string]: unknown } & { + request?: PolymorphicRequest; + normalizedRequest?: Request; + dynamicSamplingContext?: Partial; + capturedSpanScope?: Scope; + capturedSpanIsolationScope?: Scope; + spanCountBeforeProcessing?: number; + }; transaction_info?: { source: TransactionSource; }; diff --git a/packages/types/src/request.ts b/packages/types/src/request.ts index 3c04a788ded9..f0c2bdb268ca 100644 --- a/packages/types/src/request.ts +++ b/packages/types/src/request.ts @@ -1,4 +1,7 @@ -/** Request data included in an event as sent to Sentry */ +/** + * Request data included in an event as sent to Sentry. + * TODO(v9): Rename this to avoid confusion, because Request is also a native type. + */ export interface Request { url?: string; method?: string; diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index a4eae547edb1..edffc6f67da7 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -1,7 +1,9 @@ +/* eslint-disable max-lines */ import type { Event, ExtractedNodeRequestData, PolymorphicRequest, + Request, TransactionSource, WebFetchHeaders, WebFetchRequest, @@ -12,6 +14,7 @@ import { DEBUG_BUILD } from './debug-build'; import { isPlainObject, isString } from './is'; import { logger } from './logger'; import { normalize } from './normalize'; +import { truncate } from './string'; import { stripUrlQueryAndFragment } from './url'; import { getClientIPAddress, ipHeaderNames } from './vendor/getIpAddress'; @@ -228,14 +231,27 @@ export function extractRequestData( if (method === 'GET' || method === 'HEAD') { break; } + // NOTE: As of v8, request is (unless a user sets this manually) ALWAYS a http request + // Which does not have a body by default + // However, in our http instrumentation, we patch the request to capture the body and store it on the + // request as `.body` anyhow + // In v9, we may update requestData to only work with plain http requests // body data: // express, koa, nextjs: req.body // // when using node by itself, you have to read the incoming stream(see // https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know // where they're going to store the final result, so they'll have to capture this data themselves - if (req.body !== undefined) { - requestData.data = isString(req.body) ? req.body : JSON.stringify(normalize(req.body)); + const body = req.body; + if (body !== undefined) { + const stringBody: string = isString(body) + ? body + : isPlainObject(body) + ? JSON.stringify(normalize(body)) + : truncate(`${body}`, 1024); + if (stringBody) { + requestData.data = stringBody; + } } break; } @@ -250,6 +266,61 @@ export function extractRequestData( return requestData; } +/** + * Add already normalized request data to an event. + * This mutates the passed in event. + */ +export function addNormalizedRequestDataToEvent( + event: Event, + req: Request, + // This is non-standard data that is not part of the regular HTTP request + additionalData: { ipAddress?: string; user?: Record }, + options: AddRequestDataToEventOptions, +): void { + const include = { + ...DEFAULT_INCLUDES, + ...(options && options.include), + }; + + if (include.request) { + const includeRequest = Array.isArray(include.request) ? [...include.request] : [...DEFAULT_REQUEST_INCLUDES]; + if (include.ip) { + includeRequest.push('ip'); + } + + const extractedRequestData = extractNormalizedRequestData(req, { include: includeRequest }); + + event.request = { + ...event.request, + ...extractedRequestData, + }; + } + + if (include.user) { + const extractedUser = + additionalData.user && isPlainObject(additionalData.user) + ? extractUserData(additionalData.user, include.user) + : {}; + + if (Object.keys(extractedUser).length) { + event.user = { + ...event.user, + ...extractedUser, + }; + } + } + + if (include.ip) { + const ip = (req.headers && getClientIPAddress(req.headers)) || additionalData.ipAddress; + if (ip) { + event.user = { + ...event.user, + ip_address: ip, + }; + } + } +} + /** * Add data from the given request to the given event * @@ -374,3 +445,50 @@ export function winterCGRequestToRequestData(req: WebFetchRequest): PolymorphicR headers, }; } + +function extractNormalizedRequestData(normalizedRequest: Request, { include }: { include: string[] }): Request { + const includeKeys = include ? (Array.isArray(include) ? include : DEFAULT_REQUEST_INCLUDES) : []; + + const requestData: Request = {}; + const headers = { ...normalizedRequest.headers }; + + if (includeKeys.includes('headers')) { + requestData.headers = headers; + + // Remove the Cookie header in case cookie data should not be included in the event + if (!include.includes('cookies')) { + delete (headers as { cookie?: string }).cookie; + } + + // Remove IP headers in case IP data should not be included in the event + if (!include.includes('ip')) { + ipHeaderNames.forEach(ipHeaderName => { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (headers as Record)[ipHeaderName]; + }); + } + } + + if (includeKeys.includes('method')) { + requestData.method = normalizedRequest.method; + } + + if (includeKeys.includes('url')) { + requestData.url = normalizedRequest.url; + } + + if (includeKeys.includes('cookies')) { + const cookies = normalizedRequest.cookies || (headers && headers.cookie ? parseCookie(headers.cookie) : undefined); + requestData.cookies = cookies || {}; + } + + if (includeKeys.includes('query_string')) { + requestData.query_string = normalizedRequest.query_string; + } + + if (includeKeys.includes('data')) { + requestData.data = normalizedRequest.data; + } + + return requestData; +} diff --git a/yarn.lock b/yarn.lock index 6b1ae8520693..01efab263fca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12973,6 +12973,24 @@ body-parser@1.20.3, body-parser@^1.18.3, body-parser@^1.19.0: type-is "~1.6.18" unpipe "1.0.0" +body-parser@^1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + body@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069" @@ -28336,6 +28354,13 @@ qs@^6.4.0: dependencies: side-channel "^1.0.4" +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" From b50aefd2208441ced0a764ffc22e4fc5a7a8a00b Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 13 Nov 2024 14:16:20 -0500 Subject: [PATCH 07/29] ref(profiling) Fix electron crash (#14216) This PR fixes the electron crash observed in https://github.com/getsentry/sentry-javascript/discussions/13978 I am not entirely sure as to why this causes a sigabrt, so I am working around the issue (obtaining a coredump out of electron did not seem trivial and my knowledge around electron isn't very extensive). The v8 options class and the constants are exposed correctly, I ruled that out, however the crash still seems to happen when they are used in this specific signature. In order to have this running with electron, users will require to use the electron/rebuild package, which is the recommended approach by electron that rebuilds native node addons by providing the correct abi headers for the electron version the user is running. For now, we wont provide any prebuilt binaries and instead rely on the fallback mechanism to load the correct module. I will reevaluate this if it causes issues with bundling and look to add proper runtime electron detection. --- .github/workflows/build.yml | 21 ++++++- .../node-profiling/__tests__/electron.spec.js | 24 ++++++++ .../node-profiling/index.electron.js | 54 ++++++++++++++++++ .../node-profiling/index.html | 6 ++ .../node-profiling/package.json | 11 +++- .../bin/darwin-arm64-130/profiling-node.node | Bin 0 -> 115304 bytes .../profiling-node/bindings/cpu_profiler.cc | 6 +- 7 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-profiling/__tests__/electron.spec.js create mode 100644 dev-packages/e2e-tests/test-applications/node-profiling/index.electron.js create mode 100644 dev-packages/e2e-tests/test-applications/node-profiling/index.html create mode 100755 packages/profiling-node/bin/darwin-arm64-130/profiling-node.node diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 79fc0ff4a73c..fd0e0306f52b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1235,7 +1235,7 @@ jobs: (needs.job_get_metadata.outputs.is_release == 'true') ) needs: [job_get_metadata, job_build, job_e2e_prepare] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 15 env: E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }} @@ -1255,19 +1255,24 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} + - uses: pnpm/action-setup@v4 with: version: 9.4.0 + - name: Set up Node uses: actions/setup-node@v4 with: node-version: 22 + - name: Restore caches uses: ./.github/actions/restore-cache with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Build Profiling Node run: yarn lerna run build:lib --scope @sentry/profiling-node + - name: Extract Profiling Node Prebuilt Binaries uses: actions/download-artifact@v4 with: @@ -1306,6 +1311,18 @@ jobs: env: E2E_TEST_PUBLISH_SCRIPT_NODE_VERSION: ${{ steps.versions.outputs.node }} + - name: Setup xvfb and update ubuntu dependencies + run: | + sudo apt-get install xvfb x11-xkb-utils xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic x11-apps + sudo apt-get install build-essential clang libdbus-1-dev libgtk2.0-dev \ + libnotify-dev libgconf2-dev \ + libasound2-dev libcap-dev libcups2-dev libxtst-dev \ + libxss1 libnss3-dev gcc-multilib g++-multilib + + - name: Install dependencies + working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} + run: yarn install --ignore-engines --frozen-lockfile + - name: Build E2E app working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 7 @@ -1314,7 +1331,7 @@ jobs: - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 10 - run: yarn test:assert + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:assert job_required_jobs_passed: name: All required jobs passed or were skipped diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/__tests__/electron.spec.js b/dev-packages/e2e-tests/test-applications/node-profiling/__tests__/electron.spec.js new file mode 100644 index 000000000000..4519220008d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/__tests__/electron.spec.js @@ -0,0 +1,24 @@ +const process = require('process'); +const { test, expect, _electron: electron } = require('@playwright/test'); + +test('an h1 contains hello world"', async () => { + const electronApp = await electron.launch({ + args: ['./index.electron.js'], + process: { + env: { + ...process.env, + }, + }, + }); + + // Wait for the first BrowserWindow to open + const window = await electronApp.firstWindow(); + + // Check for the presence of an h1 element with the text "hello" + const headerElement = await window.$('h1'); + const headerText = await headerElement.textContent(); + expect(headerText).toBe('Hello From Profiled Electron App'); + + // Close the app + await electronApp.close(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/index.electron.js b/dev-packages/e2e-tests/test-applications/node-profiling/index.electron.js new file mode 100644 index 000000000000..d08ac4ecc142 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/index.electron.js @@ -0,0 +1,54 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow } = require('electron'); +const Sentry = require('@sentry/electron/main'); +const path = require('node:path'); + +Sentry.init({ + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + debug: true, + tracesSampleRate: 1.0, +}); + +// Hog the cpu for a second +function block() { + const start = Date.now(); + while (start + 1000 > Date.now()) {} +} + +const createWindow = () => { + block(); + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + }, + }); + + // and load the index.html of the app. + mainWindow.loadFile('index.html'); + block(); +}; + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + Sentry.profiler.startProfiler(); + createWindow(); + Sentry.profiler.stopProfiler(); + + app.on('activate', () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/index.html b/dev-packages/e2e-tests/test-applications/node-profiling/index.html new file mode 100644 index 000000000000..97022c9b265e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/index.html @@ -0,0 +1,6 @@ + + + +

Hello From Profiled Electron App

+ + diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/package.json b/dev-packages/e2e-tests/test-applications/node-profiling/package.json index a4c4bf1284fe..cfe4e136b1c1 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling/package.json @@ -7,12 +7,17 @@ "build": "node build.mjs && node build.shimmed.mjs", "test": "node dist/index.js && node --experimental-require-module dist/index.js && node dist/index.shimmed.mjs", "clean": "npx rimraf node_modules dist", - "test:build": "npm run typecheck && npm run build", - "test:assert": "npm run test" + "test:electron": "$(pnpm bin)/electron-rebuild && playwright test", + "test:build": "pnpm run typecheck && pnpm run build", + "test:assert": "pnpm run test && pnpm run test:electron" }, "dependencies": { + "@electron/rebuild": "^3.7.0", + "@playwright/test": "^1.48.2", + "@sentry/electron": "latest || *", "@sentry/node": "latest || *", - "@sentry/profiling-node": "latest || *" + "@sentry/profiling-node": "latest || *", + "electron": "^33.2.0" }, "devDependencies": {}, "volta": { diff --git a/packages/profiling-node/bin/darwin-arm64-130/profiling-node.node b/packages/profiling-node/bin/darwin-arm64-130/profiling-node.node new file mode 100755 index 0000000000000000000000000000000000000000..65e97eca7e48d05a45e91933dd230fcc8c543d8b GIT binary patch literal 115304 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 - profile_title, - {v8::CpuProfilingMode::kCallerLineNumbers, - v8::CpuProfilingOptions::kNoSampleLimit, kSamplingInterval}); + profile_title, v8::CpuProfilingMode::kCallerLineNumbers, true, + v8::CpuProfilingOptions::kNoSampleLimit); // listen for memory sample ticks profiler->measurements_ticker.add_cpu_listener(id, cpu_sampler_cb); @@ -1169,6 +1168,7 @@ napi_value Init(napi_env env, napi_value exports) { } Profiler *profiler = new Profiler(env, isolate); + profiler->cpu_profiler->SetSamplingInterval(kSamplingInterval); if (napi_set_instance_data(env, profiler, FreeAddonData, NULL) != napi_ok) { napi_throw_error(env, nullptr, "Failed to set instance data for profiler."); From 1434fd951d4d5bffa139e641edd9016073173d39 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 14 Nov 2024 09:32:35 +0100 Subject: [PATCH 08/29] ref(vue): Reduce bundle size for starting application render span (#14275) --- packages/vue/src/tracing.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 3085e528bbf0..c00bdd184c20 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -81,18 +81,16 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { const isRoot = this.$root === this; if (isRoot) { - const activeSpan = getActiveSpan(); - if (activeSpan) { - this.$_sentryRootSpan = - this.$_sentryRootSpan || - startInactiveSpan({ - name: 'Application Render', - op: `${VUE_OP}.render`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', - }, - }); - } + this.$_sentryRootSpan = + this.$_sentryRootSpan || + startInactiveSpan({ + name: 'Application Render', + op: `${VUE_OP}.render`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', + }, + onlyIfParent: true, + }); } // Skip components that we don't want to track to minimize the noise and give a more granular control to the user From f4c59000ca57b566c3ed05fe5a0b06acd75a2543 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Thu, 14 Nov 2024 09:33:15 +0100 Subject: [PATCH 09/29] test(e2e): Add nuxt 3 minimum supported e2e test app with pinned nitro 2.9.7 dep (#14279) We wanted to ensure our nitro rollup plugin fix also properly works for the lowest nuxt version the nuxt SDK supports. This pins nitro to `2.9.7` and nuxt to `3.13.2`. --- .github/workflows/build.yml | 1 + .../test-applications/nuxt-3-min/.gitignore | 24 ++++ .../test-applications/nuxt-3-min/.npmrc | 2 + .../test-applications/nuxt-3-min/app.vue | 17 +++ .../nuxt-3-min/components/ErrorButton.vue | 22 ++++ .../nuxt-3-min/copyIITM.bash | 7 ++ .../nuxt-3-min/nuxt.config.ts | 20 ++++ .../test-applications/nuxt-3-min/package.json | 30 +++++ .../nuxt-3-min/pages/client-error.vue | 11 ++ .../nuxt-3-min/pages/fetch-server-error.vue | 13 +++ .../nuxt-3-min/pages/index.vue | 3 + .../nuxt-3-min/pages/test-param/[param].vue | 23 ++++ .../nuxt-3-min/playwright.config.ts | 19 ++++ .../nuxt-3-min/public/favicon.ico | Bin 0 -> 4286 bytes .../nuxt-3-min/sentry.client.config.ts | 10 ++ .../nuxt-3-min/sentry.server.config.ts | 8 ++ .../server/api/param-error/[param].ts | 5 + .../nuxt-3-min/server/api/server-error.ts | 5 + .../server/api/test-param/[param].ts | 7 ++ .../nuxt-3-min/server/tsconfig.json | 3 + .../nuxt-3-min/start-event-proxy.mjs | 6 + .../nuxt-3-min/tests/errors.client.test.ts | 105 ++++++++++++++++++ .../nuxt-3-min/tests/errors.server.test.ts | 40 +++++++ .../nuxt-3-min/tests/tracing.client.test.ts | 57 ++++++++++ .../nuxt-3-min/tests/tracing.server.test.ts | 46 ++++++++ .../nuxt-3-min/tests/tracing.test.ts | 51 +++++++++ .../nuxt-3-min/tsconfig.json | 4 + .../test-applications/nuxt-3/package.json | 2 +- 28 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/app.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/components/ErrorButton.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/copyIITM.bash create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/client-error.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/index.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.client.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/param-error/[param].ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/server-error.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/test-param/[param].ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/server/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fd0e0306f52b..e80e2e869905 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -938,6 +938,7 @@ jobs: 'node-koa', 'node-connect', 'nuxt-3', + 'nuxt-3-min', 'nuxt-4', 'vue-3', 'webpack-4', diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/.gitignore b/dev-packages/e2e-tests/test-applications/nuxt-3-min/.gitignore new file mode 100644 index 000000000000..4a7f73a2ed0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/.gitignore @@ -0,0 +1,24 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/.npmrc b/dev-packages/e2e-tests/test-applications/nuxt-3-min/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/app.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/app.vue new file mode 100644 index 000000000000..23283a522546 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/app.vue @@ -0,0 +1,17 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/components/ErrorButton.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/components/ErrorButton.vue new file mode 100644 index 000000000000..92ea714ae489 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/components/ErrorButton.vue @@ -0,0 +1,22 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/copyIITM.bash b/dev-packages/e2e-tests/test-applications/nuxt-3-min/copyIITM.bash new file mode 100644 index 000000000000..0e04d001c968 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/copyIITM.bash @@ -0,0 +1,7 @@ +# This script copies the `import-in-the-middle` content of the E2E test project root `node_modules` to the build output `node_modules` +# For some reason, some files are missing in the output (like `hook.mjs`) and this is not reproducible in external, standalone projects. +# +# Things we tried (that did not fix the problem): +# - Adding a resolution for `@vercel/nft` v0.27.0 (this worked in the standalone project) +# - Also adding `@vercel/nft` v0.27.0 to pnpm `peerDependencyRules` +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules/import-in-the-middle diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts new file mode 100644 index 000000000000..87e046ed39e9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts @@ -0,0 +1,20 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + modules: ['@sentry/nuxt/module'], + imports: { + autoImport: false, + }, + runtimeConfig: { + public: { + sentry: { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }, + }, + }, + nitro: { + rollupConfig: { + // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output + external: [/@sentry\/.*/], + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json new file mode 100644 index 000000000000..18f798f89246 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json @@ -0,0 +1,30 @@ +{ + "name": "nuxt-3-min", + "description": "E2E test app for the minimum nuxt 3 version our nuxt SDK supports.", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build && bash ./copyIITM.bash", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "start": "node .output/server/index.mjs", + "clean": "npx nuxi cleanup", + "test": "playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/nuxt": "latest || *", + "nuxt": "3.13.2" + }, + "devDependencies": { + "@nuxt/test-utils": "^3.14.1", + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "overrides": { + "nitropack": "2.9.7", + "@vercel/nft": "^0.27.4" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/client-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/client-error.vue new file mode 100644 index 000000000000..d244ef773140 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/client-error.vue @@ -0,0 +1,11 @@ + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue new file mode 100644 index 000000000000..8cb2a9997e58 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue @@ -0,0 +1,13 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/index.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/index.vue new file mode 100644 index 000000000000..74513c5697f3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue new file mode 100644 index 000000000000..e83392b37b5c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue @@ -0,0 +1,23 @@ + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts new file mode 100644 index 000000000000..aa1ff8e9743c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts @@ -0,0 +1,19 @@ +import { fileURLToPath } from 'node:url'; +import type { ConfigOptions } from '@nuxt/test-utils/playwright'; +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const nuxtConfigOptions: ConfigOptions = { + nuxt: { + rootDir: fileURLToPath(new URL('.', import.meta.url)), + }, +}; + +/* Make sure to import from '@nuxt/test-utils/playwright' in the tests + * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */ + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, + use: { ...nuxtConfigOptions }, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/public/favicon.ico b/dev-packages/e2e-tests/test-applications/nuxt-3-min/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..18993ad91cfd43e03b074dd0b5cc3f37ab38e49c GIT binary patch literal 4286 zcmeHLOKuuL5PjK%MHWVi6lD zOGiREbCw`xmFozJ^aNatJY>w+g ze6a2@u~m#^BZm@8wco9#Crlli0uLb^3E$t2-WIc^#(?t)*@`UpuofJ(Uyh@F>b3Ph z$D^m8Xq~pTkGJ4Q`Q2)te3mgkWYZ^Ijq|hkiP^9`De={bQQ%heZC$QU2UpP(-tbl8 zPWD2abEew;oat@w`uP3J^YpsgT%~jT(Dk%oU}sa$7|n6hBjDj`+I;RX(>)%lm_7N{+B7Mu%H?422lE%MBJH!!YTN2oT7xr>>N-8OF$C&qU^ z>vLsa{$0X%q1fjOe3P1mCv#lN{xQ4_*HCSAZjTb1`}mlc+9rl8$B3OP%VT@mch_~G z7Y+4b{r>9e=M+7vSI;BgB?ryZDY4m>&wcHSn81VH1N~`0gvwH{ z8dv#hG|OK`>1;j7tM#B)Z7zDN?{6=dUal}$e { + throw new Error('Nuxt 3 Param Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/server-error.ts new file mode 100644 index 000000000000..ec961a010510 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/server-error.ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from '#imports'; + +export default defineEventHandler(event => { + throw new Error('Nuxt 3 Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/test-param/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/test-param/[param].ts new file mode 100644 index 000000000000..1867874cd494 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/test-param/[param].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from '#imports'; + +export default defineEventHandler(event => { + const param = getRouterParam(event, 'param'); + + return `Param: ${param}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/tsconfig.json new file mode 100644 index 000000000000..b9ed69c19eaf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nuxt-3-min/start-event-proxy.mjs new file mode 100644 index 000000000000..f7e78ea06418 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nuxt-3-min', +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts new file mode 100644 index 000000000000..66f86755218e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts @@ -0,0 +1,105 @@ +import { expect, test } from '@nuxt/test-utils/playwright'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', async () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-min', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Nuxt-3-min E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual('/client-error'); + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Nuxt-3-min E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); + + test('shows parametrized route on button error', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-min', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Param Route Button'; + }); + + await page.goto(`/test-param/1234`); + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error.sdk.name).toEqual('sentry.javascript.nuxt'); + expect(error.transaction).toEqual('/test-param/:param()'); + expect(error.request.url).toMatch(/\/test-param\/1234/); + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Param Route Button', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); + + test('page is still interactive after client error', async ({ page }) => { + const error1Promise = waitForError('nuxt-3-min', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Nuxt-3-min E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + + const error1 = await error1Promise; + + const error2Promise = waitForError('nuxt-3-min', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Another Error thrown from Nuxt-3-min E2E test app'; + }); + + await page.locator('#errorBtn2').click(); + + const error2 = await error2Promise; + + expect(error1).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Nuxt-3-min E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + + expect(error2).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Another Error thrown from Nuxt-3-min E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts new file mode 100644 index 000000000000..8f20aa938893 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', async () => { + test('captures api fetch error (fetched on click)', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-min', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Server error'; + }); + + await page.goto(`/fetch-server-error`); + await page.getByText('Fetch Server Data', { exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual('GET /api/server-error'); + + const exception = error.exception.values[0]; + expect(exception.type).toEqual('Error'); + expect(exception.value).toEqual('Nuxt 3 Server error'); + expect(exception.mechanism.handled).toBe(false); + }); + + test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-min', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Param Server error'; + }); + + await page.goto(`/test-param/1234`); + await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual('GET /api/param-error/1234'); + + const exception = error.exception.values[0]; + expect(exception.type).toEqual('Error'); + expect(exception.value).toEqual('Nuxt 3 Param Server error'); + expect(exception.mechanism.handled).toBe(false); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts new file mode 100644 index 000000000000..9d0b3c694a1c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@nuxt/test-utils/playwright'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { Span } from '@sentry/nuxt'; + +test('sends a pageload root span with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3-min', async transactionEvent => { + return transactionEvent.transaction === '/test-param/:param()'; + }); + + await page.goto(`/test-param/1234`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'params.param': '1234', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/test-param/:param()', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3-min', async transactionEvent => { + return transactionEvent.transaction === '/client-error'; + }); + + await page.goto(`/client-error`); + + const rootSpan = await transactionPromise; + const errorButtonSpan = rootSpan.spans.find((span: Span) => span.description === 'Vue '); + + const expected = { + data: { 'sentry.origin': 'auto.ui.vue', 'sentry.op': 'ui.vue.mount' }, + description: 'Vue ', + op: 'ui.vue.mount', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.ui.vue', + }; + + expect(errorButtonSpan).toMatchObject(expected); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts new file mode 100644 index 000000000000..6f2085e38cd7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3-min', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transaction = await transactionPromise; + + expect(transaction.contexts.trace).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.http', + }), + }), + ); +}); + +test('does not send transactions for build asset folder "_nuxt"', async ({ page }) => { + let buildAssetFolderOccurred = false; + + waitForTransaction('nuxt-3-min', transactionEvent => { + if (transactionEvent.transaction?.match(/^GET \/_nuxt\//)) { + buildAssetFolderOccurred = true; + } + return false; // expects to return a boolean (but not relevant here) + }); + + const transactionEventPromise = waitForTransaction('nuxt-3-min', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transactionEvent = await transactionEventPromise; + + expect(buildAssetFolderOccurred).toBe(false); + + // todo: url not yet parametrized + expect(transactionEvent.transaction).toBe('GET /test-param/1234'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts new file mode 100644 index 000000000000..b110f27843e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('distributed tracing', () => { + const PARAM = 's0me-param'; + + test('capture a distributed pageload trace', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { + return txnEvent.transaction === '/test-param/:param()'; + }); + + const serverTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { + return txnEvent.transaction.includes('GET /test-param/'); + }); + + const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([ + page.goto(`/test-param/${PARAM}`), + clientTxnEventPromise, + serverTxnEventPromise, + expect(page.getByText(`Param: ${PARAM}`)).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/test-param/:param()', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /test-param/s0me-param', // todo: parametrize (nitro) + transaction_info: { source: 'url' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.otel.http', + }, + }, + }); + + // connected trace + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tsconfig.json new file mode 100644 index 000000000000..a746f2a70c28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tsconfig.json @@ -0,0 +1,4 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json index 6c8eb1fcdd95..0b9654108d48 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@sentry/nuxt": "latest || *", - "nuxt": "3.13.1" + "nuxt": "^3.13.1" }, "devDependencies": { "@nuxt/test-utils": "^3.14.1", From 5b773779693cb52c9173c67c42cf2a9e48e927cb Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 14 Nov 2024 15:57:44 +0100 Subject: [PATCH 10/29] fix(core): Set `sentry.source` attribute to `custom` when calling `span.updateName` on `SentrySpan` (#14251) Fix a "regression" introduced in v8 where we no longer updated the source of the span when calling `span.updateName`. We had this behaviour in our v7 `Transaction` class (IIRC via `transaction.setName(name, source)` but most likely forgot to port it to the `Span` class as the `event.transaction_info.source` field is only relevant for transactions (i.e. root spans in today's SDK lingo). --- .size-limit.js | 2 +- .../suites/public-api/startSpan/basic/test.ts | 41 +++++++++++++------ .../pageload-update-txn-name/init.js | 10 +++++ .../pageload-update-txn-name/subject.js | 3 ++ .../pageload-update-txn-name/test.ts | 36 ++++++++++++++++ .../pageload/test.ts | 17 +++++++- packages/core/src/tracing/sentrySpan.ts | 1 + .../core/test/lib/tracing/sentrySpan.test.ts | 15 ++++++- .../appRouterRoutingInstrumentation.ts | 3 ++ packages/solid/src/solidrouter.ts | 1 + 10 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts diff --git a/.size-limit.js b/.size-limit.js index 4903d38fef62..3f1be5b9c140 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -139,7 +139,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '38 KB', + limit: '38.5 KB', }, // Svelte SDK (ESM) { diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts index f1152bde21da..3b64a1230a5b 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts @@ -1,5 +1,10 @@ import { expect } from '@playwright/test'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, @@ -7,18 +12,30 @@ import { waitForTransactionRequestOnUrl, } from '../../../../utils/helpers'; -sentryTest('should send a transaction in an envelope', async ({ getLocalTestPath, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestPath({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const transaction = envelopeRequestParser(req); - - expect(transaction.transaction).toBe('parent_span'); - expect(transaction.spans).toBeDefined(); -}); +sentryTest( + 'sends a transaction in an envelope with manual origin and custom source', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + const req = await waitForTransactionRequestOnUrl(page, url); + const transaction = envelopeRequestParser(req); + + const attributes = transaction.contexts?.trace?.data; + expect(attributes).toEqual({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }); + + expect(transaction.transaction_info?.source).toBe('custom'); + + expect(transaction.transaction).toBe('parent_span'); + expect(transaction.spans).toBeDefined(); + }, +); sentryTest('should report finished spans as children of the root transaction', async ({ getLocalTestPath, page }) => { if (shouldSkipTracingTest()) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js new file mode 100644 index 000000000000..1f0b64911a75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/subject.js new file mode 100644 index 000000000000..6e93018cc063 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/subject.js @@ -0,0 +1,3 @@ +const activeSpan = Sentry.getActiveSpan(); +const rootSpan = activeSpan && Sentry.getRootSpan(activeSpan); +rootSpan?.updateName('new name'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts new file mode 100644 index 000000000000..ff47f1a2d238 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('sets the source to custom when updating the transaction name', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + const traceContextData = eventData.contexts?.trace?.data; + + expect(traceContextData).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }); + + expect(eventData.transaction).toBe('new name'); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('custom'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts index 6a186b63b02a..70f719d8dbbf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts @@ -1,10 +1,16 @@ import { expect } from '@playwright/test'; import type { Event } from '@sentry/types'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('should create a pageload transaction', async ({ getLocalTestPath, page }) => { +sentryTest('creates a pageload transaction with url as source', async ({ getLocalTestPath, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } @@ -16,8 +22,17 @@ sentryTest('should create a pageload transaction', async ({ getLocalTestPath, pa const { start_timestamp: startTimestamp } = eventData; + const traceContextData = eventData.contexts?.trace?.data; + expect(startTimestamp).toBeCloseTo(timeOrigin, 1); + expect(traceContextData).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }); + expect(eventData.contexts?.trace?.op).toBe('pageload'); expect(eventData.spans?.length).toBeGreaterThan(0); expect(eventData.transaction_info?.source).toEqual('url'); diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 54f59386ff17..4f0096b29773 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -193,6 +193,7 @@ export class SentrySpan implements Span { */ public updateName(name: string): this { this._name = name; + this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); return this; } diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 9698ab5e3398..8e43123e3f3c 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -1,5 +1,5 @@ import { timestampInSeconds } from '@sentry/utils'; -import { setCurrentClient } from '../../../src'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setCurrentClient } from '../../../src'; import { SentrySpan } from '../../../src/tracing/sentrySpan'; import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus'; import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, spanToJSON } from '../../../src/utils/spanUtils'; @@ -20,6 +20,19 @@ describe('SentrySpan', () => { expect(spanToJSON(span).description).toEqual('new name'); }); + + it('sets the source to custom when calling updateName', () => { + const span = new SentrySpan({ + name: 'original name', + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, + }); + + span.updateName('new name'); + + const spanJson = spanToJSON(span); + expect(spanJson.description).toEqual('new name'); + expect(spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('custom'); + }); }); describe('setters', () => { diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index c44ef444fdf7..4c92e0999f57 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -61,6 +61,7 @@ export function appRouterInstrumentNavigation(client: Client): void { WINDOW.addEventListener('popstate', () => { if (currentNavigationSpan && currentNavigationSpan.isRecording()) { currentNavigationSpan.updateName(WINDOW.location.pathname); + currentNavigationSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); } else { currentNavigationSpan = startBrowserTracingNavigationSpan(client, { name: WINDOW.location.pathname, @@ -105,9 +106,11 @@ export function appRouterInstrumentNavigation(client: Client): void { if (routerFunctionName === 'push') { span?.updateName(transactionNameifyRouterArgument(argArray[0])); + span?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); span?.setAttribute('navigation.type', 'router.push'); } else if (routerFunctionName === 'replace') { span?.updateName(transactionNameifyRouterArgument(argArray[0])); + span?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); span?.setAttribute('navigation.type', 'router.replace'); } else if (routerFunctionName === 'back') { span?.setAttribute('navigation.type', 'router.back'); diff --git a/packages/solid/src/solidrouter.ts b/packages/solid/src/solidrouter.ts index da0391dea35e..b4c0972decff 100644 --- a/packages/solid/src/solidrouter.ts +++ b/packages/solid/src/solidrouter.ts @@ -89,6 +89,7 @@ function withSentryRouterRoot(Root: Component): Component Date: Thu, 14 Nov 2024 20:19:07 +0100 Subject: [PATCH 11/29] feat(replay): Upgrade rrweb packages to 2.29.0 (#14160) Change to stop restarting canvas manager web worker when stopping recording. --- .../browser-integration-tests/package.json | 2 +- packages/replay-canvas/package.json | 2 +- packages/replay-internal/package.json | 4 +- yarn.lock | 75 ++++++------------- 4 files changed, 25 insertions(+), 58 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index b2c913488d42..5dfd72c4dcdf 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -42,7 +42,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.44.1", - "@sentry-internal/rrweb": "2.11.0", + "@sentry-internal/rrweb": "2.29.0", "@sentry/browser": "8.38.0", "axios": "1.7.7", "babel-loader": "^8.2.2", diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index e68e89ba8fe8..34bdabc15cb8 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -65,7 +65,7 @@ }, "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { - "@sentry-internal/rrweb": "2.28.0" + "@sentry-internal/rrweb": "2.29.0" }, "dependencies": { "@sentry-internal/replay": "8.38.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 7ccdeb4f62e2..50cebfab92e2 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -69,8 +69,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "8.38.0", - "@sentry-internal/rrweb": "2.28.0", - "@sentry-internal/rrweb-snapshot": "2.28.0", + "@sentry-internal/rrweb": "2.29.0", + "@sentry-internal/rrweb-snapshot": "2.29.0", "fflate": "^0.8.1", "jest-matcher-utils": "^29.0.0", "jsdom-worker": "^0.2.1" diff --git a/yarn.lock b/yarn.lock index 01efab263fca..ee2e6542a2e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8446,67 +8446,34 @@ "@angular-devkit/schematics" "14.2.13" jsonc-parser "3.1.0" -"@sentry-internal/rrdom@2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.11.0.tgz#f7c8f54705ad84ece0e97e53f12e87c687749b32" - integrity sha512-BZnkTrbLm9Y3R70W1+8TnImys0RbKsgyB70WQoFdUervGvPw1kLcWJOJrPcDWgVe7nlbG+bEWb6iQrvLqldycw== - dependencies: - "@sentry-internal/rrweb-snapshot" "2.11.0" - -"@sentry-internal/rrdom@2.28.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.28.0.tgz#91c55332e3392a8cc05b8e593ee9f6aa740cf5c3" - integrity sha512-9UqcIfy+ygCPpoXBAtlD3VxiTgaFQmYyqtvsL9b3lP1Wcj/rcd8ZZH7iFhT4AzA1bCi8Kx+VcYZxr09hZr5Qig== - dependencies: - "@sentry-internal/rrweb-snapshot" "2.28.0" - -"@sentry-internal/rrweb-snapshot@2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.11.0.tgz#1af79130604afea989d325465b209ac015b27c9a" - integrity sha512-1nP22QlplMNooSNvTh+L30NSZ+E3UcfaJyxXSMLxUjQHTGPyM1VkndxZMmxlKhyR5X+rLbxi/+RvuAcpM43VoA== - -"@sentry-internal/rrweb-snapshot@2.28.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.28.0.tgz#00e330fb0ecb569638af4b2236ed410c92dd8258" - integrity sha512-8pHeVKfmZPoWyWPOT2TbPc4fGnDMtaiHqMLLbDwUtLT9fkEq8AAv5UwfpY3utneIXuxaf1DaF7FgDSqpAWWkAw== - -"@sentry-internal/rrweb-types@2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.11.0.tgz#e598c133b87be1fb04d31d09773b86142b095072" - integrity sha512-foCf9DGfN5ffzwykEtIXsV1P5d+XLDVGaQUnKF5ecGn+g5JzKTe/rPC92rL8/gEy2unL5sCTvlYL3DQvUFM4dA== +"@sentry-internal/rrdom@2.29.0": + version "2.29.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.29.0.tgz#df60564466718ae7ada376cf1bd483b8ee07831a" + integrity sha512-TXhujPMt0Iq4l/sjm+rdU/CI6yR8K9+NheKPbCrs3UBzQHbu2VglrlEmhyx57mJY2GwRBrvLcCr5NokX7v1eBA== dependencies: - "@sentry-internal/rrweb-snapshot" "2.11.0" + "@sentry-internal/rrweb-snapshot" "2.29.0" -"@sentry-internal/rrweb-types@2.28.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.28.0.tgz#6d3262879b9d97fd84adb8df7083d0a3b7dba18d" - integrity sha512-Xmyb6U3eGloFTHp6cv5KbN5cyL1fYF0GMxTSZd2/mVcSfgr09z8XVp0WWOcxhNouzhrz9OeLDotaDo45D8rROg== - dependencies: - "@sentry-internal/rrweb-snapshot" "2.28.0" - "@types/css-font-loading-module" "0.0.7" +"@sentry-internal/rrweb-snapshot@2.29.0": + version "2.29.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.29.0.tgz#b0bb64ccffbd486bb739c87d481aa8cdcd7d5c05" + integrity sha512-nIf593YObUzdmEilT3LEXBTpcVGXRYlYTgxiESeJgXrEmNoeB1BolKh4OJa5KpEmwmHcfe3zl15GdzhjxOIwAA== -"@sentry-internal/rrweb@2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.11.0.tgz#be8e8dfff2acf64d418b625d35a20fdcd7daeb96" - integrity sha512-QuEqpKmRDb0xQe9fhJ3j/JHO6uxFMWBowADJBA4rvVU5HbExIg9gor1tZ0b3gDuChXnnx7pxFj9/QXZjQQ75zg== +"@sentry-internal/rrweb-types@2.29.0": + version "2.29.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.29.0.tgz#71b20e6dd452f005ff37f059df2dacad98f6e0ea" + integrity sha512-0x1aT+ifDjX3JKd4kmGzbofkI6qWYAOZmd5tPX07OmVnT3aIoecBqBCUagx15ewm0kMRv5Pl53is0EWzHIDvlA== dependencies: - "@sentry-internal/rrdom" "2.11.0" - "@sentry-internal/rrweb-snapshot" "2.11.0" - "@sentry-internal/rrweb-types" "2.11.0" + "@sentry-internal/rrweb-snapshot" "2.29.0" "@types/css-font-loading-module" "0.0.7" - "@xstate/fsm" "^1.4.0" - base64-arraybuffer "^1.0.1" - fflate "^0.4.4" - mitt "^3.0.0" -"@sentry-internal/rrweb@2.28.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.28.0.tgz#353ac98e3308ce8e41a3e1e3a139a9c3db10b4eb" - integrity sha512-gX5gjE4xotHFrpqACP5jNCgmiUHb6pz8wWJnvC3lrc8aBUS1xNEIel4DkKjyGs9e9OY+MQk+nJghoIiLZwisSA== +"@sentry-internal/rrweb@2.29.0": + version "2.29.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.29.0.tgz#1019bee52be0ed4bd3112a3e1a1c50adfb6bab78" + integrity sha512-UmEtyfo3yCdJsIdt0m7OLLmg9CeNmGlkmGSa91nResZVIC1+rd4RA+PmmqkwAV/WOljCXHZHs7ezlW1Mjjm2vQ== dependencies: - "@sentry-internal/rrdom" "2.28.0" - "@sentry-internal/rrweb-snapshot" "2.28.0" - "@sentry-internal/rrweb-types" "2.28.0" + "@sentry-internal/rrdom" "2.29.0" + "@sentry-internal/rrweb-snapshot" "2.29.0" + "@sentry-internal/rrweb-types" "2.29.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" From 12902c5dd89e4047b9510108bc4733c06556c517 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 15 Nov 2024 10:28:09 +0100 Subject: [PATCH 12/29] ref(utils): Stop setting `transaction` in `requestDataIntegration` (#14306) This is not really necessary anymore - it only sets this on transaction events, and those get the `transaction` in different places already anyhow. With this, we can also actually remove some other stuff. One method is exported from utils but not otherwise used, we can also drop this in v9. Finally, this was also the only place that used `route` on the request, so we can also get rid of this in `remix`, which is weird anyhow because we set it for errors there but don't even use it for them. --------- Co-authored-by: Luca Forstner --- docs/migration/draft-v9-migration-guide.md | 13 ++++ packages/core/src/integrations/requestdata.ts | 7 ++- packages/remix/src/utils/errors.ts | 19 +----- packages/remix/src/utils/instrumentServer.ts | 3 - packages/utils/src/requestdata.ts | 30 ++------- packages/utils/test/requestdata.test.ts | 63 +------------------ 6 files changed, 28 insertions(+), 107 deletions(-) create mode 100644 docs/migration/draft-v9-migration-guide.md diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md new file mode 100644 index 000000000000..5630d82ede90 --- /dev/null +++ b/docs/migration/draft-v9-migration-guide.md @@ -0,0 +1,13 @@ + + +# Deprecations + +## `@sentry/utils` + +- Deprecated `AddRequestDataToEventOptions.transaction`. This option effectively doesn't do anything anymore, and will + be removed in v9. +- Deprecated `TransactionNamingScheme` type. + +## `@sentry/core` + +- Deprecated `transactionNamingScheme` option in `requestDataIntegration`. diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index 9475973d8e2c..15be450cf27e 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -24,7 +24,11 @@ export type RequestDataIntegrationOptions = { }; }; - /** Whether to identify transactions by parameterized path, parameterized path with method, or handler name */ + /** + * Whether to identify transactions by parameterized path, parameterized path with method, or handler name. + * @deprecated This option does not do anything anymore, and will be removed in v9. + */ + // eslint-disable-next-line deprecation/deprecation transactionNamingScheme?: TransactionNamingScheme; }; @@ -110,6 +114,7 @@ function convertReqDataIntegrationOptsToAddReqDataOpts( integrationOptions: Required, ): AddRequestDataToEventOptions { const { + // eslint-disable-next-line deprecation/deprecation transactionNamingScheme, include: { ip, user, ...requestOptions }, } = integrationOptions; diff --git a/packages/remix/src/utils/errors.ts b/packages/remix/src/utils/errors.ts index 92958c2c3eb3..100dac496c75 100644 --- a/packages/remix/src/utils/errors.ts +++ b/packages/remix/src/utils/errors.ts @@ -1,12 +1,5 @@ import type { AppData, DataFunctionArgs, EntryContext, HandleDocumentRequestFunction } from '@remix-run/node'; -import { - captureException, - getActiveSpan, - getClient, - getRootSpan, - handleCallbackErrors, - spanToJSON, -} from '@sentry/core'; +import { captureException, getClient, handleCallbackErrors } from '@sentry/core'; import type { Span } from '@sentry/types'; import { addExceptionMechanism, isPrimitive, logger, objectify } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; @@ -61,19 +54,9 @@ export async function captureRemixServerException( const objectifiedErr = objectify(err); captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => { - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - const activeRootSpanName = rootSpan ? spanToJSON(rootSpan).description : undefined; - scope.setSDKProcessingMetadata({ request: { ...normalizedRequest, - // When `route` is not defined, `RequestData` integration uses the full URL - route: activeRootSpanName - ? { - path: activeRootSpanName, - } - : undefined, }, }); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index e83c14dfbbc4..666c332afa04 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -314,9 +314,6 @@ function wrapRequestHandler( isolationScope.setSDKProcessingMetadata({ request: { ...normalizedRequest, - route: { - path: name, - }, }, }); diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index edffc6f67da7..26b12b07c69e 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -21,7 +21,6 @@ import { getClientIPAddress, ipHeaderNames } from './vendor/getIpAddress'; const DEFAULT_INCLUDES = { ip: false, request: true, - transaction: true, user: true, }; const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; @@ -35,6 +34,8 @@ export type AddRequestDataToEventOptions = { include?: { ip?: boolean; request?: boolean | Array<(typeof DEFAULT_REQUEST_INCLUDES)[number]>; + /** @deprecated This option will be removed in v9. It does not do anything anymore, the `transcation` is set in other places. */ + // eslint-disable-next-line deprecation/deprecation transaction?: boolean | TransactionNamingScheme; user?: boolean | Array<(typeof DEFAULT_USER_INCLUDES)[number]>; }; @@ -52,6 +53,9 @@ export type AddRequestDataToEventOptions = { }; }; +/** + * @deprecated This type will be removed in v9. It is not in use anymore. + */ export type TransactionNamingScheme = 'path' | 'methodPath' | 'handler'; /** @@ -67,6 +71,7 @@ export type TransactionNamingScheme = 'path' | 'methodPath' | 'handler'; * used instead of the request's route) * * @returns A tuple of the fully constructed transaction name [0] and its source [1] (can be either 'route' or 'url') + * @deprecated This method will be removed in v9. It is not in use anymore. */ export function extractPathForTransaction( req: PolymorphicRequest, @@ -102,23 +107,6 @@ export function extractPathForTransaction( return [name, source]; } -function extractTransaction(req: PolymorphicRequest, type: boolean | TransactionNamingScheme): string { - switch (type) { - case 'path': { - return extractPathForTransaction(req, { path: true })[0]; - } - case 'handler': { - return (req.route && req.route.stack && req.route.stack[0] && req.route.stack[0].name) || ''; - } - case 'methodPath': - default: { - // if exist _reconstructedRoute return that path instead of route.path - const customRoute = req._reconstructedRoute ? req._reconstructedRoute : undefined; - return extractPathForTransaction(req, { path: true, method: true, customRoute })[0]; - } - } -} - function extractUserData( user: { [key: string]: unknown; @@ -379,12 +367,6 @@ export function addRequestDataToEvent( } } - if (include.transaction && !event.transaction && event.type === 'transaction') { - // TODO do we even need this anymore? - // TODO make this work for nextjs - event.transaction = extractTransaction(req, include.transaction); - } - return event; } diff --git a/packages/utils/test/requestdata.test.ts b/packages/utils/test/requestdata.test.ts index 570f80647b6b..90c734f23f2c 100644 --- a/packages/utils/test/requestdata.test.ts +++ b/packages/utils/test/requestdata.test.ts @@ -369,67 +369,6 @@ describe('addRequestDataToEvent', () => { } }); }); - - describe('transaction property', () => { - describe('for transaction events', () => { - beforeEach(() => { - mockEvent.type = 'transaction'; - }); - - test('extracts method and full route path by default`', () => { - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - - expect(parsedRequest.transaction).toEqual('POST /routerMountPath/subpath/:parameterName'); - }); - - test('extracts method and full path by default when mountpoint is `/`', () => { - mockReq.originalUrl = mockReq.originalUrl.replace('/routerMountpath', ''); - mockReq.baseUrl = ''; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - - // `subpath/` is the full path here, because there's no router mount path - expect(parsedRequest.transaction).toEqual('POST /subpath/:parameterName'); - }); - - test('fallback to method and `originalUrl` if route is missing', () => { - delete mockReq.route; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - - expect(parsedRequest.transaction).toEqual('POST /routerMountPath/subpath/specificValue'); - }); - - test('can extract path only instead if configured', () => { - const optionsWithPathTransaction = { - include: { - transaction: 'path', - }, - } as const; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq, optionsWithPathTransaction); - - expect(parsedRequest.transaction).toEqual('/routerMountPath/subpath/:parameterName'); - }); - - test('can extract handler name instead if configured', () => { - const optionsWithHandlerTransaction = { - include: { - transaction: 'handler', - }, - } as const; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq, optionsWithHandlerTransaction); - - expect(parsedRequest.transaction).toEqual('parameterNameRouteHandler'); - }); - }); - it('transaction is not applied to non-transaction events', () => { - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - - expect(parsedRequest.transaction).toBeUndefined(); - }); - }); }); describe('extractRequestData', () => { @@ -763,6 +702,7 @@ describe('extractPathForTransaction', () => { expectedRoute: string, expectedSource: TransactionSource, ) => { + // eslint-disable-next-line deprecation/deprecation const [route, source] = extractPathForTransaction(req, options); expect(route).toEqual(expectedRoute); @@ -778,6 +718,7 @@ describe('extractPathForTransaction', () => { originalUrl: '/api/users/123/details', } as PolymorphicRequest; + // eslint-disable-next-line deprecation/deprecation const [route, source] = extractPathForTransaction(req, { path: true, method: true, From a5a214c68c53ff20760216f303fa2e274d0f0516 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 15 Nov 2024 12:54:43 +0100 Subject: [PATCH 13/29] ci: Only run E2E test apps that are affected on PRs (#14254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR aims to solve two things: 1. Do not require us to keep a list of test-applications to run on CI. This is prone to be forgotten, and then certain tests do not run on CI and we may not notice (this has happened more than once in the past 😬 ) 2. Do not run E2E test applications that are unrelated to anything that was changed. With this PR, instead of keeping a list of E2E test jobs we want to run manually in the build.yml file, this is now inferred (on CI) by a script based on the nx dependency graph and some config in the test apps package.json. This is how it works: 1. Pick all folders in test-applications, by default all of them should run. --> This will "fix" the problem we had sometimes that we forgot to add test apps to the build.yml so they don't actually run on CI :grimacing: 3. For each test app, look at it's dependencies (and devDependencies), and see if any of them has changed in the current PR (based on nx affected projects). So e.g. if you change something in browser, only E2E test apps that have any dependency that uses browser will run, so e.g. node-express will be skipped. 4. Additionally, you can configure variants in the test apps package.json - instead of defining this in the workflow file, it has to be defined in the test app itself now. 5. Finally, a test app can be marked as optional in the package.json, and can also have optional variants to run. 6. The E2E test builds the required & optional matrix on the fly based on these things. 7. In pushes (=on develop/release branches) we just run everything. 8. If anything inside of the e2e-tests package changed, run everything. --> This could be further optimized to only run changed test apps, but this was the easy solution for now. ## Example test runs * In a PR with no changes related to the E2E tests, nothing is run: https://github.com/getsentry/sentry-javascript/actions/runs/11838604965 * In a CI run for a push, all E2E tests are run: https://github.com/getsentry/sentry-javascript/actions/runs/11835834748 * In a PR with a change in e2e-tests itself, all E2E tests are run: https://github.com/getsentry/sentry-javascript/actions/runs/11836668035 * In a PR with a change in the node package, only related E2E tests are run: https://github.com/getsentry/sentry-javascript/actions/runs/11838274483 --- .github/workflows/build.yml | 174 +++--------------- dev-packages/e2e-tests/lib/getTestMatrix.ts | 155 ++++++++++++++++ dev-packages/e2e-tests/package.json | 4 +- .../cloudflare-astro/package.json | 3 + .../cloudflare-workers/package.json | 3 + .../create-next-app/package.json | 8 + .../create-react-app/package.json | 8 + .../create-remix-app-legacy/package.json | 8 + .../create-remix-app/package.json | 8 + .../debug-id-sourcemaps/package.json | 3 + .../generic-ts3.8/package.json | 3 +- .../test-applications/nextjs-13/package.json | 12 ++ .../test-applications/nextjs-14/package.json | 12 ++ .../test-applications/nextjs-15/package.json | 12 ++ .../nextjs-app-dir/package.json | 18 ++ .../nextjs-turbo/package.json | 12 ++ .../node-express-send-to-sentry/package.json | 3 + .../node-profiling/package.json | 3 + .../react-router-6/package.json | 8 + .../react-send-to-sentry/package.json | 3 + yarn.lock | 58 ++---- 21 files changed, 326 insertions(+), 192 deletions(-) create mode 100644 dev-packages/e2e-tests/lib/getTestMatrix.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e80e2e869905..72d572c01e5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -801,7 +801,15 @@ jobs: needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node] runs-on: ubuntu-20.04-large-js timeout-minutes: 15 + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + matrix-optional: ${{ steps.matrix-optional.outputs.matrix }} steps: + - name: Check out base commit (${{ github.event.pull_request.base.sha }}) + uses: actions/checkout@v4 + if: github.event_name == 'pull_request' + with: + ref: ${{ github.event.pull_request.base.sha }} - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 with: @@ -851,11 +859,21 @@ jobs: path: ${{ github.workspace }}/packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + - name: Determine which E2E test applications should be run + id: matrix + run: yarn --silent ci:build-matrix --base=${{ (github.event_name == 'pull_request' && github.event.pull_request.base.sha) || '' }} >> $GITHUB_OUTPUT + working-directory: dev-packages/e2e-tests + + - name: Determine which optional E2E test applications should be run + id: matrix-optional + run: yarn --silent ci:build-matrix-optional --base=${{ (github.event_name == 'pull_request' && github.event.pull_request.base.sha) || '' }} >> $GITHUB_OUTPUT + working-directory: dev-packages/e2e-tests + job_e2e_tests: name: E2E ${{ matrix.label || matrix.test-application }} Test # We need to add the `always()` check here because the previous step has this as well :( # See: https://github.com/actions/runner/issues/2205 - if: always() && needs.job_e2e_prepare.result == 'success' + if: always() && needs.job_e2e_prepare.result == 'success' && needs.job_e2e_prepare.outputs.matrix != '{"include":[]}' needs: [job_get_metadata, job_build, job_e2e_prepare] runs-on: ubuntu-20.04 timeout-minutes: 15 @@ -870,105 +888,7 @@ jobs: E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests' strategy: fail-fast: false - matrix: - is_dependabot: - - ${{ github.actor == 'dependabot[bot]' }} - test-application: - [ - 'angular-17', - 'angular-18', - 'astro-4', - 'aws-lambda-layer-cjs', - 'aws-serverless-esm', - 'node-express', - 'create-react-app', - 'create-next-app', - 'create-remix-app', - 'create-remix-app-legacy', - 'create-remix-app-v2', - 'create-remix-app-v2-legacy', - 'create-remix-app-express', - 'create-remix-app-express-legacy', - 'create-remix-app-express-vite-dev', - 'default-browser', - 'node-express-esm-loader', - 'node-express-esm-preload', - 'node-express-esm-without-loader', - 'node-express-cjs-preload', - 'node-otel-sdk-node', - 'node-otel-custom-sampler', - 'node-otel-without-tracing', - 'ember-classic', - 'ember-embroider', - 'nextjs-app-dir', - 'nextjs-13', - 'nextjs-14', - 'nextjs-15', - 'nextjs-turbo', - 'nextjs-t3', - 'react-17', - 'react-19', - 'react-create-hash-router', - 'react-router-6-use-routes', - 'react-router-5', - 'react-router-6', - 'solid', - 'solidstart', - 'solidstart-spa', - 'svelte-5', - 'sveltekit', - 'sveltekit-2', - 'sveltekit-2-svelte-5', - 'sveltekit-2-twp', - 'tanstack-router', - 'generic-ts3.8', - 'node-fastify', - 'node-fastify-5', - 'node-hapi', - 'node-nestjs-basic', - 'node-nestjs-distributed-tracing', - 'nestjs-basic', - 'nestjs-8', - 'nestjs-distributed-tracing', - 'nestjs-with-submodules', - 'nestjs-with-submodules-decorator', - 'nestjs-basic-with-graphql', - 'nestjs-graphql', - 'node-exports-test-app', - 'node-koa', - 'node-connect', - 'nuxt-3', - 'nuxt-3-min', - 'nuxt-4', - 'vue-3', - 'webpack-4', - 'webpack-5' - ] - build-command: - - false - label: - - false - # Add any variations of a test app here - # You should provide an alternate build-command as well as a matching label - include: - - test-application: 'create-react-app' - build-command: 'test:build-ts3.8' - label: 'create-react-app (TS 3.8)' - - test-application: 'react-router-6' - build-command: 'test:build-ts3.8' - label: 'react-router-6 (TS 3.8)' - - test-application: 'create-next-app' - build-command: 'test:build-13' - label: 'create-next-app (next@13)' - - test-application: 'nextjs-app-dir' - build-command: 'test:build-13' - label: 'nextjs-app-dir (next@13)' - exclude: - - is_dependabot: true - test-application: 'cloudflare-astro' - - is_dependabot: true - test-application: 'cloudflare-workers' - + matrix: ${{ fromJson(needs.job_e2e_prepare.outputs.matrix) }} steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -1069,6 +989,7 @@ jobs: # See: https://github.com/actions/runner/issues/2205 if: always() && needs.job_e2e_prepare.result == 'success' && + needs.job_e2e_prepare.outputs.matrix-optional != '{"include":[]}' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' needs: [job_get_metadata, job_build, job_e2e_prepare] @@ -1085,58 +1006,7 @@ jobs: E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests' strategy: fail-fast: false - matrix: - test-application: - [ - 'cloudflare-astro', - 'cloudflare-workers', - 'react-send-to-sentry', - 'node-express-send-to-sentry', - 'debug-id-sourcemaps', - ] - build-command: - - false - assert-command: - - false - label: - - false - include: - - test-application: 'create-remix-app' - assert-command: 'test:assert-sourcemaps' - label: 'create-remix-app (sourcemaps)' - - test-application: 'create-remix-app-legacy' - assert-command: 'test:assert-sourcemaps' - label: 'create-remix-app-legacy (sourcemaps)' - - test-application: 'nextjs-app-dir' - build-command: 'test:build-canary' - label: 'nextjs-app-dir (canary)' - - test-application: 'nextjs-app-dir' - build-command: 'test:build-latest' - label: 'nextjs-app-dir (latest)' - - test-application: 'nextjs-13' - build-command: 'test:build-canary' - label: 'nextjs-13 (canary)' - - test-application: 'nextjs-13' - build-command: 'test:build-latest' - label: 'nextjs-13 (latest)' - - test-application: 'nextjs-14' - build-command: 'test:build-canary' - label: 'nextjs-14 (canary)' - - test-application: 'nextjs-14' - build-command: 'test:build-latest' - label: 'nextjs-14 (latest)' - - test-application: 'nextjs-15' - build-command: 'test:build-canary' - label: 'nextjs-15 (canary)' - - test-application: 'nextjs-15' - build-command: 'test:build-latest' - label: 'nextjs-15 (latest)' - - test-application: 'nextjs-turbo' - build-command: 'test:build-canary' - label: 'nextjs-turbo (canary)' - - test-application: 'nextjs-turbo' - build-command: 'test:build-latest' - label: 'nextjs-turbo (latest)' + matrix: ${{ fromJson(needs.job_e2e_prepare.outputs.matrix-optional) }} steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) diff --git a/dev-packages/e2e-tests/lib/getTestMatrix.ts b/dev-packages/e2e-tests/lib/getTestMatrix.ts new file mode 100644 index 000000000000..342f20cf9820 --- /dev/null +++ b/dev-packages/e2e-tests/lib/getTestMatrix.ts @@ -0,0 +1,155 @@ +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { dirname } from 'path'; +import { parseArgs } from 'util'; +import { sync as globSync } from 'glob'; + +interface MatrixInclude { + /** The test application (directory) name. */ + 'test-application': string; + /** Optional override for the build command to run. */ + 'build-command'?: string; + /** Optional override for the assert command to run. */ + 'assert-command'?: string; + /** Optional label for the test run. If not set, defaults to value of `test-application`. */ + label?: string; +} + +interface PackageJsonSentryTestConfig { + /** If this is true, the test app is optional. */ + optional?: boolean; + /** Variant configs that should be run in non-optional test runs. */ + variants?: Partial[]; + /** Variant configs that should be run in optional test runs. */ + optionalVariants?: Partial[]; + /** Skip this test app for matrix generation. */ + skip?: boolean; +} + +/** + * This methods generates a matrix for the GitHub Actions workflow to run the E2E tests. + * It checks which test applications are affected by the current changes in the PR and then generates a matrix + * including all test apps that have at least one dependency that was changed in the PR. + * If no `--base=xxx` is provided, it will output all test applications. + * + * If `--optional=true` is set, it will generate a matrix of optional test applications only. + * Otherwise, these will be skipped. + */ +function run(): void { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + base: { type: 'string' }, + head: { type: 'string' }, + optional: { type: 'string', default: 'false' }, + }, + }); + + const { base, head, optional } = values; + + const testApplications = globSync('*/package.json', { + cwd: `${__dirname}/../test-applications`, + }).map(filePath => dirname(filePath)); + + // If `--base=xxx` is defined, we only want to get test applications changed since that base + // Else, we take all test applications (e.g. on push) + const includedTestApplications = base + ? getAffectedTestApplications(testApplications, { base, head }) + : testApplications; + + const optionalMode = optional === 'true'; + const includes: MatrixInclude[] = []; + + includedTestApplications.forEach(testApp => { + addIncludesForTestApp(testApp, includes, { optionalMode }); + }); + + // We print this to the output, so the GHA can use it for the matrix + // eslint-disable-next-line no-console + console.log(`matrix=${JSON.stringify({ include: includes })}`); +} + +function addIncludesForTestApp( + testApp: string, + includes: MatrixInclude[], + { optionalMode }: { optionalMode: boolean }, +): void { + const packageJson = getPackageJson(testApp); + + const shouldSkip = packageJson.sentryTest?.skip || false; + const isOptional = packageJson.sentryTest?.optional || false; + const variants = (optionalMode ? packageJson.sentryTest?.optionalVariants : packageJson.sentryTest?.variants) || []; + + if (shouldSkip) { + return; + } + + // Add the basic test-application itself, if it is in the current mode + if (optionalMode === isOptional) { + includes.push({ + 'test-application': testApp, + }); + } + + variants.forEach(variant => { + includes.push({ + 'test-application': testApp, + ...variant, + }); + }); +} + +function getSentryDependencies(appName: string): string[] { + const packageJson = getPackageJson(appName) || {}; + + const dependencies = { + ...packageJson.devDependencies, + ...packageJson.dependencies, + }; + + return Object.keys(dependencies).filter(key => key.startsWith('@sentry')); +} + +function getPackageJson(appName: string): { + dependencies?: { [key: string]: string }; + devDependencies?: { [key: string]: string }; + sentryTest?: PackageJsonSentryTestConfig; +} { + const fullPath = path.resolve(__dirname, '..', 'test-applications', appName, 'package.json'); + + if (!fs.existsSync(fullPath)) { + throw new Error(`Could not find package.json for ${appName}`); + } + + return JSON.parse(fs.readFileSync(fullPath, 'utf8')); +} + +run(); + +function getAffectedTestApplications( + testApplications: string[], + { base = 'develop', head }: { base?: string; head?: string }, +): string[] { + const additionalArgs = [`--base=${base}`]; + + if (head) { + additionalArgs.push(`--head=${head}`); + } + + const affectedProjects = execSync(`yarn --silent nx show projects --affected ${additionalArgs.join(' ')}`) + .toString() + .split('\n') + .map(line => line.trim()) + .filter(Boolean); + + // If something in e2e tests themselves are changed, just run everything + if (affectedProjects.includes('@sentry-internal/e2e-tests')) { + return testApplications; + } + + return testApplications.filter(testApp => { + const sentryDependencies = getSentryDependencies(testApp); + return sentryDependencies.some(dep => affectedProjects.includes(dep)); + }); +} diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index fdb5958462ee..ccf59ef38f9d 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -14,11 +14,13 @@ "test:prepare": "ts-node prepare.ts", "test:validate": "run-s test:validate-configuration test:validate-test-app-setups", "clean": "rimraf tmp node_modules pnpm-lock.yaml && yarn clean:test-applications", + "ci:build-matrix": "ts-node ./lib/getTestMatrix.ts", + "ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true", "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.sveltekit,pnpm-lock.yaml} .last-run.json && pnpm store prune" }, "devDependencies": { "@types/glob": "8.0.0", - "@types/node": "^14.18.0", + "@types/node": "^18.0.0", "dotenv": "16.0.3", "esbuild": "0.20.0", "glob": "8.0.3", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json index fd636122d590..d2fc66736b4f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json @@ -26,5 +26,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json index bb01c0b8a8ad..f7fd08df85f9 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json @@ -24,5 +24,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/package.json b/dev-packages/e2e-tests/test-applications/create-next-app/package.json index 9c240942b3b7..316fb561cdf3 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-next-app/package.json @@ -27,5 +27,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "test:build-13", + "label": "create-next-app (next@13)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/package.json b/dev-packages/e2e-tests/test-applications/create-react-app/package.json index ce3471d2a7d1..916a17260a2a 100644 --- a/dev-packages/e2e-tests/test-applications/create-react-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-react-app/package.json @@ -43,5 +43,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "test:build-ts3.8", + "label": "create-react-app (TS 3.8)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json index 4b7c2c162b86..6b50ddc96b4a 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json @@ -36,5 +36,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "assert-command": "test:assert-sourcemaps", + "label": "create-remix-app-legacy (sourcemaps)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json index db5c5b474ef0..4850fedf1e5d 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json @@ -36,5 +36,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "assert-command": "test:assert-sourcemaps", + "label": "create-remix-app (sourcemaps)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json index 9295b7997ee6..6451610ffe86 100644 --- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -19,5 +19,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } diff --git a/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json b/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json index d13bf86e7c64..80875e5a2d0f 100644 --- a/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json +++ b/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json @@ -10,7 +10,8 @@ "test:assert": "pnpm -v" }, "devDependencies": { - "typescript": "3.8.3" + "typescript": "3.8.3", + "@types/node": "^14.18.0" }, "dependencies": { "@sentry/browser": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json index 3e7a0ac88266..c56d7c6ed204 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json @@ -41,5 +41,17 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-13 (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-13 (latest)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json index bbda1b0144cc..c8fcba03410d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -41,5 +41,17 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-14 (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-14 (latest)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json index 04033e0362b2..1c5754bd66da 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -42,5 +42,17 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-15 (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-15 (latest)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 8ccad25e6ab4..b0bc898d9bd1 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -44,5 +44,23 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "test:build-13", + "label": "nextjs-app-dir (next@13)" + } + ], + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-app-dir (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-app-dir (latest)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json index 900e0b5b2efc..9cf05720fc28 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -45,5 +45,17 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-turbo (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-turbo (latest)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json index 96f61837c597..5a1b9b7d8300 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json @@ -24,5 +24,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/package.json b/dev-packages/e2e-tests/test-applications/node-profiling/package.json index cfe4e136b1c1..8aede827a1f3 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling/package.json @@ -22,5 +22,8 @@ "devDependencies": {}, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "skip": true } } diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/package.json b/dev-packages/e2e-tests/test-applications/react-router-6/package.json index 5171a89eadb3..b3ef37f6bc4a 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6/package.json @@ -51,5 +51,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "test:build-ts3.8", + "label": "react-router-6 (TS 3.8)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json index 95b9c3bd78b4..a6ba509bc09a 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json @@ -47,5 +47,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } diff --git a/yarn.lock b/yarn.lock index ee2e6542a2e0..ab60214fdc7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10061,10 +10061,12 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0": - version "17.0.38" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947" - integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g== +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=18": + version "22.9.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365" + integrity sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ== + dependencies: + undici-types "~6.19.8" "@types/node@16.18.70": version "16.18.70" @@ -10076,13 +10078,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4" integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w== -"@types/node@>=18": - version "22.7.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc" - integrity sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg== - dependencies: - undici-types "~6.19.2" - "@types/node@^10.1.0": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" @@ -10093,6 +10088,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== +"@types/node@^18.0.0": + version "18.19.64" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.64.tgz#122897fb79f2a9ec9c979bded01c11461b2b1478" + integrity sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -12922,25 +12924,7 @@ bluebird@^3.4.6, bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.20.3, body-parser@^1.18.3, body-parser@^1.19.0: - version "1.20.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.13.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - -body-parser@^1.20.3: +body-parser@1.20.3, body-parser@^1.18.3, body-parser@^1.19.0, body-parser@^1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== @@ -28321,13 +28305,6 @@ qs@^6.4.0: dependencies: side-channel "^1.0.4" -qs@6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== - dependencies: - side-channel "^1.0.6" - query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -32419,7 +32396,12 @@ underscore@>=1.8.3: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== -undici-types@~6.19.2: +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +undici-types@~6.19.8: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== From c21bf07545ea231b9e851bb914be8141a0a3cac7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 15 Nov 2024 13:55:37 +0100 Subject: [PATCH 14/29] feat(nestjs): Instrument event handlers (#14307) --- .../nestjs-distributed-tracing/package.json | 1 + .../src/events.controller.ts | 14 +++ .../src/events.module.ts | 21 ++++ .../src/events.service.ts | 14 +++ .../src/listeners/test-event.listener.ts | 16 +++ .../nestjs-distributed-tracing/src/main.ts | 5 + .../tests/events.test.ts | 43 +++++++ packages/nestjs/package.json | 4 +- .../src/integrations/tracing/nest/helpers.ts | 18 +++ .../src/integrations/tracing/nest/nest.ts | 6 + .../nest/sentry-nest-event-instrumentation.ts | 119 ++++++++++++++++++ .../src/integrations/tracing/nest/types.ts | 9 ++ .../test/integrations/tracing/nest.test.ts | 98 +++++++++++++++ yarn.lock | 16 ++- 14 files changed, 380 insertions(+), 4 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.module.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts create mode 100644 packages/node/src/integrations/tracing/nest/sentry-nest-event-instrumentation.ts diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json index b4d0ead875f9..efc52a8a4db9 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json @@ -18,6 +18,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/event-emitter": "^2.0.0", "@sentry/nestjs": "latest || *", "@sentry/types": "latest || *", "reflect-metadata": "^0.2.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts new file mode 100644 index 000000000000..cb5ddebcc3ae --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts @@ -0,0 +1,14 @@ +import { Controller, Get } from '@nestjs/common'; +import { EventsService } from './events.service'; + +@Controller('events') +export class EventsController { + constructor(private readonly eventsService: EventsService) {} + + @Get('emit') + async emitEvents() { + await this.eventsService.emitEvents(); + + return { message: 'Events emitted' }; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.module.ts new file mode 100644 index 000000000000..b92995e323eb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { APP_FILTER } from '@nestjs/core'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup'; +import { EventsController } from './events.controller'; +import { EventsService } from './events.service'; +import { TestEventListener } from './listeners/test-event.listener'; + +@Module({ + imports: [SentryModule.forRoot(), EventEmitterModule.forRoot()], + controllers: [EventsController], + providers: [ + { + provide: APP_FILTER, + useClass: SentryGlobalFilter, + }, + EventsService, + TestEventListener, + ], +}) +export class EventsModule {} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts new file mode 100644 index 000000000000..4a9f36ddaf5c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +@Injectable() +export class EventsService { + constructor(private readonly eventEmitter: EventEmitter2) {} + + async emitEvents() { + await this.eventEmitter.emit('myEvent.pass', { data: 'test' }); + await this.eventEmitter.emit('myEvent.throw'); + + return { message: 'Events emitted' }; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts new file mode 100644 index 000000000000..c1a3237f1f0c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class TestEventListener { + @OnEvent('myEvent.pass') + async handlePassEvent(payload: any): Promise { + await new Promise(resolve => setTimeout(resolve, 100)); + } + + @OnEvent('myEvent.throw') + async handleThrowEvent(): Promise { + await new Promise(resolve => setTimeout(resolve, 100)); + throw new Error('Test error from event handler'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/main.ts index 5aad5748b244..a18877460852 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/main.ts @@ -3,11 +3,13 @@ import './instrument'; // Import other modules import { NestFactory } from '@nestjs/core'; +import { EventsModule } from './events.module'; import { TraceInitiatorModule } from './trace-initiator.module'; import { TraceReceiverModule } from './trace-receiver.module'; const TRACE_INITIATOR_PORT = 3030; const TRACE_RECEIVER_PORT = 3040; +const EVENTS_PORT = 3050; async function bootstrap() { const trace_initiator_app = await NestFactory.create(TraceInitiatorModule); @@ -15,6 +17,9 @@ async function bootstrap() { const trace_receiver_app = await NestFactory.create(TraceReceiverModule); await trace_receiver_app.listen(TRACE_RECEIVER_PORT); + + const events_app = await NestFactory.create(EventsModule); + await events_app.listen(EVENTS_PORT); } bootstrap(); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts new file mode 100644 index 000000000000..b09eabb38980 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Event emitter', async () => { + const eventErrorPromise = waitForError('nestjs-distributed-tracing', errorEvent => { + return errorEvent.exception.values[0].value === 'Test error from event handler'; + }); + const successEventTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { + return transactionEvent.transaction === 'event myEvent.pass'; + }); + + const eventsUrl = `http://localhost:3050/events/emit`; + await fetch(eventsUrl); + + const eventError = await eventErrorPromise; + const successEventTransaction = await successEventTransactionPromise; + + expect(eventError.exception).toEqual({ + values: [ + { + type: 'Error', + value: 'Test error from event handler', + stacktrace: expect.any(Object), + mechanism: expect.any(Object), + }, + ], + }); + + expect(successEventTransaction.contexts.trace).toEqual({ + parent_span_id: expect.any(String), + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.source': 'custom', + 'sentry.sample_rate': 1, + 'sentry.op': 'event.nestjs', + 'sentry.origin': 'auto.event.nestjs', + }, + origin: 'auto.event.nestjs', + op: 'event.nestjs', + status: 'ok', + }); +}); diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 9b7360a45706..76028b27e40f 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -50,8 +50,8 @@ "@sentry/utils": "8.38.0" }, "devDependencies": { - "@nestjs/common": "10.4.7", - "@nestjs/core": "10.4.7" + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", diff --git a/packages/node/src/integrations/tracing/nest/helpers.ts b/packages/node/src/integrations/tracing/nest/helpers.ts index cc83dda3855d..04dab67f65b0 100644 --- a/packages/node/src/integrations/tracing/nest/helpers.ts +++ b/packages/node/src/integrations/tracing/nest/helpers.ts @@ -36,6 +36,24 @@ export function getMiddlewareSpanOptions(target: InjectableTarget | CatchTarget, }; } +/** + * Returns span options for nest event spans. + */ +export function getEventSpanOptions(event: string): { + name: string; + attributes: Record; + forceTransaction: boolean; +} { + return { + name: `event ${event}`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'event.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.event.nestjs', + }, + forceTransaction: true, + }; +} + /** * Adds instrumentation to a js observable and attaches the span to an active parent span. */ diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts index 4f8d88fa8f86..2520367d1361 100644 --- a/packages/node/src/integrations/tracing/nest/nest.ts +++ b/packages/node/src/integrations/tracing/nest/nest.ts @@ -12,6 +12,7 @@ import { import type { IntegrationFn, Span } from '@sentry/types'; import { logger } from '@sentry/utils'; import { generateInstrumentOnce } from '../../../otel/instrument'; +import { SentryNestEventInstrumentation } from './sentry-nest-event-instrumentation'; import { SentryNestInstrumentation } from './sentry-nest-instrumentation'; import type { MinimalNestJsApp, NestJsErrorFilter } from './types'; @@ -25,10 +26,15 @@ const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => { return new SentryNestInstrumentation(); }); +const instrumentNestEvent = generateInstrumentOnce('Nest-Event', () => { + return new SentryNestEventInstrumentation(); +}); + export const instrumentNest = Object.assign( (): void => { instrumentNestCore(); instrumentNestCommon(); + instrumentNestEvent(); }, { id: INTEGRATION_NAME }, ); diff --git a/packages/node/src/integrations/tracing/nest/sentry-nest-event-instrumentation.ts b/packages/node/src/integrations/tracing/nest/sentry-nest-event-instrumentation.ts new file mode 100644 index 000000000000..16333c7fc6c3 --- /dev/null +++ b/packages/node/src/integrations/tracing/nest/sentry-nest-event-instrumentation.ts @@ -0,0 +1,119 @@ +import { isWrapped } from '@opentelemetry/core'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, +} from '@opentelemetry/instrumentation'; +import { captureException, startSpan } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/utils'; +import { getEventSpanOptions } from './helpers'; +import type { OnEventTarget } from './types'; + +const supportedVersions = ['>=2.0.0']; + +/** + * Custom instrumentation for nestjs event-emitter + * + * This hooks into the `OnEvent` decorator, which is applied on event handlers. + */ +export class SentryNestEventInstrumentation extends InstrumentationBase { + public static readonly COMPONENT = '@nestjs/event-emitter'; + public static readonly COMMON_ATTRIBUTES = { + component: SentryNestEventInstrumentation.COMPONENT, + }; + + public constructor(config: InstrumentationConfig = {}) { + super('sentry-nestjs-event', SDK_VERSION, config); + } + + /** + * Initializes the instrumentation by defining the modules to be patched. + */ + public init(): InstrumentationNodeModuleDefinition { + const moduleDef = new InstrumentationNodeModuleDefinition( + SentryNestEventInstrumentation.COMPONENT, + supportedVersions, + ); + + moduleDef.files.push(this._getOnEventFileInstrumentation(supportedVersions)); + return moduleDef; + } + + /** + * Wraps the @OnEvent decorator. + */ + private _getOnEventFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { + return new InstrumentationNodeModuleFile( + '@nestjs/event-emitter/dist/decorators/on-event.decorator.js', + versions, + (moduleExports: { OnEvent: OnEventTarget }) => { + if (isWrapped(moduleExports.OnEvent)) { + this._unwrap(moduleExports, 'OnEvent'); + } + this._wrap(moduleExports, 'OnEvent', this._createWrapOnEvent()); + return moduleExports; + }, + (moduleExports: { OnEvent: OnEventTarget }) => { + this._unwrap(moduleExports, 'OnEvent'); + }, + ); + } + + /** + * Creates a wrapper function for the @OnEvent decorator. + */ + private _createWrapOnEvent() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function wrapOnEvent(original: any) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function wrappedOnEvent(event: any, options?: any) { + const eventName = Array.isArray(event) + ? event.join(',') + : typeof event === 'string' || typeof event === 'symbol' + ? event.toString() + : ''; + + // Get the original decorator result + const decoratorResult = original(event, options); + + // Return a new decorator function that wraps the handler + return function (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) { + if (!descriptor.value || typeof descriptor.value !== 'function' || target.__SENTRY_INTERNAL__) { + return decoratorResult(target, propertyKey, descriptor); + } + + // Get the original handler + const originalHandler = descriptor.value; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const handlerName = originalHandler.name || propertyKey; + + // Instrument the handler + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = async function (...args: any[]) { + return startSpan(getEventSpanOptions(eventName), async () => { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const result = await originalHandler.apply(this, args); + return result; + } catch (error) { + // exceptions from event handlers are not caught by global error filter + captureException(error); + throw error; + } + }); + }; + + // Preserve the original function name + Object.defineProperty(descriptor.value, 'name', { + value: handlerName, + configurable: true, + }); + + // Apply the original decorator + return decoratorResult(target, propertyKey, descriptor); + }; + }; + }; + } +} diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts index 0590462c09d5..ed7e968a9600 100644 --- a/packages/node/src/integrations/tracing/nest/types.ts +++ b/packages/node/src/integrations/tracing/nest/types.ts @@ -74,6 +74,15 @@ export interface CatchTarget { }; } +/** + * Represents a target method in NestJS annotated with @OnEvent. + */ +export interface OnEventTarget { + name: string; + sentryPatched?: boolean; + __SENTRY_INTERNAL__?: boolean; +} + /** * Represents an express NextFunction. */ diff --git a/packages/node/test/integrations/tracing/nest.test.ts b/packages/node/test/integrations/tracing/nest.test.ts index 3837e3e4ee3d..7f592a93f341 100644 --- a/packages/node/test/integrations/tracing/nest.test.ts +++ b/packages/node/test/integrations/tracing/nest.test.ts @@ -1,5 +1,8 @@ +import * as core from '@sentry/core'; import { isPatched } from '../../../src/integrations/tracing/nest/helpers'; +import { SentryNestEventInstrumentation } from '../../../src/integrations/tracing/nest/sentry-nest-event-instrumentation'; import type { InjectableTarget } from '../../../src/integrations/tracing/nest/types'; +import type { OnEventTarget } from '../../../src/integrations/tracing/nest/types'; describe('Nest', () => { describe('isPatched', () => { @@ -14,4 +17,99 @@ describe('Nest', () => { expect(target.sentryPatched).toBe(true); }); }); + + describe('EventInstrumentation', () => { + let instrumentation: SentryNestEventInstrumentation; + let mockOnEvent: jest.Mock; + let mockTarget: OnEventTarget; + + beforeEach(() => { + instrumentation = new SentryNestEventInstrumentation(); + // Mock OnEvent to return a function that applies the descriptor + mockOnEvent = jest.fn().mockImplementation(() => { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + return descriptor; + }; + }); + mockTarget = { + name: 'TestClass', + prototype: {}, + } as OnEventTarget; + jest.spyOn(core, 'startSpan'); + jest.spyOn(core, 'captureException'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('init()', () => { + it('should return module definition with correct component name', () => { + const moduleDef = instrumentation.init(); + expect(moduleDef.name).toBe('@nestjs/event-emitter'); + }); + }); + + describe('OnEvent decorator wrapping', () => { + let wrappedOnEvent: any; + let descriptor: PropertyDescriptor; + let originalHandler: jest.Mock; + + beforeEach(() => { + originalHandler = jest.fn().mockResolvedValue('result'); + descriptor = { + value: originalHandler, + }; + + const moduleDef = instrumentation.init(); + const onEventFile = moduleDef.files[0]; + const moduleExports = { OnEvent: mockOnEvent }; + onEventFile?.patch(moduleExports); + wrappedOnEvent = moduleExports.OnEvent; + }); + + it('should wrap string event handlers', async () => { + const decorated = wrappedOnEvent('test.event'); + decorated(mockTarget, 'testMethod', descriptor); + + await descriptor.value(); + + expect(core.startSpan).toHaveBeenCalled(); + expect(originalHandler).toHaveBeenCalled(); + }); + + it('should wrap array event handlers', async () => { + const decorated = wrappedOnEvent(['test.event1', 'test.event2']); + decorated(mockTarget, 'testMethod', descriptor); + + await descriptor.value(); + + expect(core.startSpan).toHaveBeenCalled(); + expect(originalHandler).toHaveBeenCalled(); + }); + + it('should capture exceptions and rethrow', async () => { + const error = new Error('Test error'); + originalHandler.mockRejectedValue(error); + + const decorated = wrappedOnEvent('test.event'); + decorated(mockTarget, 'testMethod', descriptor); + + await expect(descriptor.value()).rejects.toThrow(error); + expect(core.captureException).toHaveBeenCalledWith(error); + }); + + it('should skip wrapping for internal Sentry handlers', () => { + const internalTarget = { + ...mockTarget, + __SENTRY_INTERNAL__: true, + }; + + const decorated = wrappedOnEvent('test.event'); + decorated(internalTarget, 'testMethod', descriptor); + + expect(descriptor.value).toBe(originalHandler); + }); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index ab60214fdc7a..dad8117d7ad2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6632,7 +6632,7 @@ iterare "1.2.1" tslib "2.7.0" -"@nestjs/common@10.4.7": +"@nestjs/common@^8.0.0 || ^9.0.0 || ^10.0.0": version "10.4.7" resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.7.tgz#076cb77c06149805cb1e193d8cdc69bbe8446c75" integrity sha512-gIOpjD3Mx8gfYGxYm/RHPcJzqdknNNFCyY+AxzBT3gc5Xvvik1Dn5OxaMGw5EbVfhZgJKVP0n83giUOAlZQe7w== @@ -6653,7 +6653,7 @@ path-to-regexp "3.3.0" tslib "2.7.0" -"@nestjs/core@10.4.7": +"@nestjs/core@^8.0.0 || ^9.0.0 || ^10.0.0": version "10.4.7" resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.7.tgz#adb27067a8c40b79f0713b417457fdfc6cf3406a" integrity sha512-AIpQzW/vGGqSLkKvll1R7uaSNv99AxZI2EFyVJPNGDgFsfXaohfV1Ukl6f+s75Km+6Fj/7aNl80EqzNWQCS8Ig== @@ -6665,6 +6665,13 @@ path-to-regexp "3.3.0" tslib "2.7.0" +"@nestjs/event-emitter@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@nestjs/event-emitter/-/event-emitter-2.1.1.tgz#4e34edc487c507edbe6d02033e3dd014a19210f9" + integrity sha512-6L6fBOZTyfFlL7Ih/JDdqlCzZeCW0RjCX28wnzGyg/ncv5F/EOeT1dfopQr1loBRQ3LTgu8OWM7n4zLN4xigsg== + dependencies: + eventemitter2 "6.4.9" + "@nestjs/platform-express@10.4.6": version "10.4.6" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.6.tgz#6c39c522fa66036b4256714fea203fbeb49fc4de" @@ -17958,6 +17965,11 @@ eventemitter-asyncresource@^1.0.0: resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b" integrity sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ== +eventemitter2@6.4.9: + version "6.4.9" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" + integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== + eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" From d41a04de153f68e793429e23c4a8485895b40fb7 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 15 Nov 2024 13:56:21 +0100 Subject: [PATCH 15/29] meta: Remove `beta` label from NestJS SDK readme (#14319) Ref https://github.com/getsentry/sentry-javascript/issues/14242 The NestJS SDK is stable enough for us to remove this label. All the breaking changes we do to the SDK are gonna end up in v9. --- packages/nestjs/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/nestjs/README.md b/packages/nestjs/README.md index 0cdb832a75f6..b802c6fbafe1 100644 --- a/packages/nestjs/README.md +++ b/packages/nestjs/README.md @@ -10,9 +10,6 @@ [![npm dm](https://img.shields.io/npm/dm/@sentry/nestjs.svg)](https://www.npmjs.com/package/@sentry/nestjs) [![npm dt](https://img.shields.io/npm/dt/@sentry/nestjs.svg)](https://www.npmjs.com/package/@sentry/nestjs) -This SDK is in **Beta**. The API is stable but updates may include minor changes in behavior. Please reach out on -[GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback or concerns. - ## Installation ```bash From 72751dacb88c5b970d8bac15052ee8e09b28fd5d Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 15 Nov 2024 14:53:52 +0100 Subject: [PATCH 16/29] feat(nestjs): Handle GraphQL contexts in `SentryGlobalFilter` (#14320) --- packages/nestjs/src/setup.ts | 56 +++++++++++++++--------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index a18f95417f11..0d7ccdb2c4dc 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -67,21 +67,39 @@ export { SentryTracingInterceptor }; */ class SentryGlobalFilter extends BaseExceptionFilter { public readonly __SENTRY_INTERNAL__: boolean; + private readonly _logger: Logger; public constructor(applicationRef?: HttpServer) { super(applicationRef); this.__SENTRY_INTERNAL__ = true; + this._logger = new Logger('ExceptionsHandler'); } /** * Catches exceptions and reports them to Sentry unless they are expected errors. */ public catch(exception: unknown, host: ArgumentsHost): void { - if (isExpectedError(exception)) { - return super.catch(exception, host); + // The BaseExceptionFilter does not work well in GraphQL applications. + // By default, Nest GraphQL applications use the ExternalExceptionFilter, which just rethrows the error: + // https://github.com/nestjs/nest/blob/master/packages/core/exceptions/external-exception-filter.ts + if (host.getType<'graphql'>() === 'graphql') { + // neither report nor log HttpExceptions + if (exception instanceof HttpException) { + throw exception; + } + + if (exception instanceof Error) { + this._logger.error(exception.message, exception.stack); + } + + captureException(exception); + throw exception; + } + + if (!isExpectedError(exception)) { + captureException(exception); } - captureException(exception); return super.catch(exception, host); } } @@ -89,13 +107,7 @@ Catch()(SentryGlobalFilter); export { SentryGlobalFilter }; /** - * Global filter to handle exceptions and report them to Sentry. - * - * The BaseExceptionFilter does not work well in GraphQL applications. - * By default, Nest GraphQL applications use the ExternalExceptionFilter, which just rethrows the error: - * https://github.com/nestjs/nest/blob/master/packages/core/exceptions/external-exception-filter.ts - * - * The ExternalExceptinFilter is not exported, so we reimplement this filter here. + * Global filter to handle exceptions in NestJS + GraphQL applications and report them to Sentry. */ class SentryGlobalGraphQLFilter { private static readonly _logger = new Logger('ExceptionsHandler'); @@ -129,29 +141,7 @@ export { SentryGlobalGraphQLFilter }; * * This filter is a generic filter that can handle both HTTP and GraphQL exceptions. */ -class SentryGlobalGenericFilter extends SentryGlobalFilter { - public readonly __SENTRY_INTERNAL__: boolean; - private readonly _graphqlFilter: SentryGlobalGraphQLFilter; - - public constructor(applicationRef?: HttpServer) { - super(applicationRef); - this.__SENTRY_INTERNAL__ = true; - this._graphqlFilter = new SentryGlobalGraphQLFilter(); - } - - /** - * Catches exceptions and forwards them to the according error filter. - */ - public catch(exception: unknown, host: ArgumentsHost): void { - if (host.getType<'graphql'>() === 'graphql') { - return this._graphqlFilter.catch(exception, host); - } - - super.catch(exception, host); - } -} -Catch()(SentryGlobalGenericFilter); -export { SentryGlobalGenericFilter }; +export const SentryGlobalGenericFilter = SentryGlobalFilter; /** * Service to set up Sentry performance tracing for Nest.js applications. From a374baf0bbf9b87c67615ffcf5e9354eab508d57 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Nov 2024 09:33:07 +0100 Subject: [PATCH 17/29] test(browser): Update tests that need changes for `getLocalTestUrl` (#14324) Extracted these out of https://github.com/getsentry/sentry-javascript/pull/11904 - these are all the tests that required changing any (test) code for them to pass with `getLocalTestUrl` vs `getLocalTestPath`. --- .../captureException/errorEvent/subject.js | 6 +-- .../captureException/errorEvent/test.ts | 6 +-- .../xhr/onreadystatechange/subject.js | 2 +- .../xhr/onreadystatechange/test.ts | 26 +++++++--- .../suites/replay/captureReplay/test.ts | 12 ++--- .../captureReplayFromReplayPackage/test.ts | 12 ++--- .../suites/replay/multiple-pages/test.ts | 47 +++++++++++------ .../test.ts-snapshots/seg-0-snap-full | 4 +- .../seg-0-snap-full-chromium | 2 +- .../test.ts-snapshots/seg-2-snap-full | 4 +- .../seg-2-snap-full-chromium | 2 +- .../test.ts-snapshots/seg-4-snap-full | 4 +- .../seg-4-snap-full-chromium | 2 +- .../test.ts-snapshots/seg-8-snap-full | 4 +- .../seg-8-snap-full-chromium | 2 +- .../test.ts | 10 ++-- .../protocol_fn_identifiers/test.ts | 10 ++-- .../regular_fn_identifiers/test.ts | 10 ++-- .../tracing/dsc-txn-name-update/test.ts | 2 +- .../metrics/pageload-resource-spans/test.ts | 50 ++++++++++++------- .../utils/replayEventTemplates.ts | 2 +- 21 files changed, 128 insertions(+), 91 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/subject.js index 207f9d1d58f6..3e9014dabf47 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/subject.js @@ -1,5 +1 @@ -window.addEventListener('error', function (event) { - Sentry.captureException(event); -}); - -window.thisDoesNotExist(); +Sentry.captureException(new ErrorEvent('something', { message: 'test error' })); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/test.ts index 9c09ba374e78..5e8cbc0dd9c1 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/test.ts @@ -4,15 +4,15 @@ import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; -sentryTest('should capture an ErrorEvent', async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); +sentryTest('should capture an ErrorEvent', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.exception?.values).toHaveLength(1); expect(eventData.exception?.values?.[0]).toMatchObject({ type: 'ErrorEvent', - value: 'Event `ErrorEvent` captured as exception with message `Script error.`', + value: 'Event `ErrorEvent` captured as exception with message `test error`', mechanism: { type: 'generic', handled: true, diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/subject.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/subject.js index f88672f09214..a51740976b6a 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/subject.js @@ -1,6 +1,6 @@ window.calls = {}; const xhr = new XMLHttpRequest(); -xhr.open('GET', 'test'); +xhr.open('GET', 'http://example.com'); xhr.onreadystatechange = function wat() { window.calls[xhr.readyState] = window.calls[xhr.readyState] ? window.calls[xhr.readyState] + 1 : 1; }; diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/test.ts b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/test.ts index faec510f8f47..f9b1816c6f2d 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/test.ts @@ -4,16 +4,28 @@ import { sentryTest } from '../../../../../utils/fixtures'; sentryTest( 'should not call XMLHttpRequest onreadystatechange more than once per state', - async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('http://example.com/', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); await page.goto(url); - const calls = await page.evaluate(() => { - // @ts-expect-error window.calls defined in subject.js - return window.calls; - }); + // Wait until XHR is done + await page.waitForFunction('window.calls["4"]'); - expect(calls).toEqual({ '4': 1 }); + const calls = await page.evaluate('window.calls'); + + expect(calls).toEqual({ + '2': 1, + '3': 1, + '4': 1, + }); }, ); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts index b2cd4196643b..85353801980d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts @@ -1,10 +1,10 @@ import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/browser'; -import { sentryTest } from '../../../utils/fixtures'; +import { TEST_HOST, sentryTest } from '../../../utils/fixtures'; import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; -sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalTestPath, page }) => { +sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalTestUrl, page }) => { if (shouldSkipReplayTest()) { sentryTest.skip(); } @@ -12,7 +12,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); const replayEvent0 = getReplayEvent(await reqPromise0); @@ -26,7 +26,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT timestamp: expect.any(Number), error_ids: [], trace_ids: [], - urls: [expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/)], + urls: [`${TEST_HOST}/index.html`], replay_id: expect.stringMatching(/\w{32}/), replay_start_timestamp: expect.any(Number), segment_id: 0, @@ -49,7 +49,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT name: 'sentry.javascript.browser', }, request: { - url: expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/), + url: `${TEST_HOST}/index.html`, headers: { 'User-Agent': expect.stringContaining(''), }, @@ -86,7 +86,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT name: 'sentry.javascript.browser', }, request: { - url: expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/), + url: `${TEST_HOST}/index.html`, headers: { 'User-Agent': expect.stringContaining(''), }, diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts index e9db4c92343c..82bbe104ab98 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts @@ -1,10 +1,10 @@ import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/browser'; -import { sentryTest } from '../../../utils/fixtures'; +import { TEST_HOST, sentryTest } from '../../../utils/fixtures'; import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; -sentryTest('should capture replays (@sentry-internal/replay export)', async ({ getLocalTestPath, page }) => { +sentryTest('should capture replays (@sentry-internal/replay export)', async ({ getLocalTestUrl, page }) => { if (shouldSkipReplayTest()) { sentryTest.skip(); } @@ -12,7 +12,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); const replayEvent0 = getReplayEvent(await reqPromise0); @@ -26,7 +26,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g timestamp: expect.any(Number), error_ids: [], trace_ids: [], - urls: [expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/)], + urls: [`${TEST_HOST}/index.html`], replay_id: expect.stringMatching(/\w{32}/), replay_start_timestamp: expect.any(Number), segment_id: 0, @@ -49,7 +49,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g name: 'sentry.javascript.browser', }, request: { - url: expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/), + url: `${TEST_HOST}/index.html`, headers: { 'User-Agent': expect.stringContaining(''), }, @@ -86,7 +86,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g name: 'sentry.javascript.browser', }, request: { - url: expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/), + url: `${TEST_HOST}/index.html`, headers: { 'User-Agent': expect.stringContaining(''), }, diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts index 3ee84086cc37..4f1ba066f5e4 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; +import { TEST_HOST, sentryTest } from '../../../utils/fixtures'; import { expectedCLSPerformanceSpan, expectedClickBreadcrumb, @@ -30,7 +30,7 @@ well as the correct DOM snapshots and updates are recorded and sent. */ sentryTest( 'record page navigations and performance entries across multiple pages', - async ({ getLocalTestPath, page, browserName }) => { + async ({ getLocalTestUrl, page, browserName }) => { // We only test this against the NPM package and replay bundles // and only on chromium as most performance entries are only available in chromium if (shouldSkipReplayTest() || browserName !== 'chromium') { @@ -48,7 +48,7 @@ sentryTest( const reqPromise8 = waitForReplayRequest(page, 8); const reqPromise9 = waitForReplayRequest(page, 9); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); const [req0] = await Promise.all([reqPromise0, page.goto(url)]); const replayEvent0 = getReplayEvent(req0); @@ -72,7 +72,7 @@ sentryTest( const collectedPerformanceSpans = [...recording0.performanceSpans, ...recording1.performanceSpans]; const collectedBreadcrumbs = [...recording0.breadcrumbs, ...recording1.breadcrumbs]; - expect(collectedPerformanceSpans.length).toEqual(8); + expect(collectedPerformanceSpans.length).toBeGreaterThanOrEqual(6); expect(collectedPerformanceSpans).toEqual( expect.arrayContaining([ expectedNavigationPerformanceSpan, @@ -112,7 +112,7 @@ sentryTest( const collectedPerformanceSpansAfterReload = [...recording2.performanceSpans, ...recording3.performanceSpans]; const collectedBreadcrumbsAdterReload = [...recording2.breadcrumbs, ...recording3.breadcrumbs]; - expect(collectedPerformanceSpansAfterReload.length).toEqual(8); + expect(collectedPerformanceSpansAfterReload.length).toBeGreaterThanOrEqual(6); expect(collectedPerformanceSpansAfterReload).toEqual( expect.arrayContaining([ expectedReloadPerformanceSpan, @@ -146,7 +146,8 @@ sentryTest( url: expect.stringContaining('page-0.html'), headers: { // @ts-expect-error this is fine - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/index.html`, }, }, }), @@ -168,7 +169,8 @@ sentryTest( url: expect.stringContaining('page-0.html'), headers: { // @ts-expect-error this is fine - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/index.html`, }, }, }), @@ -210,13 +212,12 @@ sentryTest( getExpectedReplayEvent({ segment_id: 6, urls: ['/spa'], - request: { - // @ts-expect-error this is fine - url: expect.stringContaining('page-0.html'), + url: `${TEST_HOST}/spa`, headers: { // @ts-expect-error this is fine - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/index.html`, }, }, }), @@ -235,11 +236,11 @@ sentryTest( urls: [], request: { - // @ts-expect-error this is fine - url: expect.stringContaining('page-0.html'), + url: `${TEST_HOST}/spa`, headers: { // @ts-expect-error this is fine - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/index.html`, }, }, }), @@ -279,6 +280,14 @@ sentryTest( expect(replayEvent8).toEqual( getExpectedReplayEvent({ segment_id: 8, + request: { + url: `${TEST_HOST}/index.html`, + headers: { + // @ts-expect-error this is fine + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/spa`, + }, + }, }), ); expect(normalize(recording8.fullSnapshots)).toMatchSnapshot('seg-8-snap-full'); @@ -293,6 +302,14 @@ sentryTest( getExpectedReplayEvent({ segment_id: 9, urls: [], + request: { + url: `${TEST_HOST}/index.html`, + headers: { + // @ts-expect-error this is fine + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/spa`, + }, + }, }), ); expect(recording9.fullSnapshots.length).toEqual(0); @@ -304,7 +321,7 @@ sentryTest( ]; const collectedBreadcrumbsAfterIndexNavigation = [...recording8.breadcrumbs, ...recording9.breadcrumbs]; - expect(collectedPerformanceSpansAfterIndexNavigation.length).toEqual(8); + expect(collectedPerformanceSpansAfterIndexNavigation.length).toBeGreaterThanOrEqual(6); expect(collectedPerformanceSpansAfterIndexNavigation).toEqual( expect.arrayContaining([ expectedNavigationPerformanceSpan, diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full index fdccbb1b9387..0d77b67cb862 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { @@ -110,4 +110,4 @@ }, "timestamp": [timestamp] } -] \ No newline at end of file +] diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full-chromium index fdccbb1b9387..40b05fcbe191 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full-chromium +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full-chromium @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full index fdccbb1b9387..0d77b67cb862 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { @@ -110,4 +110,4 @@ }, "timestamp": [timestamp] } -] \ No newline at end of file +] diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full-chromium index fdccbb1b9387..40b05fcbe191 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full-chromium +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full-chromium @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full index b0aeb348b388..1c3d1f22aeba 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full @@ -116,7 +116,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/index.html" + "href": "http://sentry-test.io/index.html" }, "childNodes": [ { @@ -153,4 +153,4 @@ }, "timestamp": [timestamp] } -] \ No newline at end of file +] diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full-chromium index b0aeb348b388..2e7bfb9bd2d2 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full-chromium +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full-chromium @@ -116,7 +116,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/index.html" + "href": "http://sentry-test.io/index.html" }, "childNodes": [ { diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full index fdccbb1b9387..0d77b67cb862 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { @@ -110,4 +110,4 @@ }, "timestamp": [timestamp] } -] \ No newline at end of file +] diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full-chromium index fdccbb1b9387..40b05fcbe191 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full-chromium +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full-chromium @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { diff --git a/dev-packages/browser-integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts b/dev-packages/browser-integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts index 4650c6b94f3c..884dea9c618c 100644 --- a/dev-packages/browser-integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts +++ b/dev-packages/browser-integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts @@ -6,8 +6,8 @@ import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; sentryTest( 'should parse function identifiers that contain protocol names correctly @firefox', - async ({ getLocalTestPath, page, runInChromium, runInFirefox, runInWebkit }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page, runInChromium, runInFirefox, runInWebkit }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); const frames = eventData.exception?.values?.[0].stacktrace?.frames; @@ -52,14 +52,14 @@ sentryTest( sentryTest( 'should not add any part of the function identifier to beginning of filename', - async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.exception?.values?.[0].stacktrace?.frames).toMatchObject( // specifically, we're trying to avoid values like `Blob@file://path/to/file` in frames with function names like `makeBlob` - Array(7).fill({ filename: expect.stringMatching(/^file:\/?/) }), + Array(7).fill({ filename: expect.stringMatching(/^http:\/?/) }), ); }, ); diff --git a/dev-packages/browser-integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts b/dev-packages/browser-integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts index c0c813058128..a78c15963814 100644 --- a/dev-packages/browser-integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts +++ b/dev-packages/browser-integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts @@ -6,8 +6,8 @@ import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; sentryTest( 'should parse function identifiers that are protocol names correctly @firefox', - async ({ getLocalTestPath, page, runInChromium, runInFirefox, runInWebkit }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page, runInChromium, runInFirefox, runInWebkit }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); const frames = eventData.exception?.values?.[0].stacktrace?.frames; @@ -56,8 +56,8 @@ sentryTest( sentryTest( 'should not add any part of the function identifier to beginning of filename', - async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); @@ -65,7 +65,7 @@ sentryTest( expect(eventData.exception?.values).toBeDefined(); expect(eventData.exception?.values?.[0].stacktrace).toBeDefined(); expect(eventData.exception?.values?.[0].stacktrace?.frames).toMatchObject( - Array(7).fill({ filename: expect.stringMatching(/^file:\/?/) }), + Array(7).fill({ filename: expect.stringMatching(/^http:\/?/) }), ); }, ); diff --git a/dev-packages/browser-integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts b/dev-packages/browser-integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts index 6ba6e15e6bd2..7585a21521e0 100644 --- a/dev-packages/browser-integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts +++ b/dev-packages/browser-integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts @@ -6,8 +6,8 @@ import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; sentryTest( 'should parse function identifiers correctly @firefox', - async ({ getLocalTestPath, page, runInChromium, runInFirefox, runInWebkit }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page, runInChromium, runInFirefox, runInWebkit }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); const frames = eventData.exception?.values?.[0].stacktrace?.frames; @@ -56,14 +56,14 @@ sentryTest( sentryTest( 'should not add any part of the function identifier to beginning of filename', - async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.exception?.values?.[0].stacktrace?.frames).toMatchObject( // specifically, we're trying to avoid values like `Blob@file://path/to/file` in frames with function names like `makeBlob` - Array(8).fill({ filename: expect.stringMatching(/^file:\/?/) }), + Array(8).fill({ filename: expect.stringMatching(/^http:\/?/) }), ); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts index e8c21a66647f..41003a133b34 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts @@ -170,7 +170,7 @@ async function makeRequestAndGetBaggageItems(page: Page): Promise { return baggage?.split(',').sort() ?? []; } -async function captureErrorAndGetEnvelopeTraceHeader(page: Page): Promise { +async function captureErrorAndGetEnvelopeTraceHeader(page: Page): Promise | undefined> { const errorEventPromise = getMultipleSentryEnvelopeRequests( page, 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index fc74fa685bc7..152cadf80418 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -5,35 +5,47 @@ import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestPath, page, browser }) => { +sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). - await page.route('**/path/to/image.svg', (route: Route) => route.fulfill({ path: `${__dirname}/assets/image.svg` })); - await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); - await page.route('**/path/to/style.css', (route: Route) => route.fulfill({ path: `${__dirname}/assets/style.css` })); + await page.route('https://example.com/path/to/image.svg', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/image.svg` }), + ); + await page.route('https://example.com/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + await page.route('https://example.com/path/to/style.css', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/style.css` }), + ); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource')); - // Webkit 16.0 (which is linked to Playwright 1.27.1) consistently creates 2 consecutive spans for `css`, - // so we need to check for 3 or 4 spans. - if (browser.browserType().name() === 'webkit') { - expect(resourceSpans?.length).toBeGreaterThanOrEqual(3); - } else { - expect(resourceSpans?.length).toBe(3); + const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script'); + const linkSpans = resourceSpans?.filter(({ op }) => op === 'resource.link'); + const imgSpans = resourceSpans?.filter(({ op }) => op === 'resource.img'); + + expect(imgSpans).toHaveLength(1); + expect(linkSpans).toHaveLength(1); + + const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle'); + + const expectedScripts = ['/init.bundle.js', '/subject.bundle.js', 'https://example.com/path/to/script.js']; + if (hasCdnBundle) { + expectedScripts.unshift('/cdn.bundle.js'); } - ['resource.img', 'resource.script', 'resource.link'].forEach(op => - expect(resourceSpans).toContainEqual( - expect.objectContaining({ - op: op, - parent_span_id: eventData.contexts?.trace?.span_id, - }), - ), - ); + expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts); + + const spanId = eventData.contexts?.trace?.span_id; + + expect(spanId).toBeDefined(); + expect(imgSpans?.[0].parent_span_id).toBe(spanId); + expect(linkSpans?.[0].parent_span_id).toBe(spanId); + expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId)); }); diff --git a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts index 51760544d868..40e303e9b0cb 100644 --- a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts +++ b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts @@ -33,7 +33,7 @@ const DEFAULT_REPLAY_EVENT = { request: { url: expect.stringContaining('/index.html'), headers: { - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), }, }, platform: 'javascript', From ef83bfc817aba0a113bb7d677635629d50c75c95 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Nov 2024 09:33:50 +0100 Subject: [PATCH 18/29] ref(types): Deprecate `Request` type in favor of `RequestEventData` (#14317) The type `Request` is very misleading and overloaded, so let's rename it to something clearer. I opted with `RequestEventData`, but happy to hear other ideas as well. Closes https://github.com/getsentry/sentry-javascript/issues/14300 --- docs/migration/draft-v9-migration-guide.md | 4 ++++ packages/browser/src/exports.ts | 2 ++ packages/bun/src/index.ts | 2 ++ packages/cloudflare/src/index.ts | 2 ++ packages/deno/src/index.ts | 2 ++ packages/node/src/index.ts | 2 ++ .../integrations/http/SentryHttpInstrumentation.ts | 6 +++--- packages/types/src/event.ts | 6 +++--- packages/types/src/index.ts | 8 +++++++- packages/types/src/request.ts | 9 +++++++-- packages/utils/src/requestdata.ts | 11 +++++++---- packages/vercel-edge/src/index.ts | 2 ++ 12 files changed, 43 insertions(+), 13 deletions(-) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 5630d82ede90..bbbb2c2d367f 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -11,3 +11,7 @@ ## `@sentry/core` - Deprecated `transactionNamingScheme` option in `requestDataIntegration`. + +## `@sentry/types`` + +- Deprecated `Request` in favor of `RequestEventData`. diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index fe5179f77661..ebea58f1fa7c 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -1,7 +1,9 @@ export type { Breadcrumb, BreadcrumbHint, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 5688d1007769..d8c97d6e8246 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -2,7 +2,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index fa0d76a54521..4115874aa5e5 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -2,7 +2,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index c7328c810f92..3531074793f3 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -2,7 +2,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 6ab536034894..88b105682e6d 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -144,7 +144,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 6b6fe8aaad40..b17810adb601 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -6,7 +6,7 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; import { getRequestInfo } from '@opentelemetry/instrumentation-http'; import { addBreadcrumb, getClient, getIsolationScope, withIsolationScope } from '@sentry/core'; -import type { PolymorphicRequest, Request, SanitizedRequestData } from '@sentry/types'; +import type { PolymorphicRequest, RequestEventData, SanitizedRequestData } from '@sentry/types'; import { getBreadcrumbLogLevelFromHttpStatusCode, getSanitizedUrlString, @@ -142,7 +142,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase; capturedSpanScope?: Scope; capturedSpanIsolationScope?: Scope; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b100c1e9c26a..5dd1839aeba7 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -87,7 +87,13 @@ export type { SendFeedbackParams, UserFeedback, } from './feedback'; -export type { QueryParams, Request, SanitizedRequestData } from './request'; +export type { + QueryParams, + RequestEventData, + // eslint-disable-next-line deprecation/deprecation + Request, + SanitizedRequestData, +} from './request'; export type { Runtime } from './runtime'; export type { CaptureContext, Scope, ScopeContext, ScopeData } from './scope'; export type { SdkInfo } from './sdkinfo'; diff --git a/packages/types/src/request.ts b/packages/types/src/request.ts index f0c2bdb268ca..6ba060219dfd 100644 --- a/packages/types/src/request.ts +++ b/packages/types/src/request.ts @@ -1,8 +1,7 @@ /** * Request data included in an event as sent to Sentry. - * TODO(v9): Rename this to avoid confusion, because Request is also a native type. */ -export interface Request { +export interface RequestEventData { url?: string; method?: string; data?: any; @@ -12,6 +11,12 @@ export interface Request { headers?: { [key: string]: string }; } +/** + * Request data included in an event as sent to Sentry. + * @deprecated: This type will be removed in v9. Use `RequestEventData` instead. + */ +export type Request = RequestEventData; + export type QueryParams = string | { [key: string]: string } | Array<[string, string]>; /** diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index 26b12b07c69e..13ec367addda 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -3,7 +3,7 @@ import type { Event, ExtractedNodeRequestData, PolymorphicRequest, - Request, + RequestEventData, TransactionSource, WebFetchHeaders, WebFetchRequest, @@ -260,7 +260,7 @@ export function extractRequestData( */ export function addNormalizedRequestDataToEvent( event: Event, - req: Request, + req: RequestEventData, // This is non-standard data that is not part of the regular HTTP request additionalData: { ipAddress?: string; user?: Record }, options: AddRequestDataToEventOptions, @@ -428,10 +428,13 @@ export function winterCGRequestToRequestData(req: WebFetchRequest): PolymorphicR }; } -function extractNormalizedRequestData(normalizedRequest: Request, { include }: { include: string[] }): Request { +function extractNormalizedRequestData( + normalizedRequest: RequestEventData, + { include }: { include: string[] }, +): RequestEventData { const includeKeys = include ? (Array.isArray(include) ? include : DEFAULT_REQUEST_INCLUDES) : []; - const requestData: Request = {}; + const requestData: RequestEventData = {}; const headers = { ...normalizedRequest.headers }; if (includeKeys.includes('headers')) { diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index e222d2de1ad1..4eea3f90d2d8 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -2,7 +2,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, From 43c70797dca71f8994e6f8a2b0a01a8dd90f6c4f Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 18 Nov 2024 09:41:32 +0100 Subject: [PATCH 19/29] feat(nestjs): Duplicate `SentryService` behaviour into `@sentry/nestjs` SDK `init()` (#14321) --- packages/nestjs/src/sdk.ts | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/nestjs/src/sdk.ts b/packages/nestjs/src/sdk.ts index 8d5ca21b1706..b4789e2d01c2 100644 --- a/packages/nestjs/src/sdk.ts +++ b/packages/nestjs/src/sdk.ts @@ -1,5 +1,10 @@ -import { applySdkMetadata } from '@sentry/core'; -import type { NodeClient, NodeOptions } from '@sentry/node'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + applySdkMetadata, + spanToJSON, +} from '@sentry/core'; +import type { NodeClient, NodeOptions, Span } from '@sentry/node'; import { init as nodeInit } from '@sentry/node'; /** @@ -12,5 +17,29 @@ export function init(options: NodeOptions | undefined = {}): NodeClient | undefi applySdkMetadata(opts, 'nestjs'); - return nodeInit(opts); + const client = nodeInit(opts); + + if (client) { + client.on('spanStart', span => { + // The NestInstrumentation has no requestHook, so we add NestJS-specific attributes here + addNestSpanAttributes(span); + }); + } + + return client; +} + +function addNestSpanAttributes(span: Span): void { + const attributes = spanToJSON(span).data || {}; + + // this is one of: app_creation, request_context, handler + const type = attributes['nestjs.type']; + + // Only set the NestJS attributes for spans that are created by the NestJS instrumentation and for spans that do not have an op already. + if (type && !attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]) { + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.nestjs`, + }); + } } From 538702e40cfd0fcdd87bf1390f34075747383245 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 18 Nov 2024 09:42:30 +0100 Subject: [PATCH 20/29] feat(nestjs): Add alias `@SentryExceptionCaptured` for `@WithSentry` (#14322) --- .../src/example-global.filter.ts | 4 +- packages/nestjs/README.md | 6 +- packages/nestjs/src/decorators.ts | 87 +++++++++++++++++++ packages/nestjs/src/decorators/sentry-cron.ts | 24 ----- .../nestjs/src/decorators/sentry-traced.ts | 34 -------- packages/nestjs/src/decorators/with-sentry.ts | 24 ----- packages/nestjs/src/index.ts | 9 +- 7 files changed, 98 insertions(+), 90 deletions(-) create mode 100644 packages/nestjs/src/decorators.ts delete mode 100644 packages/nestjs/src/decorators/sentry-cron.ts delete mode 100644 packages/nestjs/src/decorators/sentry-traced.ts delete mode 100644 packages/nestjs/src/decorators/with-sentry.ts diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/example-global.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/example-global.filter.ts index cee50d0d2c7c..a2afcff4dc1b 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/example-global.filter.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/example-global.filter.ts @@ -1,10 +1,10 @@ import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; -import { WithSentry } from '@sentry/nestjs'; +import { SentryExceptionCaptured } from '@sentry/nestjs'; import { Request, Response } from 'express'; @Catch() export class ExampleWrappedGlobalFilter implements ExceptionFilter { - @WithSentry() + @SentryExceptionCaptured() catch(exception: BadRequestException, host: ArgumentsHost): void { const ctx = host.switchToHttp(); const response = ctx.getResponse(); diff --git a/packages/nestjs/README.md b/packages/nestjs/README.md index b802c6fbafe1..749e3d4efd6c 100644 --- a/packages/nestjs/README.md +++ b/packages/nestjs/README.md @@ -69,8 +69,8 @@ export class AppModule {} In case you are using a global catch-all exception filter (which is either a filter registered with `app.useGlobalFilters()` or a filter registered in your app module providers annotated with an empty `@Catch()` -decorator), add a `@WithSentry()` decorator to the `catch()` method of this global error filter. This decorator will -report all unexpected errors that are received by your global error filter to Sentry: +decorator), add a `@SentryExceptionCaptured()` decorator to the `catch()` method of this global error filter. This +decorator will report all unexpected errors that are received by your global error filter to Sentry: ```typescript import { Catch, ExceptionFilter } from '@nestjs/common'; @@ -78,7 +78,7 @@ import { WithSentry } from '@sentry/nestjs'; @Catch() export class YourCatchAllExceptionFilter implements ExceptionFilter { - @WithSentry() + @SentryExceptionCaptured() catch(exception, host): void { // your implementation here } diff --git a/packages/nestjs/src/decorators.ts b/packages/nestjs/src/decorators.ts new file mode 100644 index 000000000000..60e1049b3fd2 --- /dev/null +++ b/packages/nestjs/src/decorators.ts @@ -0,0 +1,87 @@ +import { captureException } from '@sentry/core'; +import * as Sentry from '@sentry/node'; +import { startSpan } from '@sentry/node'; +import type { MonitorConfig } from '@sentry/types'; +import { isExpectedError } from './helpers'; + +/** + * A decorator wrapping the native nest Cron decorator, sending check-ins to Sentry. + */ +export const SentryCron = (monitorSlug: string, monitorConfig?: MonitorConfig): MethodDecorator => { + return (target: unknown, propertyKey, descriptor: PropertyDescriptor) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const originalMethod = descriptor.value as (...args: any[]) => Promise; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = function (...args: any[]) { + return Sentry.withMonitor( + monitorSlug, + () => { + return originalMethod.apply(this, args); + }, + monitorConfig, + ); + }; + return descriptor; + }; +}; + +/** + * A decorator usable to wrap arbitrary functions with spans. + */ +export function SentryTraced(op: string = 'function') { + return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const originalMethod = descriptor.value as (...args: any[]) => Promise | any; // function can be sync or async + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = function (...args: any[]) { + return startSpan( + { + op: op, + name: propertyKey, + }, + () => { + return originalMethod.apply(this, args); + }, + ); + }; + + // preserve the original name on the decorated function + Object.defineProperty(descriptor.value, 'name', { + value: originalMethod.name, + configurable: true, + enumerable: true, + writable: true, + }); + + return descriptor; + }; +} + +/** + * A decorator to wrap user-defined exception filters and add Sentry error reporting. + */ +export function SentryExceptionCaptured() { + return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const originalCatch = descriptor.value as (exception: unknown, host: unknown, ...args: any[]) => void; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = function (exception: unknown, host: unknown, ...args: any[]) { + if (isExpectedError(exception)) { + return originalCatch.apply(this, [exception, host, ...args]); + } + + captureException(exception); + return originalCatch.apply(this, [exception, host, ...args]); + }; + + return descriptor; + }; +} + +/** + * A decorator to wrap user-defined exception filters and add Sentry error reporting. + */ +export const WithSentry = SentryExceptionCaptured; diff --git a/packages/nestjs/src/decorators/sentry-cron.ts b/packages/nestjs/src/decorators/sentry-cron.ts deleted file mode 100644 index 8cb86c6d66cc..000000000000 --- a/packages/nestjs/src/decorators/sentry-cron.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Sentry from '@sentry/node'; -import type { MonitorConfig } from '@sentry/types'; - -/** - * A decorator wrapping the native nest Cron decorator, sending check-ins to Sentry. - */ -export const SentryCron = (monitorSlug: string, monitorConfig?: MonitorConfig): MethodDecorator => { - return (target: unknown, propertyKey, descriptor: PropertyDescriptor) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const originalMethod = descriptor.value as (...args: any[]) => Promise; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (...args: any[]) { - return Sentry.withMonitor( - monitorSlug, - () => { - return originalMethod.apply(this, args); - }, - monitorConfig, - ); - }; - return descriptor; - }; -}; diff --git a/packages/nestjs/src/decorators/sentry-traced.ts b/packages/nestjs/src/decorators/sentry-traced.ts deleted file mode 100644 index 2f90e4dab5d9..000000000000 --- a/packages/nestjs/src/decorators/sentry-traced.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { startSpan } from '@sentry/node'; - -/** - * A decorator usable to wrap arbitrary functions with spans. - */ -export function SentryTraced(op: string = 'function') { - return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const originalMethod = descriptor.value as (...args: any[]) => Promise | any; // function can be sync or async - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (...args: any[]) { - return startSpan( - { - op: op, - name: propertyKey, - }, - () => { - return originalMethod.apply(this, args); - }, - ); - }; - - // preserve the original name on the decorated function - Object.defineProperty(descriptor.value, 'name', { - value: originalMethod.name, - configurable: true, - enumerable: true, - writable: true, - }); - - return descriptor; - }; -} diff --git a/packages/nestjs/src/decorators/with-sentry.ts b/packages/nestjs/src/decorators/with-sentry.ts deleted file mode 100644 index cf86ea6e7cc5..000000000000 --- a/packages/nestjs/src/decorators/with-sentry.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { captureException } from '@sentry/core'; -import { isExpectedError } from '../helpers'; - -/** - * A decorator to wrap user-defined exception filters and add Sentry error reporting. - */ -export function WithSentry() { - return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const originalCatch = descriptor.value as (exception: unknown, host: unknown, ...args: any[]) => void; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (exception: unknown, host: unknown, ...args: any[]) { - if (isExpectedError(exception)) { - return originalCatch.apply(this, [exception, host, ...args]); - } - - captureException(exception); - return originalCatch.apply(this, [exception, host, ...args]); - }; - - return descriptor; - }; -} diff --git a/packages/nestjs/src/index.ts b/packages/nestjs/src/index.ts index 71fb1ae4f78c..d99f491c1f6c 100644 --- a/packages/nestjs/src/index.ts +++ b/packages/nestjs/src/index.ts @@ -2,6 +2,9 @@ export * from '@sentry/node'; export { init } from './sdk'; -export { SentryTraced } from './decorators/sentry-traced'; -export { SentryCron } from './decorators/sentry-cron'; -export { WithSentry } from './decorators/with-sentry'; +export { + SentryTraced, + SentryCron, + WithSentry, + SentryExceptionCaptured, +} from './decorators'; From 3778ac9e2d5917c5af46687affeef5edd3aab7e6 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Nov 2024 10:06:59 +0100 Subject: [PATCH 21/29] ci: Bump `@actions/artifact` from 1x to 2.x (#13503) v1 of this is deprecated, which you can see in each CI run on develop as of now. Updating this should fix the deprecation. This _should_ not break stuff for us: https://www.npmjs.com/package/@actions/artifact#v2---whats-new --- dev-packages/size-limit-gh-action/index.mjs | 4 +- .../size-limit-gh-action/package.json | 2 +- yarn.lock | 363 ++++++++++++++---- 3 files changed, 283 insertions(+), 86 deletions(-) diff --git a/dev-packages/size-limit-gh-action/index.mjs b/dev-packages/size-limit-gh-action/index.mjs index 1b8daa867e82..c12f263f9ea9 100644 --- a/dev-packages/size-limit-gh-action/index.mjs +++ b/dev-packages/size-limit-gh-action/index.mjs @@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import * as artifact from '@actions/artifact'; +import { DefaultArtifactClient } from '@actions/artifact'; import * as core from '@actions/core'; import { exec } from '@actions/exec'; import { context, getOctokit } from '@actions/github'; @@ -195,7 +195,7 @@ async function runSizeLimitOnComparisonBranch() { const resultsFilePath = getResultsFilePath(); const limit = new SizeLimitFormatter(); - const artifactClient = artifact.create(); + const artifactClient = new DefaultArtifactClient(); const { output: baseOutput } = await execSizeLimit(); diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index ff7d7001625a..985e50ef37be 100644 --- a/dev-packages/size-limit-gh-action/package.json +++ b/dev-packages/size-limit-gh-action/package.json @@ -14,7 +14,7 @@ "fix": "eslint . --format stylish --fix" }, "dependencies": { - "@actions/artifact": "1.1.2", + "@actions/artifact": "2.1.11", "@actions/core": "1.10.1", "@actions/exec": "1.1.1", "@actions/github": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index dad8117d7ad2..1e0aafbf7b8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,17 +2,26 @@ # yarn lockfile v1 -"@actions/artifact@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@actions/artifact/-/artifact-1.1.2.tgz#13e796ce35214bd6486508f97b29b4b8e44f5a35" - integrity sha512-1gLONA4xw3/Q/9vGxKwkFdV9u1LE2RWGx/IpAqg28ZjprCnJFjwn4pA7LtShqg5mg5WhMek2fjpyH1leCmOlQQ== - dependencies: - "@actions/core" "^1.9.1" - "@actions/http-client" "^2.0.1" - tmp "^0.2.1" - tmp-promise "^3.0.2" +"@actions/artifact@2.1.11": + version "2.1.11" + resolved "https://registry.yarnpkg.com/@actions/artifact/-/artifact-2.1.11.tgz#3dac32ea6feaa545bb99cb04bc4dd97b0c58e86a" + integrity sha512-V/N/3yM3oLxozq2dpdGqbd/39UbDOR54bF25vYsvn3QZnyZERSzPjTAAwpGzdcwESye9G7vnuhPiKQACEuBQpg== + dependencies: + "@actions/core" "^1.10.0" + "@actions/github" "^5.1.1" + "@actions/http-client" "^2.1.0" + "@azure/storage-blob" "^12.15.0" + "@octokit/core" "^3.5.1" + "@octokit/plugin-request-log" "^1.0.4" + "@octokit/plugin-retry" "^3.0.9" + "@octokit/request-error" "^5.0.0" + "@protobuf-ts/plugin" "^2.2.3-alpha.1" + archiver "^7.0.1" + jwt-decode "^3.1.2" + twirp-ts "^2.5.0" + unzip-stream "^0.3.1" -"@actions/core@1.10.1", "@actions/core@^1.9.1": +"@actions/core@1.10.1": version "1.10.1" resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.10.1.tgz#61108e7ac40acae95ee36da074fa5850ca4ced8a" integrity sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g== @@ -20,14 +29,22 @@ "@actions/http-client" "^2.0.1" uuid "^8.3.2" -"@actions/exec@1.1.1": +"@actions/core@^1.10.0", "@actions/core@^1.9.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.11.1.tgz#ae683aac5112438021588030efb53b1adb86f172" + integrity sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A== + dependencies: + "@actions/exec" "^1.1.1" + "@actions/http-client" "^2.0.1" + +"@actions/exec@1.1.1", "@actions/exec@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@actions/exec/-/exec-1.1.1.tgz#2e43f28c54022537172819a7cf886c844221a611" integrity sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w== dependencies: "@actions/io" "^1.0.1" -"@actions/github@^5.0.0": +"@actions/github@^5.0.0", "@actions/github@^5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.1.1.tgz#40b9b9e1323a5efcf4ff7dadd33d8ea51651bbcb" integrity sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g== @@ -45,10 +62,10 @@ "@actions/core" "^1.9.1" minimatch "^3.0.4" -"@actions/http-client@^2.0.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.1.tgz#ed3fe7a5a6d317ac1d39886b0bb999ded229bb38" - integrity sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw== +"@actions/http-client@^2.0.1", "@actions/http-client@^2.1.0": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.3.tgz#31fc0b25c0e665754ed39a9f19a8611fc6dab674" + integrity sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA== dependencies: tunnel "^0.0.6" undici "^5.25.4" @@ -1066,7 +1083,7 @@ dependencies: tslib "^2.2.0" -"@azure/abort-controller@^2.0.0": +"@azure/abort-controller@^2.0.0", "@azure/abort-controller@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== @@ -1091,7 +1108,16 @@ "@azure/core-util" "^1.1.0" tslib "^2.6.2" -"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.9.2": +"@azure/core-auth@^1.8.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.9.0.tgz#ac725b03fabe3c892371065ee9e2041bee0fd1ac" + integrity sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.11.0" + tslib "^2.6.2" + +"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.6.2", "@azure/core-client@^1.9.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@azure/core-client/-/core-client-1.9.2.tgz#6fc69cee2816883ab6c5cdd653ee4f2ff9774f74" integrity sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w== @@ -1104,7 +1130,7 @@ "@azure/logger" "^1.0.0" tslib "^2.6.2" -"@azure/core-http-compat@^2.0.1": +"@azure/core-http-compat@^2.0.0", "@azure/core-http-compat@^2.0.1": version "2.1.2" resolved "https://registry.yarnpkg.com/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz#d1585ada24ba750dc161d816169b33b35f762f0d" integrity sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ== @@ -1144,6 +1170,20 @@ https-proxy-agent "^7.0.0" tslib "^2.6.2" +"@azure/core-rest-pipeline@^1.10.1": + version "1.18.0" + resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz#165f1cd9bb1060be3b6895742db3d1f1106271d3" + integrity sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.8.0" + "@azure/core-tracing" "^1.0.1" + "@azure/core-util" "^1.11.0" + "@azure/logger" "^1.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + tslib "^2.6.2" + "@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.1.2.tgz#065dab4e093fb61899988a1cdbc827d9ad90b4ee" @@ -1151,6 +1191,13 @@ dependencies: tslib "^2.6.2" +"@azure/core-tracing@^1.1.2": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.2.0.tgz#7be5d53c3522d639cf19042cbcdb19f71bc35ab2" + integrity sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg== + dependencies: + tslib "^2.6.2" + "@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.2.0", "@azure/core-util@^1.6.1", "@azure/core-util@^1.9.0": version "1.9.2" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.2.tgz#1dc37dc5b0dae34c578be62cf98905ba7c0cafe7" @@ -1159,6 +1206,14 @@ "@azure/abort-controller" "^2.0.0" tslib "^2.6.2" +"@azure/core-util@^1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.11.0.tgz#f530fc67e738aea872fbdd1cc8416e70219fada7" + integrity sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g== + dependencies: + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" + "@azure/core-util@^1.3.0": version "1.10.0" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.10.0.tgz#cf3163382d40343972848c914869864df5d44bdb" @@ -1167,6 +1222,14 @@ "@azure/abort-controller" "^2.0.0" tslib "^2.6.2" +"@azure/core-xml@^1.4.3": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@azure/core-xml/-/core-xml-1.4.4.tgz#a8656751943bf492762f758d147d33dfcd933d9e" + integrity sha512-J4FYAqakGXcbfeZjwjMzjNcpcH4E+JtEBv+xcV1yL0Ydn/6wbQfeFKTCHh9wttAi0lmajHw7yBbHPRG+YHckZQ== + dependencies: + fast-xml-parser "^4.4.1" + tslib "^2.6.2" + "@azure/identity@^4.2.1": version "4.4.1" resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.4.1.tgz#490fa2ad26786229afa36411892bb53dfa3478d3" @@ -1232,6 +1295,25 @@ jsonwebtoken "^9.0.0" uuid "^8.3.0" +"@azure/storage-blob@^12.15.0": + version "12.25.0" + resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.25.0.tgz#fa9a1d2456cdf6526450a8b73059d2f2e9b1ec76" + integrity sha512-oodouhA3nCCIh843tMMbxty3WqfNT+Vgzj3Xo5jqR9UPnzq3d7mzLjlHAYz7lW+b4km3SIgz+NAgztvhm7Z6kQ== + dependencies: + "@azure/abort-controller" "^2.1.2" + "@azure/core-auth" "^1.4.0" + "@azure/core-client" "^1.6.2" + "@azure/core-http-compat" "^2.0.0" + "@azure/core-lro" "^2.2.0" + "@azure/core-paging" "^1.1.1" + "@azure/core-rest-pipeline" "^1.10.1" + "@azure/core-tracing" "^1.1.2" + "@azure/core-util" "^1.6.1" + "@azure/core-xml" "^1.4.3" + "@azure/logger" "^1.0.0" + events "^3.0.0" + tslib "^2.2.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -7201,13 +7283,11 @@ "@octokit/types" "^6.0.3" "@octokit/auth-token@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.2.tgz#a0fc8de149fd15876e1ac78f6525c1c5ab48435f" - integrity sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q== - dependencies: - "@octokit/types" "^8.0.0" + version "3.0.4" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.4.tgz#70e941ba742bdd2b49bdb7393e821dea8520a3db" + integrity sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ== -"@octokit/core@^3.6.0": +"@octokit/core@^3.5.1", "@octokit/core@^3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q== @@ -7243,11 +7323,11 @@ universal-user-agent "^6.0.0" "@octokit/endpoint@^7.0.0": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.3.tgz#0b96035673a9e3bedf8bab8f7335de424a2147ed" - integrity sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw== + version "7.0.6" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.6.tgz#791f65d3937555141fb6c08f91d618a7d645f1e2" + integrity sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg== dependencies: - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" is-plain-object "^5.0.0" universal-user-agent "^6.0.0" @@ -7261,12 +7341,12 @@ universal-user-agent "^6.0.0" "@octokit/graphql@^5.0.0": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.4.tgz#519dd5c05123868276f3ae4e50ad565ed7dff8c8" - integrity sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A== + version "5.0.6" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.6.tgz#9eac411ac4353ccc5d3fca7d76736e6888c5d248" + integrity sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw== dependencies: "@octokit/request" "^6.0.0" - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" universal-user-agent "^6.0.0" "@octokit/openapi-types@^12.11.0": @@ -7274,24 +7354,19 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" integrity sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ== -"@octokit/openapi-types@^14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-14.0.0.tgz#949c5019028c93f189abbc2fb42f333290f7134a" - integrity sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw== - -"@octokit/openapi-types@^16.0.0": - version "16.0.0" - resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz#d92838a6cd9fb4639ca875ddb3437f1045cc625e" - integrity sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA== - "@octokit/openapi-types@^18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-18.0.0.tgz#f43d765b3c7533fd6fb88f3f25df079c24fccf69" - integrity sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw== + version "18.1.1" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-18.1.1.tgz#09bdfdabfd8e16d16324326da5148010d765f009" + integrity sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw== + +"@octokit/openapi-types@^22.2.0": + version "22.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" + integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== "@octokit/plugin-enterprise-rest@6.0.1": version "6.0.1" - resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" + resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== "@octokit/plugin-paginate-rest@^2.17.0": @@ -7329,6 +7404,14 @@ dependencies: "@octokit/types" "^10.0.0" +"@octokit/plugin-retry@^3.0.9": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz#ae625cca1e42b0253049102acd71c1d5134788fe" + integrity sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ== + dependencies: + "@octokit/types" "^6.0.3" + bottleneck "^2.15.3" + "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" @@ -7339,11 +7422,20 @@ once "^1.4.0" "@octokit/request-error@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.2.tgz#f74c0f163d19463b87528efe877216c41d6deb0a" - integrity sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.3.tgz#ef3dd08b8e964e53e55d471acfe00baa892b9c69" + integrity sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ== dependencies: - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request-error@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.0.tgz#ee4138538d08c81a60be3f320cd71063064a3b30" + integrity sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q== + dependencies: + "@octokit/types" "^13.1.0" deprecation "^2.0.0" once "^1.4.0" @@ -7360,13 +7452,13 @@ universal-user-agent "^6.0.0" "@octokit/request@^6.0.0": - version "6.2.2" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.2.tgz#a2ba5ac22bddd5dcb3f539b618faa05115c5a255" - integrity sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw== + version "6.2.8" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.8.tgz#aaf480b32ab2b210e9dadd8271d187c93171d8eb" + integrity sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw== dependencies: "@octokit/endpoint" "^7.0.0" "@octokit/request-error" "^3.0.0" - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" is-plain-object "^5.0.0" node-fetch "^2.6.7" universal-user-agent "^6.0.0" @@ -7393,6 +7485,13 @@ dependencies: "@octokit/openapi-types" "^18.0.0" +"@octokit/types@^13.1.0": + version "13.6.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.1.tgz#432fc6c0aaae54318e5b2d3e15c22ac97fc9b15f" + integrity sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g== + dependencies: + "@octokit/openapi-types" "^22.2.0" + "@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.39.0", "@octokit/types@^6.40.0": version "6.41.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.41.0.tgz#e58ef78d78596d2fb7df9c6259802464b5f84a04" @@ -7400,21 +7499,7 @@ dependencies: "@octokit/openapi-types" "^12.11.0" -"@octokit/types@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-8.0.0.tgz#93f0b865786c4153f0f6924da067fe0bb7426a9f" - integrity sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg== - dependencies: - "@octokit/openapi-types" "^14.0.0" - -"@octokit/types@^9.0.0": - version "9.0.0" - resolved "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz#6050db04ddf4188ec92d60e4da1a2ce0633ff635" - integrity sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw== - dependencies: - "@octokit/openapi-types" "^16.0.0" - -"@octokit/types@^9.2.3": +"@octokit/types@^9.0.0", "@octokit/types@^9.2.3": version "9.3.2" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-9.3.2.tgz#3f5f89903b69f6a2d196d78ec35f888c0013cac5" integrity sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA== @@ -7989,6 +8074,42 @@ "@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51 || ^0.52.0" "@opentelemetry/sdk-trace-base" "^1.22" +"@protobuf-ts/plugin-framework@^2.0.7", "@protobuf-ts/plugin-framework@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/plugin-framework/-/plugin-framework-2.9.4.tgz#d7a617dedda4a12c568fdc1db5aa67d5e4da2406" + integrity sha512-9nuX1kjdMliv+Pes8dQCKyVhjKgNNfwxVHg+tx3fLXSfZZRcUHMc1PMwB9/vTvc6gBKt9QGz5ERqSqZc0++E9A== + dependencies: + "@protobuf-ts/runtime" "^2.9.4" + typescript "^3.9" + +"@protobuf-ts/plugin@^2.2.3-alpha.1": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/plugin/-/plugin-2.9.4.tgz#4e593e59013aaad313e7abbabe6e61964ef0ca28" + integrity sha512-Db5Laq5T3mc6ERZvhIhkj1rn57/p8gbWiCKxQWbZBBl20wMuqKoHbRw4tuD7FyXi+IkwTToaNVXymv5CY3E8Rw== + dependencies: + "@protobuf-ts/plugin-framework" "^2.9.4" + "@protobuf-ts/protoc" "^2.9.4" + "@protobuf-ts/runtime" "^2.9.4" + "@protobuf-ts/runtime-rpc" "^2.9.4" + typescript "^3.9" + +"@protobuf-ts/protoc@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/protoc/-/protoc-2.9.4.tgz#a92262ee64d252998540238701d2140f4ffec081" + integrity sha512-hQX+nOhFtrA+YdAXsXEDrLoGJqXHpgv4+BueYF0S9hy/Jq0VRTVlJS1Etmf4qlMt/WdigEes5LOd/LDzui4GIQ== + +"@protobuf-ts/runtime-rpc@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.9.4.tgz#d6ab2316c0ba67ce5a08863bb23203a837ff2a3b" + integrity sha512-y9L9JgnZxXFqH5vD4d7j9duWvIJ7AShyBRoNKJGhu9Q27qIbchfzli66H9RvrQNIFk5ER7z1Twe059WZGqERcA== + dependencies: + "@protobuf-ts/runtime" "^2.9.4" + +"@protobuf-ts/runtime@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime/-/runtime-2.9.4.tgz#db8a78b1c409e26d258ca39464f4757d804add8f" + integrity sha512-vHRFWtJJB/SiogWDF0ypoKfRIZ41Kq+G9cEFj6Qm1eQaAhJ1LDFvgZ7Ja4tb3iLOQhz0PaoPnnOijF1qmEqTxg== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -12858,6 +12979,14 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +binary@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + "binaryextensions@1 || 2", binaryextensions@^2.1.2: version "2.3.0" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22" @@ -12977,6 +13106,11 @@ boolean@^3.1.4: resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== +bottleneck@^2.15.3: + version "2.19.5" + resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" + integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== + bower-config@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.3.tgz#3454fecdc5f08e7aa9cc6d556e492be0669689ae" @@ -13658,6 +13792,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== + builtin-modules@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -13992,6 +14131,13 @@ chai@^4.3.10: pathval "^1.1.1" type-detect "^4.0.8" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== + dependencies: + traverse ">=0.3.0 <0.4" + chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -14516,6 +14662,11 @@ commander@^4.0.0, commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + commander@^8.0.0, commander@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" @@ -16047,6 +16198,14 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" +dot-object@^2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/dot-object/-/dot-object-2.1.5.tgz#0ff0f1bff42c47ff06272081b208658c0a0231c2" + integrity sha512-xHF8EP4XH/Ba9fvAF2LDd5O3IITVolerVV6xvkxoM8zlGEiCUrggpAnHyOoKJKCrhvPcGATFAUwIujj7bRG5UA== + dependencies: + commander "^6.1.0" + glob "^7.1.6" + dot-prop@^5.1.0, dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -18374,6 +18533,13 @@ fast-xml-parser@4.2.5: dependencies: strnum "^1.0.5" +fast-xml-parser@^4.4.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz#2882b7d01a6825dfdf909638f2de0256351def37" + integrity sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg== + dependencies: + strnum "^1.0.5" + fastq@^1.6.0: version "1.11.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" @@ -22457,6 +22623,11 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + kafkajs@2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-2.2.4.tgz#59e6e16459d87fdf8b64be73970ed5aa42370a5b" @@ -31810,13 +31981,6 @@ titleize@^3.0.0: resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53" integrity sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ== -tmp-promise@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7" - integrity sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ== - dependencies: - tmp "^0.2.0" - tmp@0.0.28: version "0.0.28" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" @@ -31838,11 +32002,6 @@ tmp@^0.1.0: dependencies: rimraf "^2.6.3" -tmp@^0.2.0: - version "0.2.3" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" - integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== - tmp@^0.2.1, tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -31959,6 +32118,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== + tree-kill@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -32049,6 +32213,14 @@ ts-node@10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +ts-poet@^4.5.0: + version "4.15.0" + resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.15.0.tgz#637145fa554d3b27c56541578df0ce08cd9eb328" + integrity sha512-sLLR8yQBvHzi9d4R1F4pd+AzQxBfzOSSjfxiJxQhkUoH5bL7RsAC6wgvtVUQdGqiCsyS9rT6/8X2FI7ipdir5g== + dependencies: + lodash "^4.17.15" + prettier "^2.5.1" + tsconfck@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.0.tgz#b469f1ced12973bbec3209a55ed8de3bb04223c9" @@ -32145,6 +32317,18 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +twirp-ts@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/twirp-ts/-/twirp-ts-2.5.0.tgz#b43f09e95868d68ecd5c755ecbb08a7e51388504" + integrity sha512-JTKIK5Pf/+3qCrmYDFlqcPPUx+ohEWKBaZy8GL8TmvV2VvC0SXVyNYILO39+GCRbqnuP6hBIF+BVr8ZxRz+6fw== + dependencies: + "@protobuf-ts/plugin-framework" "^2.0.7" + camel-case "^4.1.2" + dot-object "^2.1.4" + path-to-regexp "^6.2.0" + ts-poet "^4.5.0" + yaml "^1.10.2" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -32269,6 +32453,11 @@ typescript@4.9.5, typescript@^4.9.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== +typescript@^3.9: + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== + typescript@^5.0.4, typescript@^5.4.4: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" @@ -32889,6 +33078,14 @@ unwasm@^0.3.9: pkg-types "^1.0.3" unplugin "^1.10.0" +unzip-stream@^0.3.1: + version "0.3.4" + resolved "https://registry.yarnpkg.com/unzip-stream/-/unzip-stream-0.3.4.tgz#b4576755061809cf210b776cf26888d6a7823ead" + integrity sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw== + dependencies: + binary "^0.3.0" + mkdirp "^0.5.1" + upath@2.0.1, upath@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" @@ -34266,7 +34463,7 @@ yaml@2.2.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== -yaml@^1.10.0: +yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== From 9993d1e9228e6830b3771b64b5d980840bc012f3 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 18 Nov 2024 15:08:31 +0100 Subject: [PATCH 22/29] feat(node): Add alias `childProcessIntegration` for `processThreadBreadcrumbIntegration` and deprecate it (#14334) --- .../scripts/consistentExports.ts | 1 + docs/migration/draft-v9-migration-guide.md | 6 +++++- packages/astro/src/index.server.ts | 2 ++ packages/aws-serverless/src/index.ts | 2 ++ packages/google-cloud-serverless/src/index.ts | 2 ++ packages/node/src/index.ts | 3 ++- .../{processThread.ts => childProcess.ts} | 13 +++++++++---- packages/node/src/sdk/index.ts | 4 ++-- 8 files changed, 25 insertions(+), 8 deletions(-) rename packages/node/src/integrations/{processThread.ts => childProcess.ts} (85%) diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 546639e8a766..83f9c1639cdc 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -52,6 +52,7 @@ const DEPENDENTS: Dependent[] = [ 'NodeClient', // Bun doesn't emit the required diagnostics_channel events 'processThreadBreadcrumbIntegration', + 'childProcessIntegration', ], }, { diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index bbbb2c2d367f..8b0cebd7e51d 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -12,6 +12,10 @@ - Deprecated `transactionNamingScheme` option in `requestDataIntegration`. -## `@sentry/types`` +## `@sentry/types` - Deprecated `Request` in favor of `RequestEventData`. + +## Server-side SDKs (`@sentry/node` and all dependents) + +- Deprecated `processThreadBreadcrumbIntegration` in favor of `childProcessIntegration`. Functionally they are the same. diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index e4c871ec74ea..853623abbc8a 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -91,7 +91,9 @@ export { parameterize, postgresIntegration, prismaIntegration, + // eslint-disable-next-line deprecation/deprecation processThreadBreadcrumbIntegration, + childProcessIntegration, redisIntegration, requestDataIntegration, rewriteFramesIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 060dddd51787..8341b01719c1 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -105,7 +105,9 @@ export { setupNestErrorHandler, postgresIntegration, prismaIntegration, + // eslint-disable-next-line deprecation/deprecation processThreadBreadcrumbIntegration, + childProcessIntegration, hapiIntegration, setupHapiErrorHandler, spotlightIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 5e1c2bba5bc1..53cf4c026868 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -117,7 +117,9 @@ export { zodErrorsIntegration, profiler, amqplibIntegration, + // eslint-disable-next-line deprecation/deprecation processThreadBreadcrumbIntegration, + childProcessIntegration, } from '@sentry/node'; export { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 88b105682e6d..cc81dce37577 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -33,7 +33,8 @@ export { tediousIntegration } from './integrations/tracing/tedious'; export { genericPoolIntegration } from './integrations/tracing/genericPool'; export { dataloaderIntegration } from './integrations/tracing/dataloader'; export { amqplibIntegration } from './integrations/tracing/amqplib'; -export { processThreadBreadcrumbIntegration } from './integrations/processThread'; +// eslint-disable-next-line deprecation/deprecation +export { processThreadBreadcrumbIntegration, childProcessIntegration } from './integrations/childProcess'; export { SentryContextManager } from './otel/contextManager'; export { generateInstrumentOnce } from './otel/instrument'; diff --git a/packages/node/src/integrations/processThread.ts b/packages/node/src/integrations/childProcess.ts similarity index 85% rename from packages/node/src/integrations/processThread.ts rename to packages/node/src/integrations/childProcess.ts index 870a0dc6df64..99525b4092b4 100644 --- a/packages/node/src/integrations/processThread.ts +++ b/packages/node/src/integrations/childProcess.ts @@ -2,7 +2,6 @@ import type { ChildProcess } from 'node:child_process'; import * as diagnosticsChannel from 'node:diagnostics_channel'; import type { Worker } from 'node:worker_threads'; import { addBreadcrumb, defineIntegration } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; interface Options { /** @@ -13,9 +12,13 @@ interface Options { includeChildProcessArgs?: boolean; } +// TODO(v9): Update this name and mention in migration docs. const INTEGRATION_NAME = 'ProcessAndThreadBreadcrumbs'; -const _processThreadBreadcrumbIntegration = ((options: Options = {}) => { +/** + * Capture breadcrumbs for child processes and worker threads. + */ +export const childProcessIntegration = defineIntegration((options: Options = {}) => { return { name: INTEGRATION_NAME, setup(_client) { @@ -34,12 +37,14 @@ const _processThreadBreadcrumbIntegration = ((options: Options = {}) => { }); }, }; -}) satisfies IntegrationFn; +}); /** * Capture breadcrumbs for child processes and worker threads. + * + * @deprecated Use `childProcessIntegration` integration instead. Functionally they are the same. `processThreadBreadcrumbIntegration` will be removed in the next major version. */ -export const processThreadBreadcrumbIntegration = defineIntegration(_processThreadBreadcrumbIntegration); +export const processThreadBreadcrumbIntegration = childProcessIntegration; function captureChildProcessEvents(child: ChildProcess, options: Options): void { let hasExited = false; diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 87d61cc908bc..edebeea384db 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -30,13 +30,13 @@ import { consoleIntegration } from '../integrations/console'; import { nodeContextIntegration } from '../integrations/context'; import { contextLinesIntegration } from '../integrations/contextlines'; +import { childProcessIntegration } from '../integrations/childProcess'; import { httpIntegration } from '../integrations/http'; import { localVariablesIntegration } from '../integrations/local-variables'; import { modulesIntegration } from '../integrations/modules'; import { nativeNodeFetchIntegration } from '../integrations/node-fetch'; import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexception'; import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection'; -import { processThreadBreadcrumbIntegration } from '../integrations/processThread'; import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight'; import { getAutoPerformanceIntegrations } from '../integrations/tracing'; import { makeNodeTransport } from '../transports'; @@ -72,7 +72,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { contextLinesIntegration(), localVariablesIntegration(), nodeContextIntegration(), - processThreadBreadcrumbIntegration(), + childProcessIntegration(), ...getCjsOnlyIntegrations(), ]; } From 4fbc3b2ee7cb4247c4d03339bd92d567da0345ab Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 18 Nov 2024 16:04:38 +0100 Subject: [PATCH 23/29] meta(utils): Replace custom versionbump script with rollup replace plugin (#14340) --- .github/workflows/build.yml | 1 - packages/core/test/lib/carrier.test.ts | 5 ++--- packages/utils/.eslintrc.js | 2 +- packages/utils/package.json | 1 - packages/utils/rollup.npm.config.mjs | 10 +++++++++ packages/utils/src/version.ts | 5 ++++- scripts/versionbump.js | 31 -------------------------- 7 files changed, 17 insertions(+), 38 deletions(-) delete mode 100644 scripts/versionbump.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72d572c01e5c..cd358158b2ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,6 @@ env: ${{ github.workspace }}/packages/*/build ${{ github.workspace }}/packages/ember/*.d.ts ${{ github.workspace }}/packages/gatsby/*.d.ts - ${{ github.workspace }}/packages/core/src/version.ts ${{ github.workspace }}/packages/utils/cjs ${{ github.workspace }}/packages/utils/esm diff --git a/packages/core/test/lib/carrier.test.ts b/packages/core/test/lib/carrier.test.ts index 3c94c96b98c1..6e846d496ea5 100644 --- a/packages/core/test/lib/carrier.test.ts +++ b/packages/core/test/lib/carrier.test.ts @@ -42,14 +42,13 @@ describe('getSentryCarrier', () => { describe('multiple (older) SDKs', () => { it("returns the version of the sentry carrier object of the SDK's version rather than the one set in .version", () => { const sentryCarrier = getSentryCarrier({ + // @ts-expect-error - this is just a test object __SENTRY__: { - version: '8.0.0' as const, // another SDK set this + version: '8.0.0', // another SDK set this '8.0.0': { - // @ts-expect-error - this is just a test object, not passing a full stack stack: {}, }, [SDK_VERSION]: { - // @ts-expect-error - this is just a test object, not passing a full ACS acs: {}, }, hub: {}, diff --git a/packages/utils/.eslintrc.js b/packages/utils/.eslintrc.js index 35c6aab563f5..604db95b9dbe 100644 --- a/packages/utils/.eslintrc.js +++ b/packages/utils/.eslintrc.js @@ -15,5 +15,5 @@ module.exports = { }, ], // symlinks to the folders inside of `build`, created to simulate what's in the npm package - ignorePatterns: ['cjs/**', 'esm/**'], + ignorePatterns: ['cjs/**', 'esm/**', 'rollup.npm.config.mjs'], }; diff --git a/packages/utils/package.json b/packages/utils/package.json index ed4493a2a98b..3ef330009ccc 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -63,7 +63,6 @@ "lint": "eslint . --format stylish", "test": "jest", "test:watch": "jest --watch", - "version": "node ../../scripts/versionbump.js src/version.ts", "yalc:publish": "yalc publish --push --sig" }, "volta": { diff --git a/packages/utils/rollup.npm.config.mjs b/packages/utils/rollup.npm.config.mjs index d28a7a6f54a0..8e219b3c2d9b 100644 --- a/packages/utils/rollup.npm.config.mjs +++ b/packages/utils/rollup.npm.config.mjs @@ -1,4 +1,6 @@ +import replace from '@rollup/plugin-replace'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +import packageJson from './package.json' with { type: 'json' }; export default makeNPMConfigVariants( makeBaseNPMConfig({ @@ -12,6 +14,14 @@ export default makeNPMConfigVariants( ? true : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), }, + plugins: [ + replace({ + preventAssignment: true, + values: { + __SENTRY_SDK_VERSION__: JSON.stringify(packageJson.version), + }, + }), + ], }, }), ); diff --git a/packages/utils/src/version.ts b/packages/utils/src/version.ts index 79596a866885..c4f3fcfb8363 100644 --- a/packages/utils/src/version.ts +++ b/packages/utils/src/version.ts @@ -1 +1,4 @@ -export const SDK_VERSION = '8.38.0'; +// This is a magic string replaced by rollup +declare const __SENTRY_SDK_VERSION__: string; + +export const SDK_VERSION = typeof __SENTRY_SDK_VERSION__ === 'string' ? __SENTRY_SDK_VERSION__ : '0.0.0-unknown.0'; diff --git a/scripts/versionbump.js b/scripts/versionbump.js deleted file mode 100644 index 931df2a7829c..000000000000 --- a/scripts/versionbump.js +++ /dev/null @@ -1,31 +0,0 @@ -const { readFile, writeFile } = require('node:fs').promises; -const pjson = require(`${process.cwd()}/package.json`); - -const REPLACE_REGEX = /\d+\.\d+.\d+(?:-\w+(?:\.\w+)?)?/g; - -async function run() { - const files = process.argv.slice(2); - if (files.length === 0) { - // eslint-disable-next-line no-console - console.error('[versionbump] Please provide files to bump'); - process.exit(1); - } - - try { - await Promise.all( - files.map(async file => { - const data = String(await readFile(file, 'utf8')); - await writeFile(file, data.replace(REPLACE_REGEX, pjson.version)); - }), - ); - - // eslint-disable-next-line no-console - console.log(`[versionbump] Bumped version for ${files.join(', ')}`); - } catch (error) { - // eslint-disable-next-line no-console - console.error('[versionbump] Error occurred:', error); - process.exit(1); - } -} - -run(); From d253621128cf795e89c74e3188efc9722e60d1ca Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 18 Nov 2024 16:04:54 +0100 Subject: [PATCH 24/29] ref(utils): Explicitly export API in `@sentry/utils` (#14338) Resolves https://github.com/getsentry/sentry-javascript/issues/14211 --- packages/utils/src/index.ts | 209 ++++++++++++++++++++++++++++-------- yarn.lock | 12 --- 2 files changed, 167 insertions(+), 54 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 2a89826313e8..e60bc3bec409 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,43 +1,168 @@ -export * from './aggregate-errors'; -export * from './array'; -export * from './breadcrumb-log-level'; -export * from './browser'; -export * from './dsn'; -export * from './error'; -export * from './worldwide'; -export * from './instrument'; -export * from './is'; -export * from './isBrowser'; -export * from './logger'; -export * from './memo'; -export * from './misc'; -export * from './node'; -export * from './normalize'; -export * from './object'; -export * from './path'; -export * from './promisebuffer'; +export { applyAggregateErrorsToEvent } from './aggregate-errors'; +export { flatten } from './array'; +export { getBreadcrumbLogLevelFromHttpStatusCode } from './breadcrumb-log-level'; +export { getComponentName, getDomElement, getLocationHref, htmlTreeAsString } from './browser'; +export { dsnFromString, dsnToString, makeDsn } from './dsn'; +export { SentryError } from './error'; +export { GLOBAL_OBJ, getGlobalSingleton } from './worldwide'; +export type { InternalGlobal } from './worldwide'; +export { addConsoleInstrumentationHandler } from './instrument/console'; +export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './instrument/fetch'; +export { addGlobalErrorInstrumentationHandler } from './instrument/globalError'; +export { addGlobalUnhandledRejectionInstrumentationHandler } from './instrument/globalUnhandledRejection'; +export { + addHandler, + maybeInstrument, + resetInstrumentationHandlers, + triggerHandlers, +} from './instrument/handlers'; +export { + isDOMError, + isDOMException, + isElement, + isError, + isErrorEvent, + isEvent, + isInstanceOf, + isParameterizedString, + isPlainObject, + isPrimitive, + isRegExp, + isString, + isSyntheticEvent, + isThenable, + isVueViewModel, +} from './is'; +export { isBrowser } from './isBrowser'; +export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './logger'; +export { memoBuilder } from './memo'; +export { + addContextToFrame, + addExceptionMechanism, + addExceptionTypeValue, + arrayify, + checkOrSetAlreadyCaught, + getEventDescription, + parseSemver, + uuid4, +} from './misc'; +export { dynamicRequire, isNodeEnv, loadModule } from './node'; +export { normalize, normalizeToSize, normalizeUrlToBase } from './normalize'; +export { + addNonEnumerableProperty, + convertToPlainObject, + dropUndefinedKeys, + extractExceptionKeysForMessage, + fill, + getOriginalFunction, + markFunctionWrapped, + objectify, + urlEncode, +} from './object'; +export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './path'; +export { makePromiseBuffer } from './promisebuffer'; +export type { PromiseBuffer } from './promisebuffer'; + // TODO: Remove requestdata export once equivalent integration is used everywhere -export * from './requestdata'; -export * from './severity'; -export * from './stacktrace'; -export * from './node-stack-trace'; -export * from './string'; -export * from './supports'; -export * from './syncpromise'; -export * from './time'; -export * from './tracing'; -export * from './env'; -export * from './envelope'; -export * from './clientreport'; -export * from './ratelimit'; -export * from './baggage'; -export * from './url'; -export * from './cache'; -export * from './eventbuilder'; -export * from './anr'; -export * from './lru'; -export * from './buildPolyfills'; -export * from './propagationContext'; -export * from './vercelWaitUntil'; -export * from './version'; -export * from './debug-ids'; +export { + DEFAULT_USER_INCLUDES, + addNormalizedRequestDataToEvent, + addRequestDataToEvent, + // eslint-disable-next-line deprecation/deprecation + extractPathForTransaction, + extractRequestData, + winterCGHeadersToDict, + winterCGRequestToRequestData, +} from './requestdata'; +export type { + AddRequestDataToEventOptions, + // eslint-disable-next-line deprecation/deprecation + TransactionNamingScheme, +} from './requestdata'; + +export { severityLevelFromString, validSeverityLevels } from './severity'; +export { + UNKNOWN_FUNCTION, + createStackParser, + getFramesFromEvent, + getFunctionName, + stackParserFromStackParserOptions, + stripSentryFramesAndReverse, +} from './stacktrace'; +export { filenameIsInApp, node, nodeStackLineParser } from './node-stack-trace'; +export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './string'; +export { + isNativeFunction, + supportsDOMError, + supportsDOMException, + supportsErrorEvent, + supportsFetch, + supportsNativeFetch, + supportsReferrerPolicy, + supportsReportingObserver, +} from './supports'; +export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './syncpromise'; +export { + _browserPerformanceTimeOriginMode, + browserPerformanceTimeOrigin, + dateTimestampInSeconds, + timestampInSeconds, +} from './time'; +export { + TRACEPARENT_REGEXP, + extractTraceparentData, + generateSentryTraceHeader, + propagationContextFromHeaders, +} from './tracing'; +export { getSDKSource, isBrowserBundle } from './env'; +export type { SdkSource } from './env'; +export { + addItemToEnvelope, + createAttachmentEnvelopeItem, + createEnvelope, + createEventEnvelopeHeaders, + createSpanEnvelopeItem, + envelopeContainsItemType, + envelopeItemTypeToDataCategory, + forEachEnvelopeItem, + getSdkMetadataForEnvelopeHeader, + parseEnvelope, + serializeEnvelope, +} from './envelope'; +export { createClientReportEnvelope } from './clientreport'; +export { + DEFAULT_RETRY_AFTER, + disabledUntil, + isRateLimited, + parseRetryAfterHeader, + updateRateLimits, +} from './ratelimit'; +export type { RateLimits } from './ratelimit'; +export { + BAGGAGE_HEADER_NAME, + MAX_BAGGAGE_STRING_LENGTH, + SENTRY_BAGGAGE_KEY_PREFIX, + SENTRY_BAGGAGE_KEY_PREFIX_REGEX, + baggageHeaderToDynamicSamplingContext, + dynamicSamplingContextToSentryBaggageHeader, + parseBaggageHeader, +} from './baggage'; + +export { getNumberOfUrlSegments, getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url'; +export { makeFifoCache } from './cache'; +export { eventFromMessage, eventFromUnknownInput, exceptionFromError, parseStackFrames } from './eventbuilder'; +export { callFrameToStackFrame, watchdogTimer } from './anr'; +export { LRUMap } from './lru'; +export { generatePropagationContext } from './propagationContext'; +export { vercelWaitUntil } from './vercelWaitUntil'; +export { SDK_VERSION } from './version'; +export { getDebugImagesForResources, getFilenameToDebugIdMap } from './debug-ids'; +export { escapeStringForRegex } from './vendor/escapeStringForRegex'; +export { supportsHistory } from './vendor/supportsHistory'; + +export { _asyncNullishCoalesce } from './buildPolyfills/_asyncNullishCoalesce'; +export { _asyncOptionalChain } from './buildPolyfills/_asyncOptionalChain'; +export { _asyncOptionalChainDelete } from './buildPolyfills/_asyncOptionalChainDelete'; +export { _nullishCoalesce } from './buildPolyfills/_nullishCoalesce'; +export { _optionalChain } from './buildPolyfills/_optionalChain'; +export { _optionalChainDelete } from './buildPolyfills/_optionalChainDelete'; diff --git a/yarn.lock b/yarn.lock index 1e0aafbf7b8c..320f57f45de4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6747,13 +6747,6 @@ path-to-regexp "3.3.0" tslib "2.7.0" -"@nestjs/event-emitter@^2.0.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@nestjs/event-emitter/-/event-emitter-2.1.1.tgz#4e34edc487c507edbe6d02033e3dd014a19210f9" - integrity sha512-6L6fBOZTyfFlL7Ih/JDdqlCzZeCW0RjCX28wnzGyg/ncv5F/EOeT1dfopQr1loBRQ3LTgu8OWM7n4zLN4xigsg== - dependencies: - eventemitter2 "6.4.9" - "@nestjs/platform-express@10.4.6": version "10.4.6" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.6.tgz#6c39c522fa66036b4256714fea203fbeb49fc4de" @@ -18124,11 +18117,6 @@ eventemitter-asyncresource@^1.0.0: resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b" integrity sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ== -eventemitter2@6.4.9: - version "6.4.9" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" - integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== - eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" From fe1fb8c6849bf2e22191e956a538e9e00f836421 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Nov 2024 17:31:34 +0100 Subject: [PATCH 25/29] fix(cdn): Ensure `_sentryModuleMetadata` is not mangled (#14344) Fixes https://github.com/getsentry/sentry-javascript/issues/14343 --- dev-packages/rollup-utils/plugins/bundlePlugins.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index a3e25c232479..dce0ca15bf35 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -138,6 +138,8 @@ export function makeTerserPlugin() { '_resolveFilename', // Set on e.g. the shim feedbackIntegration to be able to detect it '_isShim', + // This is used in metadata integration + '_sentryModuleMetadata', ], }, }, From da5b2e09acd6f10243b528cc2e99d2fb70494161 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 19 Nov 2024 09:40:22 +0100 Subject: [PATCH 26/29] fix(replay): Remove replay id from DSC on expired sessions (#14342) --- .../src/coreHandlers/handleGlobalEvent.ts | 3 +++ .../coreHandlers/handleGlobalEvent.test.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts b/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts index d0ea607e1c06..6ba64244fddf 100644 --- a/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts +++ b/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts @@ -5,6 +5,7 @@ import type { ReplayContainer } from '../types'; import { isErrorEvent, isFeedbackEvent, isReplayEvent, isTransactionEvent } from '../util/eventUtils'; import { isRrwebError } from '../util/isRrwebError'; import { logger } from '../util/logger'; +import { resetReplayIdOnDynamicSamplingContext } from '../util/resetReplayIdOnDynamicSamplingContext'; import { addFeedbackBreadcrumb } from './util/addFeedbackBreadcrumb'; import { shouldSampleForBufferEvent } from './util/shouldSampleForBufferEvent'; @@ -34,6 +35,8 @@ export function handleGlobalEventListener(replay: ReplayContainer): (event: Even // Ensure we do not add replay_id if the session is expired const isSessionActive = replay.checkAndHandleExpiredSession(); if (!isSessionActive) { + // prevent exceeding replay durations by removing the expired replayId from the DSC + resetReplayIdOnDynamicSamplingContext(); return event; } diff --git a/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts b/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts index 9e888568d04d..a3ad967e1586 100644 --- a/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts +++ b/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts @@ -11,6 +11,7 @@ import { REPLAY_EVENT_NAME, SESSION_IDLE_EXPIRE_DURATION } from '../../../src/co import { handleGlobalEventListener } from '../../../src/coreHandlers/handleGlobalEvent'; import type { ReplayContainer } from '../../../src/replay'; import { makeSession } from '../../../src/session/Session'; +import * as resetReplayIdOnDynamicSamplingContextModule from '../../../src/util/resetReplayIdOnDynamicSamplingContext'; import { Error } from '../../fixtures/error'; import { Transaction } from '../../fixtures/transaction'; import { resetSdkMock } from '../../mocks/resetSdkMock'; @@ -416,4 +417,21 @@ describe('Integration | coreHandlers | handleGlobalEvent', () => { }), ); }); + + it('resets replayId on DSC when session expires', () => { + const errorEvent = Error(); + const txEvent = Transaction(); + + vi.spyOn(replay, 'checkAndHandleExpiredSession').mockReturnValue(false); + + const resetReplayIdSpy = vi.spyOn( + resetReplayIdOnDynamicSamplingContextModule, + 'resetReplayIdOnDynamicSamplingContext', + ); + + handleGlobalEventListener(replay)(errorEvent, {}); + handleGlobalEventListener(replay)(txEvent, {}); + + expect(resetReplayIdSpy).toHaveBeenCalledTimes(2); + }); }); From 4357b44c3e366b37045963feb4879c1932152809 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 19 Nov 2024 10:00:08 +0100 Subject: [PATCH 27/29] test: Clean up test output warnings (#14358) This PR cleans up some jest test output: 1. Ensure we do avoid printing out some `console.error` stuff for invalid DSN parsing. 2. Update jest config to avoid printing of these warnings you get all the time in the tests: ``` ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information. ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information. ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information. ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information. ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information. ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information. ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information. ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information. ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information. ``` --- jest/jest.config.js | 7 +++++-- packages/core/test/lib/envelope.test.ts | 7 +++++++ .../core/test/lib/metrics/browser-aggregator.test.ts | 12 ++++++++++++ packages/nextjs/test/utils/tunnelRoute.test.ts | 5 +++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/jest/jest.config.js b/jest/jest.config.js index 6bb8d30df35e..ce564a640f2d 100644 --- a/jest/jest.config.js +++ b/jest/jest.config.js @@ -3,8 +3,7 @@ module.exports = { rootDir: process.cwd(), collectCoverage: true, transform: { - '^.+\\.ts$': 'ts-jest', - '^.+\\.tsx$': 'ts-jest', + '^.+\\.(ts|tsx)$': 'ts-jest', }, coverageDirectory: '/coverage', moduleFileExtensions: ['js', 'ts', 'tsx'], @@ -15,6 +14,10 @@ module.exports = { globals: { 'ts-jest': { tsconfig: '/tsconfig.test.json', + diagnostics: { + // Ignore this warning for tests, we do not care about this + ignoreCodes: ['TS151001'], + }, }, __DEBUG_BUILD__: true, }, diff --git a/packages/core/test/lib/envelope.test.ts b/packages/core/test/lib/envelope.test.ts index 74e764c1d938..253ac07d96c2 100644 --- a/packages/core/test/lib/envelope.test.ts +++ b/packages/core/test/lib/envelope.test.ts @@ -95,6 +95,13 @@ describe('createSpanEnvelope', () => { client = new TestClient(options); setCurrentClient(client); client.init(); + + // We want to avoid console errors in the tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.resetAllMocks(); }); it('creates a span envelope', () => { diff --git a/packages/core/test/lib/metrics/browser-aggregator.test.ts b/packages/core/test/lib/metrics/browser-aggregator.test.ts index 669959a03e05..e5ed6b3f8296 100644 --- a/packages/core/test/lib/metrics/browser-aggregator.test.ts +++ b/packages/core/test/lib/metrics/browser-aggregator.test.ts @@ -3,6 +3,10 @@ import { CounterMetric } from '../../../src/metrics/instance'; import { serializeMetricBuckets } from '../../../src/metrics/utils'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; +function _cleanupAggregator(aggregator: BrowserMetricsAggregator): void { + clearInterval(aggregator['_interval']); +} + describe('BrowserMetricsAggregator', () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); const testClient = new TestClient(options); @@ -21,6 +25,8 @@ describe('BrowserMetricsAggregator', () => { timestamp: expect.any(Number), unit: 'none', }); + + _cleanupAggregator(aggregator); }); it('groups same items together', () => { @@ -40,6 +46,8 @@ describe('BrowserMetricsAggregator', () => { unit: 'none', }); expect(firstValue.metric._value).toEqual(2); + + _cleanupAggregator(aggregator); }); it('differentiates based on tag value', () => { @@ -48,6 +56,8 @@ describe('BrowserMetricsAggregator', () => { expect(aggregator['_buckets'].size).toEqual(1); aggregator.add('g', 'cpu', 55, undefined, { a: 'value' }); expect(aggregator['_buckets'].size).toEqual(2); + + _cleanupAggregator(aggregator); }); describe('serializeBuckets', () => { @@ -69,6 +79,8 @@ describe('BrowserMetricsAggregator', () => { expect(serializedBuckets).toContain('cpu@none:52:50:55:157:3|g|T'); expect(serializedBuckets).toContain('lcp@second:1:1.2|d|#a:value,b:anothervalue|T'); expect(serializedBuckets).toContain('important_people@none:97:98|s|#numericKey:2|T'); + + _cleanupAggregator(aggregator); }); }); }); diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index 05aa992f39e6..3a33130a3220 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -34,6 +34,9 @@ describe('applyTunnelRouteOption()', () => { }); it("Doesn't apply `tunnelRoute` when DSN is invalid", () => { + // Avoid polluting the test output with error messages + const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route'; const options: any = { dsn: 'invalidDsn', @@ -42,6 +45,8 @@ describe('applyTunnelRouteOption()', () => { applyTunnelRouteOption(options); expect(options.tunnel).toBeUndefined(); + + mockConsoleError.mockRestore(); }); it("Doesn't apply `tunnelRoute` option when `tunnelRoute` option wasn't injected", () => { From 100c6628f5129f1659aad78c87cab06bb838bd10 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 19 Nov 2024 12:26:03 +0100 Subject: [PATCH 28/29] meta(utils): Don't use import assertion in rollup config (#14362) --- packages/utils/rollup.npm.config.mjs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/utils/rollup.npm.config.mjs b/packages/utils/rollup.npm.config.mjs index 8e219b3c2d9b..cc3ad4064820 100644 --- a/packages/utils/rollup.npm.config.mjs +++ b/packages/utils/rollup.npm.config.mjs @@ -1,6 +1,18 @@ +// @ts-check + +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; import replace from '@rollup/plugin-replace'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -import packageJson from './package.json' with { type: 'json' }; + +const packageJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf-8')); + +if (!packageJson.version) { + throw new Error('invariant: package version not found'); +} + +const packageVersion = packageJson.version; export default makeNPMConfigVariants( makeBaseNPMConfig({ @@ -18,7 +30,7 @@ export default makeNPMConfigVariants( replace({ preventAssignment: true, values: { - __SENTRY_SDK_VERSION__: JSON.stringify(packageJson.version), + __SENTRY_SDK_VERSION__: JSON.stringify(packageVersion), }, }), ], From 8659f8d0e9ed827d63c267329c3a90decf8ba1c5 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 19 Nov 2024 10:07:05 +0000 Subject: [PATCH 29/29] meta(changelog): Update changelog for 8.39.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23375eb92a2b..1eea88c2fc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,31 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.39.0 + +### Important Changes + +- **feat(nestjs): Instrument event handlers ([#14307](https://github.com/getsentry/sentry-javascript/pull/14307))** + +The `@sentry/nestjs` SDK will now capture performance data for [NestJS Events (`@nestjs/event-emitter`)](https://docs.nestjs.com/techniques/events) + +### Other Changes + +- feat(nestjs): Add alias `@SentryExceptionCaptured` for `@WithSentry` ([#14322](https://github.com/getsentry/sentry-javascript/pull/14322)) +- feat(nestjs): Duplicate `SentryService` behaviour into `@sentry/nestjs` SDK `init()` ([#14321](https://github.com/getsentry/sentry-javascript/pull/14321)) +- feat(nestjs): Handle GraphQL contexts in `SentryGlobalFilter` ([#14320](https://github.com/getsentry/sentry-javascript/pull/14320)) +- feat(node): Add alias `childProcessIntegration` for `processThreadBreadcrumbIntegration` and deprecate it ([#14334](https://github.com/getsentry/sentry-javascript/pull/14334)) +- feat(node): Ensure request bodies are reliably captured for http requests ([#13746](https://github.com/getsentry/sentry-javascript/pull/13746)) +- feat(replay): Upgrade rrweb packages to 2.29.0 ([#14160](https://github.com/getsentry/sentry-javascript/pull/14160)) +- fix(cdn): Ensure `_sentryModuleMetadata` is not mangled ([#14344](https://github.com/getsentry/sentry-javascript/pull/14344)) +- fix(core): Set `sentry.source` attribute to `custom` when calling `span.updateName` on `SentrySpan` ([#14251](https://github.com/getsentry/sentry-javascript/pull/14251)) +- fix(mongo): rewrite Buffer as ? during serialization ([#14071](https://github.com/getsentry/sentry-javascript/pull/14071)) +- fix(replay): Remove replay id from DSC on expired sessions ([#14342](https://github.com/getsentry/sentry-javascript/pull/14342)) +- ref(profiling) Fix electron crash ([#14216](https://github.com/getsentry/sentry-javascript/pull/14216)) +- ref(types): Deprecate `Request` type in favor of `RequestEventData` ([#14317](https://github.com/getsentry/sentry-javascript/pull/14317)) +- ref(utils): Stop setting `transaction` in `requestDataIntegration` ([#14306](https://github.com/getsentry/sentry-javascript/pull/14306)) +- ref(vue): Reduce bundle size for starting application render span ([#14275](https://github.com/getsentry/sentry-javascript/pull/14275)) + ## 8.38.0 - docs: Improve docstrings for node otel integrations ([#14217](https://github.com/getsentry/sentry-javascript/pull/14217))