diff --git a/packages/hub/src/exports.ts b/packages/hub/src/exports.ts index 528da6289d3e..0fb2345fd3ec 100644 --- a/packages/hub/src/exports.ts +++ b/packages/hub/src/exports.ts @@ -168,6 +168,9 @@ export function withScope(callback: (scope: Scope) => void): ReturnType { - return getCurrentHub().startTransaction({ ...context }, customSamplingContext); + return getCurrentHub().startTransaction( + { + metadata: { source: 'custom' }, + ...context, + }, + customSamplingContext, + ); } diff --git a/packages/hub/test/exports.test.ts b/packages/hub/test/exports.test.ts index 91b7450b66eb..b6e688e3df00 100644 --- a/packages/hub/test/exports.test.ts +++ b/packages/hub/test/exports.test.ts @@ -10,6 +10,7 @@ import { setTag, setTags, setUser, + startTransaction, withScope, } from '../src/exports'; @@ -184,6 +185,35 @@ describe('Top Level API', () => { }); }); + describe('startTransaction', () => { + beforeEach(() => { + global.__SENTRY__ = { + hub: undefined, + extensions: { + startTransaction: (context: any) => ({ + name: context.name, + // Spread rather than assign in case it's undefined + metadata: { ...context.metadata }, + }), + }, + }; + }); + + it("sets source to `'custom'` if no source provided", () => { + const transaction = startTransaction({ name: 'dogpark' }); + + expect(transaction.name).toEqual('dogpark'); + expect(transaction.metadata.source).toEqual('custom'); + }); + + it('uses given `source` value', () => { + const transaction = startTransaction({ name: 'dogpark', metadata: { source: 'route' } }); + + expect(transaction.name).toEqual('dogpark'); + expect(transaction.metadata.source).toEqual('route'); + }); + }); + test('Clear Scope', () => { const client: any = new TestClient({}); getCurrentHub().withScope(() => { diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 662fbf775ebd..0b0dd7c18790 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -1,4 +1,4 @@ -import { captureException, getCurrentHub, startTransaction } from '@sentry/node'; +import { captureException, getCurrentHub } from '@sentry/node'; import { getActiveTransaction } from '@sentry/tracing'; import { addExceptionMechanism, fill, loadModule, logger, stripUrlQueryAndFragment } from '@sentry/utils'; @@ -175,8 +175,9 @@ function makeWrappedLoader(origAction: DataFunction): DataFunction { function wrapRequestHandler(origRequestHandler: RequestHandler): RequestHandler { return async function (this: unknown, request: Request, loadContext?: unknown): Promise { - const currentScope = getCurrentHub().getScope(); - const transaction = startTransaction({ + const hub = getCurrentHub(); + const currentScope = hub.getScope(); + const transaction = hub.startTransaction({ name: stripUrlQueryAndFragment(request.url), op: 'http.server', tags: { diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index 826bc34ab29c..50350dd922da 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -1,14 +1,6 @@ /* eslint-disable max-lines */ import * as Sentry from '@sentry/node'; -import { - captureException, - captureMessage, - flush, - getCurrentHub, - Scope, - startTransaction, - withScope, -} from '@sentry/node'; +import { captureException, captureMessage, flush, getCurrentHub, Scope, withScope } from '@sentry/node'; import { extractTraceparentData } from '@sentry/tracing'; import { Integration } from '@sentry/types'; import { dsnFromString, dsnToString, isString, logger, parseBaggageSetMutability } from '@sentry/utils'; @@ -320,14 +312,15 @@ export function wrapHandler( eventWithHeaders.headers && isString(eventWithHeaders.headers.baggage) && eventWithHeaders.headers.baggage; const baggage = parseBaggageSetMutability(rawBaggageString, traceparentData); - const transaction = startTransaction({ + const hub = getCurrentHub(); + + const transaction = hub.startTransaction({ name: context.functionName, op: 'awslambda.handler', ...traceparentData, metadata: { baggage, source: 'component' }, }); - const hub = getCurrentHub(); const scope = hub.pushScope(); let rv: TResult; try { diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 605a3a486dd4..168e138f02ba 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -1,4 +1,4 @@ -import { captureException, flush, getCurrentHub, startTransaction } from '@sentry/node'; +import { captureException, flush, getCurrentHub } from '@sentry/node'; import { logger } from '@sentry/utils'; import { domainify, getActiveDomain, proxyFunction } from '../utils'; @@ -30,7 +30,9 @@ function _wrapCloudEventFunction( ...wrapOptions, }; return (context, callback) => { - const transaction = startTransaction({ + const hub = getCurrentHub(); + + const transaction = hub.startTransaction({ name: context.type || '', op: 'gcp.function.cloud_event', metadata: { source: 'component' }, @@ -39,7 +41,7 @@ function _wrapCloudEventFunction( // getCurrentHub() is expected to use current active domain as a carrier // since functions-framework creates a domain for each incoming request. // So adding of event processors every time should not lead to memory bloat. - getCurrentHub().configureScope(scope => { + hub.configureScope(scope => { scope.setContext('gcp.function.context', { ...context }); // We put the transaction on the scope so users can attach children to it scope.setSpan(transaction); diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index 9d13f18386e0..2b8606fa6997 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -1,4 +1,4 @@ -import { captureException, flush, getCurrentHub, startTransaction } from '@sentry/node'; +import { captureException, flush, getCurrentHub } from '@sentry/node'; import { logger } from '@sentry/utils'; import { domainify, getActiveDomain, proxyFunction } from '../utils'; @@ -30,7 +30,9 @@ function _wrapEventFunction( ...wrapOptions, }; return (data, context, callback) => { - const transaction = startTransaction({ + const hub = getCurrentHub(); + + const transaction = hub.startTransaction({ name: context.eventType, op: 'gcp.function.event', metadata: { source: 'component' }, @@ -39,7 +41,7 @@ function _wrapEventFunction( // getCurrentHub() is expected to use current active domain as a carrier // since functions-framework creates a domain for each incoming request. // So adding of event processors every time should not lead to memory bloat. - getCurrentHub().configureScope(scope => { + hub.configureScope(scope => { scope.setContext('gcp.function.context', { ...context }); // We put the transaction on the scope so users can attach children to it scope.setSpan(transaction); diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index fd3af7b08ac2..99cec2811fae 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -4,7 +4,6 @@ import { captureException, flush, getCurrentHub, - startTransaction, } from '@sentry/node'; import { extractTraceparentData } from '@sentry/tracing'; import { isString, logger, parseBaggageSetMutability, stripUrlQueryAndFragment } from '@sentry/utils'; @@ -83,7 +82,9 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial { + hub.configureScope(scope => { scope.addEventProcessor(event => addRequestDataToEvent(event, req, options.addRequestDataToEventOptions)); // We put the transaction on the scope so users can attach children to it scope.setSpan(transaction); diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index 14043efdd42c..7c544c14fcda 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -11,6 +11,7 @@ export const fakeHub = { pushScope: jest.fn(() => fakeScope), popScope: jest.fn(), getScope: jest.fn(() => fakeScope), + startTransaction: jest.fn(context => ({ ...fakeTransaction, ...context })), }; export const fakeScope = { addEventProcessor: jest.fn(), diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index d6d72859ceff..2f2068e1ccba 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -38,9 +38,11 @@ const fakeCallback: Callback = (err, result) => { return err; }; -function expectScopeSettings() { +function expectScopeSettings(fakeTransactionContext: any) { // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); // @ts-ignore see "Why @ts-ignore" note @@ -186,13 +188,17 @@ describe('AWSLambda', () => { }; const wrappedHandler = wrapHandler(handler); const rv = await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(rv).toStrictEqual(42); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'functionName', op: 'awslambda.handler', metadata: { baggage: [{}, '', true], source: 'component' }, - }); - expectScopeSettings(); + }; + + expect(rv).toStrictEqual(42); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + expectScopeSettings(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -210,12 +216,15 @@ describe('AWSLambda', () => { try { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { - expect(Sentry.startTransaction).toBeCalledWith({ + const fakeTransactionContext = { name: 'functionName', op: 'awslambda.handler', metadata: { baggage: [{}, '', true], source: 'component' }, - }); - expectScopeSettings(); + }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + expectScopeSettings(fakeTransactionContext); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -244,7 +253,8 @@ describe('AWSLambda', () => { }; const handler: Handler = (_event, _context, callback) => { - expect(Sentry.startTransaction).toBeCalledWith( + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith( expect.objectContaining({ parentSpanId: '1121201211212012', parentSampled: false, @@ -284,15 +294,18 @@ describe('AWSLambda', () => { fakeEvent.headers = { 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0' }; await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { - expect(Sentry.startTransaction).toBeCalledWith({ + const fakeTransactionContext = { name: 'functionName', op: 'awslambda.handler', traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, metadata: { baggage: [{}, '', false], source: 'component' }, - }); - expectScopeSettings(); + }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + expectScopeSettings(fakeTransactionContext); expect(Sentry.captureException).toBeCalledWith(e); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -310,13 +323,17 @@ describe('AWSLambda', () => { }; const wrappedHandler = wrapHandler(handler); const rv = await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(rv).toStrictEqual(42); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'functionName', op: 'awslambda.handler', metadata: { baggage: [{}, '', true], source: 'component' }, - }); - expectScopeSettings(); + }; + + expect(rv).toStrictEqual(42); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + expectScopeSettings(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalled(); @@ -345,12 +362,15 @@ describe('AWSLambda', () => { try { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { - expect(Sentry.startTransaction).toBeCalledWith({ + const fakeTransactionContext = { name: 'functionName', op: 'awslambda.handler', metadata: { baggage: [{}, '', true], source: 'component' }, - }); - expectScopeSettings(); + }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + expectScopeSettings(fakeTransactionContext); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -383,13 +403,17 @@ describe('AWSLambda', () => { }; const wrappedHandler = wrapHandler(handler); const rv = await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(rv).toStrictEqual(42); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'functionName', op: 'awslambda.handler', metadata: { baggage: [{}, '', true], source: 'component' }, - }); - expectScopeSettings(); + }; + + expect(rv).toStrictEqual(42); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + expectScopeSettings(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalled(); @@ -418,12 +442,15 @@ describe('AWSLambda', () => { try { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { - expect(Sentry.startTransaction).toBeCalledWith({ + const fakeTransactionContext = { name: 'functionName', op: 'awslambda.handler', metadata: { baggage: [{}, '', true], source: 'component' }, - }); - expectScopeSettings(); + }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + expectScopeSettings(fakeTransactionContext); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 4e54be9d21ca..9f1498714d0e 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -110,13 +110,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapHttpFunction(handler); await handleHttp(wrappedHandler); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'POST /path', op: 'gcp.function.http', metadata: { baggage: [{}, '', true], source: 'route' }, - }); + }; // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.setHttpStatus).toBeCalledWith(200); // @ts-ignore see "Why @ts-ignore" note @@ -138,25 +144,29 @@ describe('GCPFunction', () => { }; await handleHttp(wrappedHandler, traceHeaders); - expect(Sentry.startTransaction).toBeCalledWith( - expect.objectContaining({ - name: 'POST /path', - op: 'gcp.function.http', - traceId: '12312012123120121231201212312012', - parentSpanId: '1121201211212012', - parentSampled: false, - metadata: { - baggage: [ - { - release: '2.12.1', - }, - '', - false, - ], - source: 'route', - }, - }), - ); + const fakeTransactionContext = { + name: 'POST /path', + op: 'gcp.function.http', + traceId: '12312012123120121231201212312012', + parentSpanId: '1121201211212012', + parentSampled: false, + metadata: { + baggage: [ + { + release: '2.12.1', + }, + '', + false, + ], + source: 'route', + }, + }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + + // @ts-ignore see "Why @ts-ignore" note + // expect(Sentry.fakeHub.startTransaction).toBeCalledWith(expect.objectContaining(fakeTransactionContext)); }); test('capture error', async () => { @@ -173,16 +183,22 @@ describe('GCPFunction', () => { }; await handleHttp(wrappedHandler, trace_headers); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'POST /path', op: 'gcp.function.http', traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, metadata: { baggage: [{}, '', false], source: 'route' }, - }); + }; + // @ts-ignore see "Why @ts-ignore" note + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -246,13 +262,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapEventFunction(func); await expect(handleEvent(wrappedHandler)).resolves.toBe(42); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.event', metadata: { source: 'component' }, - }); + }; + // @ts-ignore see "Why @ts-ignore" note + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -267,13 +289,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapEventFunction(handler); await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.event', metadata: { source: 'component' }, - }); + }; + // @ts-ignore see "Why @ts-ignore" note + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -293,13 +321,19 @@ describe('GCPFunction', () => { }); const wrappedHandler = wrapEventFunction(func); await expect(handleEvent(wrappedHandler)).resolves.toBe(42); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.event', metadata: { source: 'component' }, - }); + }; // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -318,13 +352,19 @@ describe('GCPFunction', () => { const wrappedHandler = wrapEventFunction(handler); await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.event', metadata: { source: 'component' }, - }); + }; // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -341,13 +381,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapEventFunction(func); await expect(handleEvent(wrappedHandler)).resolves.toBe(42); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.event', metadata: { source: 'component' }, - }); + }; // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -362,13 +408,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapEventFunction(handler); await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.event', metadata: { source: 'component' }, - }); + }; + // @ts-ignore see "Why @ts-ignore" note + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -384,13 +436,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapEventFunction(handler); await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.event', metadata: { source: 'component' }, - }); + }; + // @ts-ignore see "Why @ts-ignore" note + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); }); }); @@ -417,13 +475,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapCloudEventFunction(func); await expect(handleCloudEvent(wrappedHandler)).resolves.toBe(42); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.cloud_event', metadata: { source: 'component' }, - }); + }; + // @ts-ignore see "Why @ts-ignore" note + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -438,13 +502,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapCloudEventFunction(handler); await expect(handleCloudEvent(wrappedHandler)).rejects.toThrowError(error); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.cloud_event', metadata: { source: 'component' }, - }); + }; + // @ts-ignore see "Why @ts-ignore" note + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -461,13 +531,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapCloudEventFunction(func); await expect(handleCloudEvent(wrappedHandler)).resolves.toBe(42); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.cloud_event', metadata: { source: 'component' }, - }); + }; + // @ts-ignore see "Why @ts-ignore" note + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -482,13 +558,19 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapCloudEventFunction(handler); await expect(handleCloudEvent(wrappedHandler)).rejects.toThrowError(error); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.cloud_event', metadata: { source: 'component' }, - }); + }; // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -504,13 +586,20 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapCloudEventFunction(handler); await expect(handleCloudEvent(wrappedHandler)).rejects.toThrowError(error); - expect(Sentry.startTransaction).toBeCalledWith({ + + const fakeTransactionContext = { name: 'event.type', op: 'gcp.function.cloud_event', metadata: { source: 'component' }, - }); + }; + // @ts-ignore see "Why @ts-ignore" note + const fakeTransaction = { ...Sentry.fakeTransaction, ...fakeTransactionContext }; + + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(fakeTransaction); + expect(Sentry.captureException).toBeCalledWith(error); }); }); diff --git a/packages/tracing/src/browser/browsertracing.ts b/packages/tracing/src/browser/browsertracing.ts index ee927cf8a779..089245bfd31b 100644 --- a/packages/tracing/src/browser/browsertracing.ts +++ b/packages/tracing/src/browser/browsertracing.ts @@ -222,6 +222,12 @@ export class BrowserTracing implements Integration { // from being sent to Sentry). const finalContext = modifiedContext === undefined ? { ...expandedContext, sampled: false } : modifiedContext; + // If `beforeNavigate` set a custom name, record that fact + finalContext.metadata = + finalContext.name !== expandedContext.name + ? { ...finalContext.metadata, source: 'custom' } + : finalContext.metadata; + if (finalContext.sampled === false) { __DEBUG_BUILD__ && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`); diff --git a/packages/tracing/src/transaction.ts b/packages/tracing/src/transaction.ts index eb64f035d57b..10435f5ecdb0 100644 --- a/packages/tracing/src/transaction.ts +++ b/packages/tracing/src/transaction.ts @@ -15,8 +15,6 @@ import { Span as SpanClass, SpanRecorder } from './span'; /** JSDoc */ export class Transaction extends SpanClass implements TransactionInterface { - public name: string; - public metadata: TransactionMetadata; /** @@ -24,6 +22,8 @@ export class Transaction extends SpanClass implements TransactionInterface { */ public readonly _hub: Hub; + private _name: string; + private _measurements: Measurements = {}; private _trimEnd?: boolean; @@ -40,7 +40,7 @@ export class Transaction extends SpanClass implements TransactionInterface { this._hub = hub || getCurrentHub(); - this.name = transactionContext.name || ''; + this._name = transactionContext.name || ''; this.metadata = transactionContext.metadata || {}; this._trimEnd = transactionContext.trimEnd; @@ -49,11 +49,23 @@ export class Transaction extends SpanClass implements TransactionInterface { this.transaction = this; } + /** Getter for `name` property */ + public get name(): string { + return this._name; + } + + /** Setter for `name` property, which also sets `source` */ + public set name(newName: string) { + this._name = newName; + this.metadata.source = 'custom'; + } + /** * JSDoc */ - public setName(name: string): void { + public setName(name: string, source: TransactionMetadata['source'] = 'custom'): void { this.name = name; + this.metadata.source = source; } /** diff --git a/packages/tracing/test/browser/browsertracing.test.ts b/packages/tracing/test/browser/browsertracing.test.ts index 466f0cfa78ee..acee76d0c966 100644 --- a/packages/tracing/test/browser/browsertracing.test.ts +++ b/packages/tracing/test/browser/browsertracing.test.ts @@ -216,6 +216,39 @@ describe('BrowserTracing', () => { expect(mockBeforeNavigation).toHaveBeenCalledTimes(1); }); + + it("sets transaction name source to `'custom'` if name is changed", () => { + const mockBeforeNavigation = jest.fn(ctx => ({ + ...ctx, + name: 'newName', + })); + createBrowserTracing(true, { + beforeNavigate: mockBeforeNavigation, + routingInstrumentation: customInstrumentRouting, + }); + const transaction = getActiveTransaction(hub) as IdleTransaction; + expect(transaction).toBeDefined(); + expect(transaction.name).toBe('newName'); + expect(transaction.metadata.source).toBe('custom'); + + expect(mockBeforeNavigation).toHaveBeenCalledTimes(1); + }); + + it("doesn't set transaction name source if name is not changed", () => { + const mockBeforeNavigation = jest.fn(ctx => ({ + ...ctx, + })); + createBrowserTracing(true, { + beforeNavigate: mockBeforeNavigation, + routingInstrumentation: customInstrumentRouting, + }); + const transaction = getActiveTransaction(hub) as IdleTransaction; + expect(transaction).toBeDefined(); + expect(transaction.name).toBe('a/path'); + expect(transaction.metadata.source).toBeUndefined(); + + expect(mockBeforeNavigation).toHaveBeenCalledTimes(1); + }); }); it('sets transaction context from sentry-trace header', () => { diff --git a/packages/tracing/test/transaction.test.ts b/packages/tracing/test/transaction.test.ts new file mode 100644 index 000000000000..d47971679a71 --- /dev/null +++ b/packages/tracing/test/transaction.test.ts @@ -0,0 +1,45 @@ +import { Transaction } from '../src/transaction'; + +describe('`Transaction` class', () => { + describe('transaction name source', () => { + it('sets source in constructor if provided', () => { + const transaction = new Transaction({ name: 'dogpark', metadata: { source: 'route' } }); + + expect(transaction.name).toEqual('dogpark'); + expect(transaction.metadata.source).toEqual('route'); + }); + + it("doesn't set source in constructor if not provided", () => { + const transaction = new Transaction({ name: 'dogpark' }); + + expect(transaction.name).toEqual('dogpark'); + expect(transaction.metadata.source).toBeUndefined(); + }); + + it("sets source to `'custom'` when assigning to `name` property", () => { + const transaction = new Transaction({ name: 'dogpark' }); + transaction.name = 'ballpit'; + + expect(transaction.name).toEqual('ballpit'); + expect(transaction.metadata.source).toEqual('custom'); + }); + + describe('`setName` method', () => { + it("sets source to `'custom'` if no source provided", () => { + const transaction = new Transaction({ name: 'dogpark' }); + transaction.setName('ballpit'); + + expect(transaction.name).toEqual('ballpit'); + expect(transaction.metadata.source).toEqual('custom'); + }); + + it('uses given `source` value', () => { + const transaction = new Transaction({ name: 'dogpark' }); + transaction.setName('ballpit', 'route'); + + expect(transaction.name).toEqual('ballpit'); + expect(transaction.metadata.source).toEqual('route'); + }); + }); + }); +});