diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index c531ef1f41e3..be6350b04fae 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -4,14 +4,12 @@ import { flush, getCurrentHub, Scope, - SDK_VERSION, Severity, startTransaction, withScope, } from '@sentry/node'; import * as Sentry from '@sentry/node'; import { Integration } from '@sentry/types'; -import { addExceptionMechanism } from '@sentry/utils'; // NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil // eslint-disable-next-line import/no-unresolved import { Context, Handler } from 'aws-lambda'; @@ -20,6 +18,7 @@ import { performance } from 'perf_hooks'; import { types } from 'util'; import { AWSServices } from './awsservices'; +import { serverlessEventProcessor } from './utils'; export * from '@sentry/node'; @@ -54,37 +53,8 @@ export function init(options: Sentry.NodeOptions = {}): void { if (options.defaultIntegrations === undefined) { options.defaultIntegrations = defaultIntegrations; } - return Sentry.init(options); -} - -/** - * Add event processor that will override SDK details to point to the serverless SDK instead of Node, - * as well as set correct mechanism type, which should be set to `handled: false`. - * We do it like this, so that we don't introduce any side-effects in this module, which makes it tree-shakeable. - * @param scope Scope that processor should be added to - */ -function addServerlessEventProcessor(scope: Scope): void { - scope.addEventProcessor(event => { - event.sdk = { - ...event.sdk, - name: 'sentry.javascript.serverless', - integrations: [...((event.sdk && event.sdk.integrations) || []), 'AWSLambda'], - packages: [ - ...((event.sdk && event.sdk.packages) || []), - { - name: 'npm:@sentry/serverless', - version: SDK_VERSION, - }, - ], - version: SDK_VERSION, - }; - - addExceptionMechanism(event, { - handled: false, - }); - - return event; - }); + Sentry.init(options); + Sentry.addGlobalEventProcessor(serverlessEventProcessor('AWSLambda')); } /** @@ -125,20 +95,6 @@ function enhanceScopeWithEnvironmentData(scope: Scope, context: Context): void { }); } -/** - * Capture exception with a a context. - * - * @param e exception to be captured - * @param context Context - */ -function captureExceptionWithContext(e: unknown, context: Context): void { - withScope(scope => { - addServerlessEventProcessor(scope); - enhanceScopeWithEnvironmentData(scope, context); - captureException(e); - }); -} - /** * Wraps a lambda handler adding it error capture and tracing capabilities. * @@ -205,8 +161,6 @@ export function wrapHandler( timeoutWarningTimer = setTimeout(() => { withScope(scope => { - addServerlessEventProcessor(scope); - enhanceScopeWithEnvironmentData(scope, context); scope.setTag('timeout', humanReadableTimeout); captureMessage(`Possible function timeout: ${context.functionName}`, Severity.Warning); }); @@ -217,22 +171,24 @@ export function wrapHandler( name: context.functionName, op: 'awslambda.handler', }); - // We put the transaction on the scope so users can attach children to it - getCurrentHub().configureScope(scope => { - scope.setSpan(transaction); - }); + const hub = getCurrentHub(); + const scope = hub.pushScope(); let rv: TResult | undefined; try { + enhanceScopeWithEnvironmentData(scope, context); + // We put the transaction on the scope so users can attach children to it + scope.setSpan(transaction); rv = await asyncHandler(event, context); } catch (e) { - captureExceptionWithContext(e, context); + captureException(e); if (options.rethrowAfterCapture) { throw e; } } finally { clearTimeout(timeoutWarningTimer); transaction.finish(); + hub.popScope(); await flush(options.flushTimeout); } return rv; diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 9b9844d28940..def1323d6ce8 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -4,10 +4,10 @@ import { CloudEventFunction, CloudEventFunctionWithCallback, } from '@google-cloud/functions-framework/build/src/functions'; -import { flush, getCurrentHub, startTransaction } from '@sentry/node'; +import { captureException, flush, getCurrentHub, startTransaction } from '@sentry/node'; import { logger } from '@sentry/utils'; -import { captureEventError, getActiveDomain, WrapperOptions } from './general'; +import { configureScopeWithContext, getActiveDomain, WrapperOptions } from './general'; export type CloudEventFunctionWrapperOptions = WrapperOptions; @@ -32,20 +32,22 @@ export function wrapCloudEventFunction( op: 'gcp.function.cloud_event', }); - // We put the transaction on the scope so users can attach children to it + // 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 => { + configureScopeWithContext(scope, context); + // We put the transaction on the scope so users can attach children to it scope.setSpan(transaction); }); const activeDomain = getActiveDomain(); - activeDomain.on('error', err => { - captureEventError(err, context); - }); + activeDomain.on('error', captureException); const newCallback = activeDomain.bind((...args: unknown[]) => { if (args[0] !== null && args[0] !== undefined) { - captureEventError(args[0], context); + captureException(args[0]); } transaction.finish(); diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index 80bd3a461bc4..7693f5aed34b 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -1,10 +1,10 @@ // '@google-cloud/functions-framework/build/src/functions' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. import { EventFunction, EventFunctionWithCallback } from '@google-cloud/functions-framework/build/src/functions'; -import { flush, getCurrentHub, startTransaction } from '@sentry/node'; +import { captureException, flush, getCurrentHub, startTransaction } from '@sentry/node'; import { logger } from '@sentry/utils'; -import { captureEventError, getActiveDomain, WrapperOptions } from './general'; +import { configureScopeWithContext, getActiveDomain, WrapperOptions } from './general'; export type EventFunctionWrapperOptions = WrapperOptions; @@ -29,20 +29,22 @@ export function wrapEventFunction( op: 'gcp.function.event', }); - // We put the transaction on the scope so users can attach children to it + // 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 => { + configureScopeWithContext(scope, context); + // We put the transaction on the scope so users can attach children to it scope.setSpan(transaction); }); const activeDomain = getActiveDomain(); - activeDomain.on('error', err => { - captureEventError(err, context); - }); + activeDomain.on('error', captureException); const newCallback = activeDomain.bind((...args: unknown[]) => { if (args[0] !== null && args[0] !== undefined) { - captureEventError(args[0], context); + captureException(args[0]); } transaction.finish(); diff --git a/packages/serverless/src/gcpfunction/general.ts b/packages/serverless/src/gcpfunction/general.ts index 8d61f89d9c8b..fac009202fab 100644 --- a/packages/serverless/src/gcpfunction/general.ts +++ b/packages/serverless/src/gcpfunction/general.ts @@ -1,9 +1,8 @@ // '@google-cloud/functions-framework/build/src/functions' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. import { Context } from '@google-cloud/functions-framework/build/src/functions'; -import { captureException, Scope, SDK_VERSION, withScope } from '@sentry/node'; +import { Scope } from '@sentry/node'; import { Context as SentryContext } from '@sentry/types'; -import { addExceptionMechanism } from '@sentry/utils'; import * as domain from 'domain'; import { hostname } from 'os'; @@ -12,52 +11,18 @@ export interface WrapperOptions { } /** - * Capture exception with additional event information. + * Enhances the scope with additional event information. * - * @param e exception to be captured + * @param scope scope * @param context event context */ -export function captureEventError(e: unknown, context: Context): void { - withScope(scope => { - addServerlessEventProcessor(scope); - scope.setContext('runtime', { - name: 'node', - version: global.process.version, - }); - scope.setTag('server_name', process.env.SENTRY_NAME || hostname()); - scope.setContext('gcp.function.context', { ...context } as SentryContext); - captureException(e); - }); -} - -/** - * Add event processor that will override SDK details to point to the serverless SDK instead of Node, - * as well as set correct mechanism type, which should be set to `handled: false`. - * We do it like this, so that we don't introduce any side-effects in this module, which makes it tree-shakeable. - * @param scope Scope that processor should be added to - */ -export function addServerlessEventProcessor(scope: Scope): void { - scope.addEventProcessor(event => { - event.sdk = { - ...event.sdk, - name: 'sentry.javascript.serverless', - integrations: [...((event.sdk && event.sdk.integrations) || []), 'GCPFunction'], - packages: [ - ...((event.sdk && event.sdk.packages) || []), - { - name: 'npm:@sentry/serverless', - version: SDK_VERSION, - }, - ], - version: SDK_VERSION, - }; - - addExceptionMechanism(event, { - handled: false, - }); - - return event; +export function configureScopeWithContext(scope: Scope, context: Context): void { + scope.setContext('runtime', { + name: 'node', + version: global.process.version, }); + scope.setTag('server_name', process.env.SENTRY_NAME || hostname()); + scope.setContext('gcp.function.context', { ...context } as SentryContext); } /** diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 08f178f35c72..33e9b74dde11 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -1,10 +1,10 @@ // '@google-cloud/functions-framework/build/src/functions' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. import { HttpFunction } from '@google-cloud/functions-framework/build/src/functions'; -import { captureException, flush, getCurrentHub, Handlers, startTransaction, withScope } from '@sentry/node'; +import { captureException, flush, getCurrentHub, Handlers, startTransaction } from '@sentry/node'; import { logger, stripUrlQueryAndFragment } from '@sentry/utils'; -import { addServerlessEventProcessor, getActiveDomain, WrapperOptions } from './general'; +import { getActiveDomain, WrapperOptions } from './general'; type Request = Parameters[0]; type Response = Parameters[1]; @@ -18,21 +18,6 @@ export { Request, Response }; const { parseRequest } = Handlers; -/** - * Capture exception with additional request information. - * - * @param e exception to be captured - * @param req incoming request - * @param options request capture options - */ -function captureRequestError(e: unknown, req: Request, options: ParseRequestOptions): void { - withScope(scope => { - addServerlessEventProcessor(scope); - scope.addEventProcessor(event => parseRequest(event, req, options)); - captureException(e); - }); -} - /** * Wraps an HTTP function handler adding it error capture and tracing capabilities. * @@ -58,8 +43,12 @@ export function wrapHttpFunction( op: 'gcp.function.http', }); - // We put the transaction on the scope so users can attach children to it + // 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 => { + scope.addEventProcessor(event => parseRequest(event, req, options.parseRequestOptions)); + // We put the transaction on the scope so users can attach children to it scope.setSpan(transaction); }); @@ -71,7 +60,7 @@ export function wrapHttpFunction( // functions-framework creates a domain for each incoming request so we take advantage of this fact and add an error handler. // BTW this is the only way to catch any exception occured during request lifecycle. getActiveDomain().on('error', err => { - captureRequestError(err, req, options.parseRequestOptions); + captureException(err); }); // eslint-disable-next-line @typescript-eslint/unbound-method diff --git a/packages/serverless/src/gcpfunction/index.ts b/packages/serverless/src/gcpfunction/index.ts index 0db5992320c0..5672f0336a18 100644 --- a/packages/serverless/src/gcpfunction/index.ts +++ b/packages/serverless/src/gcpfunction/index.ts @@ -1,4 +1,15 @@ +import * as Sentry from '@sentry/node'; + +import { serverlessEventProcessor } from '../utils'; + export * from './http'; export * from './events'; export * from './cloud_events'; -export { init } from '@sentry/node'; + +/** + * @see {@link Sentry.init} + */ +export function init(options: Sentry.NodeOptions = {}): void { + Sentry.init(options); + Sentry.addGlobalEventProcessor(serverlessEventProcessor('GCPFunction')); +} diff --git a/packages/serverless/src/utils.ts b/packages/serverless/src/utils.ts new file mode 100644 index 000000000000..2d21e1d0a527 --- /dev/null +++ b/packages/serverless/src/utils.ts @@ -0,0 +1,33 @@ +import { Event, SDK_VERSION } from '@sentry/node'; +import { addExceptionMechanism } from '@sentry/utils'; + +/** + * Event processor that will override SDK details to point to the serverless SDK instead of Node, + * as well as set correct mechanism type, which should be set to `handled: false`. + * We do it like this, so that we don't introduce any side-effects in this module, which makes it tree-shakeable. + * @param event Event + * @param integration Name of the serverless integration ('AWSLambda', 'GCPFunction', etc) + */ +export function serverlessEventProcessor(integration: string): (event: Event) => Event { + return event => { + event.sdk = { + ...event.sdk, + name: 'sentry.javascript.serverless', + integrations: [...((event.sdk && event.sdk.integrations) || []), integration], + packages: [ + ...((event.sdk && event.sdk.packages) || []), + { + name: 'npm:@sentry/serverless', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }; + + addExceptionMechanism(event, { + handled: false, + }); + + return event; + }; +} diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index b364796956ba..769d22f6e2f6 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -5,19 +5,19 @@ export const SDK_VERSION = '6.6.6'; export const Severity = { Warning: 'warning', }; -export const fakeParentScope = { - setSpan: jest.fn(), - getTransaction: jest.fn(() => fakeTransaction), -}; export const fakeHub = { - configureScope: jest.fn((fn: (arg: any) => any) => fn(fakeParentScope)), - getScope: jest.fn(() => fakeParentScope), + configureScope: jest.fn((fn: (arg: any) => any) => fn(fakeScope)), + pushScope: jest.fn(() => fakeScope), + popScope: jest.fn(), + getScope: jest.fn(() => fakeScope), }; export const fakeScope = { addEventProcessor: jest.fn(), setTransactionName: jest.fn(), setTag: jest.fn(), setContext: jest.fn(), + setSpan: jest.fn(), + getTransaction: jest.fn(() => fakeTransaction), }; export const fakeSpan = { finish: jest.fn(), @@ -27,6 +27,8 @@ export const fakeTransaction = { setHttpStatus: jest.fn(), startChild: jest.fn(() => fakeSpan), }; +export const init = jest.fn(); +export const addGlobalEventProcessor = jest.fn(); export const getCurrentHub = jest.fn(() => fakeHub); export const startTransaction = jest.fn(_ => fakeTransaction); export const captureException = jest.fn(); @@ -39,16 +41,20 @@ export const resetMocks = (): void => { fakeTransaction.finish.mockClear(); fakeTransaction.startChild.mockClear(); fakeSpan.finish.mockClear(); - fakeParentScope.setSpan.mockClear(); - fakeParentScope.getTransaction.mockClear(); fakeHub.configureScope.mockClear(); + fakeHub.pushScope.mockClear(); + fakeHub.popScope.mockClear(); fakeHub.getScope.mockClear(); fakeScope.addEventProcessor.mockClear(); fakeScope.setTransactionName.mockClear(); fakeScope.setTag.mockClear(); fakeScope.setContext.mockClear(); + fakeScope.setSpan.mockClear(); + fakeScope.getTransaction.mockClear(); + init.mockClear(); + addGlobalEventProcessor.mockClear(); getCurrentHub.mockClear(); startTransaction.mockClear(); captureException.mockClear(); diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index 1f84eadec3f7..e80b79f2515d 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -41,6 +41,36 @@ const fakeCallback: Callback = (err, result) => { return err; }; +function expectScopeSettings() { + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setTag).toBeCalledWith('url', 'awslambda:///functionName'); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setContext).toBeCalledWith('runtime', { name: 'node', version: expect.anything() }); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setContext).toBeCalledWith( + 'aws.lambda', + expect.objectContaining({ + aws_request_id: 'awsRequestId', + function_name: 'functionName', + function_version: 'functionVersion', + invoked_function_arn: 'invokedFunctionArn', + remaining_time_in_millis: 100, + }), + ); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setContext).toBeCalledWith( + 'aws.cloudwatch.logs', + expect.objectContaining({ + log_group: 'logGroupName', + log_stream: 'logStreamName', + }), + ); +} + describe('AWSLambda', () => { afterEach(() => { // @ts-ignore see "Why @ts-ignore" note @@ -140,7 +170,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on sync handler', () => { test('successful execution', async () => { - expect.assertions(5); + expect.assertions(10); const handler: Handler = (_event, _context, callback) => { callback(null, 42); @@ -149,15 +179,14 @@ describe('AWSLambda', () => { const rv = await wrappedHandler(fakeEvent, fakeContext, fakeCallback); expect(rv).toStrictEqual(42); expect(Sentry.startTransaction).toBeCalledWith({ name: 'functionName', op: 'awslambda.handler' }); - // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expectScopeSettings(); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); }); test('unsuccessful execution', async () => { - expect.assertions(5); + expect.assertions(10); const error = new Error('sorry'); const handler: Handler = (_event, _context, callback) => { @@ -169,8 +198,7 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { expect(Sentry.startTransaction).toBeCalledWith({ name: 'functionName', op: 'awslambda.handler' }); - // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expectScopeSettings(); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -191,7 +219,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(5); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = (_event, _context, _callback) => { @@ -203,8 +231,7 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { expect(Sentry.startTransaction).toBeCalledWith({ name: 'functionName', op: 'awslambda.handler' }); - // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expectScopeSettings(); expect(Sentry.captureException).toBeCalledWith(e); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -215,7 +242,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler', () => { test('successful execution', async () => { - expect.assertions(5); + expect.assertions(10); const handler: Handler = async (_event, _context) => { return 42; @@ -224,8 +251,7 @@ describe('AWSLambda', () => { const rv = await wrappedHandler(fakeEvent, fakeContext, fakeCallback); expect(rv).toStrictEqual(42); expect(Sentry.startTransaction).toBeCalledWith({ name: 'functionName', op: 'awslambda.handler' }); - // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expectScopeSettings(); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalled(); @@ -243,7 +269,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(5); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context) => { @@ -255,8 +281,7 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { expect(Sentry.startTransaction).toBeCalledWith({ name: 'functionName', op: 'awslambda.handler' }); - // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expectScopeSettings(); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -267,7 +292,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler with a callback method (aka incorrect usage)', () => { test('successful execution', async () => { - expect.assertions(5); + expect.assertions(10); const handler: Handler = async (_event, _context, _callback) => { return 42; @@ -276,8 +301,7 @@ describe('AWSLambda', () => { const rv = await wrappedHandler(fakeEvent, fakeContext, fakeCallback); expect(rv).toStrictEqual(42); expect(Sentry.startTransaction).toBeCalledWith({ name: 'functionName', op: 'awslambda.handler' }); - // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expectScopeSettings(); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalled(); @@ -295,7 +319,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(5); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context, _callback) => { @@ -307,8 +331,7 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { expect(Sentry.startTransaction).toBeCalledWith({ name: 'functionName', op: 'awslambda.handler' }); - // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expectScopeSettings(); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -317,88 +340,88 @@ describe('AWSLambda', () => { }); }); - test('enhance event with SDK info and correct mechanism value', async () => { - expect.assertions(2); - - const error = new Error('wat'); - const handler = () => { - throw error; - }; - const wrappedHandler = wrapHandler(handler, { rethrowAfterCapture: false }); - - const eventWithSomeData = { - exception: { - values: [{}], - }, - sdk: { - integrations: ['SomeIntegration'], - packages: [ - { - name: 'some:@random/package', - version: '1337', - }, - ], - }, - }; - // @ts-ignore see "Why @ts-ignore" note - Sentry.fakeScope.addEventProcessor.mockImplementationOnce(cb => cb(eventWithSomeData)); - await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(eventWithSomeData).toEqual({ - exception: { - values: [ - { - mechanism: { - handled: false, + describe('init()', () => { + test('enhance event with SDK info and correct mechanism value', async () => { + expect.assertions(1); + + const eventWithSomeData = { + exception: { + values: [{}], + }, + sdk: { + integrations: ['SomeIntegration'], + packages: [ + { + name: 'some:@random/package', + version: '1337', + }, + ], + }, + }; + // @ts-ignore see "Why @ts-ignore" note + Sentry.addGlobalEventProcessor.mockImplementationOnce(cb => cb(eventWithSomeData)); + Sentry.AWSLambda.init({ defaultIntegrations: [] }); + expect(eventWithSomeData).toEqual({ + exception: { + values: [ + { + mechanism: { + handled: false, + }, }, - }, - ], - }, - sdk: { - name: 'sentry.javascript.serverless', - integrations: ['SomeIntegration', 'AWSLambda'], - packages: [ - { - name: 'some:@random/package', - version: '1337', - }, - { - name: 'npm:@sentry/serverless', - version: '6.6.6', - }, - ], - version: '6.6.6', - }, + ], + }, + sdk: { + name: 'sentry.javascript.serverless', + integrations: ['SomeIntegration', 'AWSLambda'], + packages: [ + { + name: 'some:@random/package', + version: '1337', + }, + { + name: 'npm:@sentry/serverless', + version: '6.6.6', + }, + ], + version: '6.6.6', + }, + }); }); - const eventWithoutAnyData: Event = { - exception: { - values: [{}], - }, - }; - // @ts-ignore see "Why @ts-ignore" note - Sentry.fakeScope.addEventProcessor.mockImplementationOnce(cb => cb(eventWithoutAnyData)); - await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(eventWithoutAnyData).toEqual({ - exception: { - values: [ - { - mechanism: { - handled: false, + test('populates missing SDK info and mechanism', async () => { + expect.assertions(1); + + const eventWithoutAnyData: Event = { + exception: { + values: [{}], + }, + }; + // @ts-ignore see "Why @ts-ignore" note + Sentry.addGlobalEventProcessor.mockImplementationOnce(cb => cb(eventWithoutAnyData)); + Sentry.AWSLambda.init({ defaultIntegrations: [] }); + expect(eventWithoutAnyData).toEqual({ + exception: { + values: [ + { + mechanism: { + handled: false, + }, }, - }, - ], - }, - sdk: { - name: 'sentry.javascript.serverless', - integrations: ['AWSLambda'], - packages: [ - { - name: 'npm:@sentry/serverless', - version: '6.6.6', - }, - ], - version: '6.6.6', - }, + ], + }, + sdk: { + name: 'sentry.javascript.serverless', + integrations: ['AWSLambda'], + packages: [ + { + name: 'npm:@sentry/serverless', + version: '6.6.6', + }, + ], + version: '6.6.6', + }, + }); }); }); }); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 9b2a080d5fd9..a25478bb8e71 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -28,12 +28,10 @@ describe('GCPFunction', () => { return new Promise((resolve, _reject) => { const d = domain.create(); const req = { - method: 'GET', - host: 'hostname', - cookies: {}, - query: {}, - url: '/path', - headers: {}, + method: 'POST', + url: '/path?q=query', + headers: { host: 'hostname', 'content-type': 'application/json' }, + body: { foo: 'bar' }, } as Request; const res = { end: resolve } as Response; d.on('error', () => res.end()); @@ -106,9 +104,9 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapHttpFunction(handler); await handleHttp(wrappedHandler); - expect(Sentry.startTransaction).toBeCalledWith({ name: 'GET /path', op: 'gcp.function.http' }); + expect(Sentry.startTransaction).toBeCalledWith({ name: 'POST /path', op: 'gcp.function.http' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.setHttpStatus).toBeCalledWith(200); // @ts-ignore see "Why @ts-ignore" note @@ -125,9 +123,9 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapHttpFunction(handler); await handleHttp(wrappedHandler); - expect(Sentry.startTransaction).toBeCalledWith({ name: 'GET /path', op: 'gcp.function.http' }); + expect(Sentry.startTransaction).toBeCalledWith({ name: 'POST /path', op: 'gcp.function.http' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -135,6 +133,26 @@ describe('GCPFunction', () => { }); }); + test('wrapHttpFunction request data', async () => { + expect.assertions(7); + + const handler: HttpFunction = (_req, res) => { + res.end(); + }; + const wrappedHandler = wrapHttpFunction(handler); + const event: Event = {}; + // @ts-ignore see "Why @ts-ignore" note + Sentry.fakeScope.addEventProcessor.mockImplementation(cb => cb(event)); + await handleHttp(wrappedHandler); + expect(event.transaction).toEqual('POST /path'); + expect(event.contexts?.runtime).toEqual({ name: 'node', version: expect.anything() }); + expect(event.request?.method).toEqual('POST'); + expect(event.request?.url).toEqual('http://hostname/path?q=query'); + expect(event.request?.query_string).toEqual('q=query'); + expect(event.request?.headers).toEqual({ host: 'hostname', 'content-type': 'application/json' }); + expect(event.request?.data).toEqual('{"foo":"bar"}'); + }); + describe('wrapEventFunction() without callback', () => { test('successful execution', async () => { expect.assertions(5); @@ -146,7 +164,7 @@ describe('GCPFunction', () => { await expect(handleEvent(wrappedHandler)).resolves.toBe(42); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -163,7 +181,7 @@ describe('GCPFunction', () => { await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -182,7 +200,7 @@ describe('GCPFunction', () => { await expect(handleEvent(wrappedHandler)).resolves.toBe(42); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -199,7 +217,7 @@ describe('GCPFunction', () => { await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -217,11 +235,28 @@ describe('GCPFunction', () => { await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); }); }); + test('wrapEventFunction scope data', async () => { + expect.assertions(3); + + const handler: EventFunction = (_data, _context) => 42; + const wrappedHandler = wrapEventFunction(handler); + await handleEvent(wrappedHandler); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setContext).toBeCalledWith('runtime', { name: 'node', version: expect.anything() }); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setContext).toBeCalledWith('gcp.function.context', { + eventType: 'event.type', + resource: 'some.resource', + }); + }); + describe('wrapCloudEventFunction() without callback', () => { test('successful execution', async () => { expect.assertions(5); @@ -233,7 +268,7 @@ describe('GCPFunction', () => { await expect(handleCloudEvent(wrappedHandler)).resolves.toBe(42); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.cloud_event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -250,7 +285,7 @@ describe('GCPFunction', () => { await expect(handleCloudEvent(wrappedHandler)).rejects.toThrowError(error); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.cloud_event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -269,7 +304,7 @@ describe('GCPFunction', () => { await expect(handleCloudEvent(wrappedHandler)).resolves.toBe(42); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.cloud_event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalledWith(2000); @@ -286,7 +321,7 @@ describe('GCPFunction', () => { await expect(handleCloudEvent(wrappedHandler)).rejects.toThrowError(error); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.cloud_event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.finish).toBeCalled(); @@ -304,93 +339,107 @@ describe('GCPFunction', () => { await expect(handleCloudEvent(wrappedHandler)).rejects.toThrowError(error); expect(Sentry.startTransaction).toBeCalledWith({ name: 'event.type', op: 'gcp.function.cloud_event' }); // @ts-ignore see "Why @ts-ignore" note - expect(Sentry.fakeParentScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); + expect(Sentry.fakeScope.setSpan).toBeCalledWith(Sentry.fakeTransaction); expect(Sentry.captureException).toBeCalledWith(error); }); }); - test('enhance event with SDK info and correct mechanism value', async () => { - expect.assertions(2); + test('wrapCloudEventFunction scope data', async () => { + expect.assertions(3); - const error = new Error('wat'); - const handler: HttpFunction = () => { - throw error; - }; - const wrappedHandler = wrapHttpFunction(handler); - - const eventWithSomeData = { - exception: { - values: [{}], - }, - sdk: { - integrations: ['SomeIntegration'], - packages: [ - { - name: 'some:@random/package', - version: '1337', - }, - ], - }, - }; + const handler: CloudEventFunction = _context => 42; + const wrappedHandler = wrapCloudEventFunction(handler); + await handleCloudEvent(wrappedHandler); // @ts-ignore see "Why @ts-ignore" note - Sentry.fakeScope.addEventProcessor.mockImplementationOnce(cb => cb(eventWithSomeData)); - await handleHttp(wrappedHandler); - expect(eventWithSomeData).toEqual({ - exception: { - values: [ - { - mechanism: { - handled: false, + expect(Sentry.fakeScope.setContext).toBeCalledWith('runtime', { name: 'node', version: expect.anything() }); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); + // @ts-ignore see "Why @ts-ignore" note + expect(Sentry.fakeScope.setContext).toBeCalledWith('gcp.function.context', { type: 'event.type' }); + }); + + describe('init()', () => { + test('enhance event with SDK info and correct mechanism value', async () => { + expect.assertions(1); + + const eventWithSomeData = { + exception: { + values: [{}], + }, + sdk: { + integrations: ['SomeIntegration'], + packages: [ + { + name: 'some:@random/package', + version: '1337', + }, + ], + }, + }; + // @ts-ignore see "Why @ts-ignore" note + Sentry.addGlobalEventProcessor.mockImplementationOnce(cb => cb(eventWithSomeData)); + Sentry.AWSLambda.init({ defaultIntegrations: [] }); + expect(eventWithSomeData).toEqual({ + exception: { + values: [ + { + mechanism: { + handled: false, + }, + }, + ], + }, + sdk: { + name: 'sentry.javascript.serverless', + integrations: ['SomeIntegration', 'AWSLambda'], + packages: [ + { + name: 'some:@random/package', + version: '1337', }, - }, - ], - }, - sdk: { - name: 'sentry.javascript.serverless', - integrations: ['SomeIntegration', 'GCPFunction'], - packages: [ - { - name: 'some:@random/package', - version: '1337', - }, - { - name: 'npm:@sentry/serverless', - version: '6.6.6', - }, - ], - version: '6.6.6', - }, + { + name: 'npm:@sentry/serverless', + version: '6.6.6', + }, + ], + version: '6.6.6', + }, + }); }); - const eventWithoutAnyData: Event = { - exception: { - values: [{}], - }, - }; - // @ts-ignore see "Why @ts-ignore" note - Sentry.fakeScope.addEventProcessor.mockImplementationOnce(cb => cb(eventWithoutAnyData)); - await handleHttp(wrappedHandler); - expect(eventWithoutAnyData).toEqual({ - exception: { - values: [ - { - mechanism: { - handled: false, + test('populates missing SDK info and mechanism', async () => { + expect.assertions(1); + + const eventWithoutAnyData: Event = { + exception: { + values: [{}], + }, + }; + // @ts-ignore see "Why @ts-ignore" note + Sentry.addGlobalEventProcessor.mockImplementationOnce(cb => cb(eventWithoutAnyData)); + Sentry.AWSLambda.init({ defaultIntegrations: [] }); + expect(eventWithoutAnyData).toEqual({ + exception: { + values: [ + { + mechanism: { + handled: false, + }, + }, + ], + }, + sdk: { + name: 'sentry.javascript.serverless', + integrations: ['AWSLambda'], + packages: [ + { + name: 'npm:@sentry/serverless', + version: '6.6.6', }, - }, - ], - }, - sdk: { - name: 'sentry.javascript.serverless', - integrations: ['GCPFunction'], - packages: [ - { - name: 'npm:@sentry/serverless', - version: '6.6.6', - }, - ], - version: '6.6.6', - }, + ], + version: '6.6.6', + }, + }); }); }); });