Skip to content

feat(aws): Add support for automatic wrapping in ESM #17407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const handler = async () => {
throw new Error('test esm');
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/aws-serverless';

import * as http from 'node:http';

export const handler = Sentry.wrapHandler(async () => {
export const handler = async () => {
await Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => {
await new Promise(resolve => {
http.get('http://example.com', res => {
Expand All @@ -16,4 +16,4 @@ export const handler = Sentry.wrapHandler(async () => {
});
});
});
});
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const http = require('http');
const Sentry = require('@sentry/aws-serverless');

exports.handler = Sentry.wrapHandler(async () => {
exports.handler = async () => {
await new Promise(resolve => {
const req = http.request(
{
Expand All @@ -21,4 +21,4 @@ exports.handler = Sentry.wrapHandler(async () => {
});

Sentry.startSpan({ name: 'manual-span', op: 'manual' }, () => {});
});
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as http from 'node:http';
import * as Sentry from '@sentry/aws-serverless';

export const handler = Sentry.wrapHandler(async () => {
export const handler = async () => {
await new Promise(resolve => {
const req = http.request(
{
Expand All @@ -21,4 +21,4 @@ export const handler = Sentry.wrapHandler(async () => {
});

Sentry.startSpan({ name: 'manual-span', op: 'manual' }, () => {});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,35 @@ test.describe('Lambda layer', () => {
type: 'Error',
value: 'test',
mechanism: {
type: 'auto.function.aws-serverless.handler',
type: 'auto.function.aws-serverless.otel',
handled: false,
},
}),
);
});

test('capturing errors works in ESM', async ({ lambdaClient }) => {
const errorEventPromise = waitForError('aws-serverless-lambda-sam', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'test esm';
});

await lambdaClient.send(
new InvokeCommand({
FunctionName: 'LayerErrorEsm',
Payload: JSON.stringify({}),
}),
);

const errorEvent = await errorEventPromise;

// shows the SDK sent an error event
expect(errorEvent.exception?.values).toHaveLength(1);
expect(errorEvent.exception?.values?.[0]).toEqual(
expect.objectContaining({
type: 'Error',
value: 'test esm',
mechanism: {
type: 'auto.function.aws-serverless.otel',
handled: false,
},
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-serverless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/instrumentation": "^0.203.0",
"@opentelemetry/instrumentation-aws-lambda": "0.54.0",
"@opentelemetry/instrumentation-aws-sdk": "0.56.0",
"@opentelemetry/semantic-conventions": "^1.36.0",
"@sentry/core": "10.5.0",
"@sentry/node": "10.5.0",
"@types/aws-lambda": "^8.10.62"
Expand Down
4 changes: 0 additions & 4 deletions packages/aws-serverless/src/awslambda-auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ if (lambdaTaskRoot) {
: {},
),
});

if (typeof require !== 'undefined') {
Sentry.tryPatchHandler(lambdaTaskRoot, handlerString);
}
} else {
throw Error('LAMBDA_TASK_ROOT environment variable is not set');
}
3 changes: 2 additions & 1 deletion packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,6 @@ export {
export { awsIntegration } from './integration/aws';
export { awsLambdaIntegration } from './integration/awslambda';

export { getDefaultIntegrations, init, tryPatchHandler, wrapHandler } from './sdk';
export { getDefaultIntegrations, init } from './init';
export { wrapHandler } from './sdk';
export type { WrapperOptions } from './sdk';
31 changes: 31 additions & 0 deletions packages/aws-serverless/src/init.ts
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved init code to separate file to avoid circular dependency

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Integration, Options } from '@sentry/core';
import { applySdkMetadata, getSDKSource } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';

/**
* Get the default integrations for the AWSLambda SDK.
*/
// NOTE: in awslambda-auto.ts, we also call the original `getDefaultIntegrations` from `@sentry/node` to load performance integrations.
// If at some point we need to filter a node integration out for good, we need to make sure to also filter it out there.
export function getDefaultIntegrations(_options: Options): Integration[] {
return [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()];
}

/**
* Initializes the Sentry AWS Lambda SDK.
*
* @param options Configuration options for the SDK, @see {@link AWSLambdaOptions}.
*/
export function init(options: NodeOptions = {}): NodeClient | undefined {
const opts = {
defaultIntegrations: getDefaultIntegrations(options),
...options,
};

applySdkMetadata(opts, 'aws-serverless', ['aws-serverless'], getSDKSource());

return initWithoutDefaultIntegrations(opts);
}
11 changes: 8 additions & 3 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node';
import { eventContextExtractor } from '../utils';
import { captureException, generateInstrumentOnce } from '@sentry/node';
import { eventContextExtractor, markEventUnhandled } from '../utils';
import { AwsLambdaInstrumentation } from './instrumentation-aws-lambda/instrumentation';

interface AwsLambdaOptions {
/**
Expand All @@ -27,6 +27,11 @@ export const instrumentAwsLambda = generateInstrumentOnce(
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
responseHook(_span, { err }) {
if (err) {
captureException(err, scope => markEventUnhandled(scope, 'auto.function.aws-serverless.otel'));
}
},
};
Comment on lines +30 to +34
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

capturing errors here ensures that they're connected to the span correctly

},
);
Expand Down
Loading
Loading