Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(aws): Add experimental AWS Lambda extension for tunnelling events #17525

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
msonnb wants to merge 3 commits into develop
base: develop
Choose a base branch
Loading
from ms/aws-extension
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,16 @@
import * as Sentry from '@sentry/aws-serverless';

Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1,
debug: true,
_experiments: {
enableLambdaExtension: true,
},
});

export const handler = async (event, context) => {
Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => {
return 'Hello, world!';
});
};
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,52 @@ test.describe('Lambda layer', () => {
}),
);
});

test('experimental extension works', async ({ lambdaClient }) => {
const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
return transactionEvent?.transaction === 'LayerExperimentalExtension';
});

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

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.transaction).toEqual('LayerExperimentalExtension');
expect(transactionEvent.contexts?.trace).toEqual({
data: {
'sentry.sample_rate': 1,
'sentry.source': 'custom',
'sentry.origin': 'auto.otel.aws-lambda',
'sentry.op': 'function.aws.lambda',
'cloud.account.id': '012345678912',
'faas.execution': expect.any(String),
'faas.id': 'arn:aws:lambda:us-east-1:012345678912:function:LayerExperimentalExtension',
'faas.coldstart': true,
'otel.kind': 'SERVER',
},
op: 'function.aws.lambda',
origin: 'auto.otel.aws-lambda',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
status: 'ok',
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
});

expect(transactionEvent.spans).toHaveLength(1);

expect(transactionEvent.spans).toContainEqual(
expect.objectContaining({
data: expect.objectContaining({
'sentry.op': 'test',
'sentry.origin': 'manual',
}),
description: 'manual-span',
op: 'test',
}),
);
});
});
18 changes: 14 additions & 4 deletions dev-packages/rollup-utils/bundleHelpers.mjs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function makeBaseBundleConfig(options) {
},
},
context: 'window',
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin],
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin, licensePlugin],
};

// used by `@sentry/wasm` & pluggable integrations from core/browser (bundles which need to be combined with a stand-alone SDK bundle)
Expand Down Expand Up @@ -87,14 +87,23 @@ export function makeBaseBundleConfig(options) {
// code to add after the CJS wrapper
footer: '}(window));',
},
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin],
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin, licensePlugin],
};

const workerBundleConfig = {
output: {
format: 'esm',
},
plugins: [commonJSPlugin, makeTerserPlugin()],
plugins: [commonJSPlugin, makeTerserPlugin(), licensePlugin],
// Don't bundle any of Node's core modules
external: builtinModules,
};

const awsLambdaExtensionBundleConfig = {
output: {
format: 'esm',
},
plugins: [commonJSPlugin, makeIsDebugBuildPlugin(true), makeTerserPlugin()],
Copy link

@cursor cursor bot Sep 5, 2025

Choose a reason for hiding this comment

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

Bug: Missing License Plugin in Lambda Extension Config

The awsLambdaExtensionBundleConfig is missing the licensePlugin. This plugin was moved from the shared config to individual bundle configurations, but the new lambda extension config wasn't updated. This means lambda extension bundles will lack license information, unlike other bundle types.

Fix in Cursor Fix in Web

// Don't bundle any of Node's core modules
external: builtinModules,
};
Expand All @@ -110,14 +119,15 @@ export function makeBaseBundleConfig(options) {
strict: false,
esModule: false,
},
plugins: [sucrasePlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin],
plugins: [sucrasePlugin, nodeResolvePlugin, cleanupPlugin],
treeshake: 'smallest',
};

const bundleTypeConfigMap = {
standalone: standAloneBundleConfig,
addon: addOnBundleConfig,
'node-worker': workerBundleConfig,
'lambda-extension': awsLambdaExtensionBundleConfig,
};

return deepMerge.all([sharedBundleConfig, bundleTypeConfigMap[bundleType], packageSpecificConfig || {}], {
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-serverless/package.json
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
},
"scripts": {
"build": "run-p build:transpile build:types",
"build:layer": "yarn ts-node scripts/buildLambdaLayer.ts",
"build:layer": "rollup -c rollup.lambda-extension.config.mjs && yarn ts-node scripts/buildLambdaLayer.ts",
"build:dev": "run-p build:transpile build:types",
"build:transpile": "rollup -c rollup.npm.config.mjs && yarn build:layer",
"build:types": "run-s build:types:core build:types:downlevel",
Expand Down
15 changes: 15 additions & 0 deletions packages/aws-serverless/rollup.lambda-extension.config.mjs
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { makeBaseBundleConfig } from '@sentry-internal/rollup-utils';

export default [
makeBaseBundleConfig({
bundleType: 'lambda-extension',
entrypoints: ['src/lambda-extension/index.ts'],
outputFileBase: 'index.mjs',
packageSpecificConfig: {
output: {
dir: 'build/aws/dist-serverless/sentry-extension',
sourcemap: false,
},
},
}),
];
5 changes: 5 additions & 0 deletions packages/aws-serverless/scripts/buildLambdaLayer.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ async function buildLambdaLayer(): Promise<void> {

replaceSDKSource();

fsForceMkdirSync('./build/aws/dist-serverless/extensions');
fs.copyFileSync('./src/lambda-extension/sentry-extension', './build/aws/dist-serverless/extensions/sentry-extension');
fs.chmodSync('./build/aws/dist-serverless/extensions/sentry-extension', 0o755);
fs.chmodSync('./build/aws/dist-serverless/sentry-extension/index.mjs', 0o755);

const zipFilename = `sentry-node-serverless-${version}.zip`;
console.log(`Creating final layer zip file ${zipFilename}.`);
// need to preserve the symlink above with -y
Expand Down
35 changes: 31 additions & 4 deletions packages/aws-serverless/src/init.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Integration, Options } from '@sentry/core';
import { applySdkMetadata, getSDKSource } from '@sentry/core';
import { applySdkMetadata, debug, getSDKSource } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node';
import { DEBUG_BUILD } from './debug-build';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';

/**
* Get the default integrations for the AWSLambda SDK.
*/
Expand All @@ -14,18 +14,45 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
return [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()];
}

export interface AwsServerlessOptions extends NodeOptions {
_experiments?: NodeOptions['_experiments'] & {
/**
* If proxying Sentry events through the Sentry Lambda extension should be enabled.
*/
enableLambdaExtension?: boolean;
};
}

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

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

if (opts._experiments?.enableLambdaExtension) {
if (sdkSource === 'aws-lambda-layer') {
if (!opts.tunnel) {
DEBUG_BUILD && debug.log('Proxying Sentry events through the Sentry Lambda extension');
opts.tunnel = 'http://localhost:9000/envelope';
} else {
DEBUG_BUILD &&
debug.warn(
`Using a custom tunnel with the Sentry Lambda extension is not supported. Events will be tunnelled to ${opts.tunnel} and not through the extension.`,
);
}
} else {
DEBUG_BUILD && debug.warn('The Sentry Lambda extension is only supported when using the AWS Lambda layer.');
}
}

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

return initWithoutDefaultIntegrations(opts);
}
145 changes: 145 additions & 0 deletions packages/aws-serverless/src/lambda-extension/aws-lambda-extension.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as http from 'node:http';
import { buffer } from 'node:stream/consumers';
import { debug, dsnFromString, getEnvelopeEndpointWithUrlEncodedAuth } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';

/**
* The Extension API Client.
*/
export class AwsLambdaExtension {
private readonly _baseUrl: string;
private _extensionId: string | null;

public constructor() {
this._baseUrl = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension`;
this._extensionId = null;
}

/**
* Register this extension as an external extension with AWS.
*/
public async register(): Promise<void> {
const res = await fetch(`${this._baseUrl}/register`, {
method: 'POST',
body: JSON.stringify({
events: ['INVOKE', 'SHUTDOWN'],
}),
headers: {
'Content-Type': 'application/json',
'Lambda-Extension-Name': 'sentry-extension',
},
});

if (!res.ok) {
throw new Error(`Failed to register with the extension API: ${await res.text()}`);
}

this._extensionId = res.headers.get('lambda-extension-identifier');
}

/**
* Advances the extension to the next event.
*/
public async next(): Promise<void> {
if (!this._extensionId) {
throw new Error('Extension ID is not set');
}

const res = await fetch(`${this._baseUrl}/event/next`, {
headers: {
'Lambda-Extension-Identifier': this._extensionId,
'Content-Type': 'application/json',
},
});

if (!res.ok) {
throw new Error(`Failed to advance to next event: ${await res.text()}`);
}
}

/**
* Reports an error to the extension API.
* @param phase The phase of the extension.
* @param err The error to report.
*/
public async error(phase: 'init' | 'exit', err: Error): Promise<never> {
if (!this._extensionId) {
throw new Error('Extension ID is not set');
}

const errorType = `Extension.${err.name || 'UnknownError'}`;

const res = await fetch(`${this._baseUrl}/${phase}/error`, {
method: 'POST',
body: JSON.stringify({
errorMessage: err.message || err.toString(),
errorType,
stackTrace: [err.stack],
}),
headers: {
'Content-Type': 'application/json',
'Lambda-Extension-Identifier': this._extensionId,
'Lambda-Extension-Function-Error': errorType,
},
});

if (!res.ok) {
DEBUG_BUILD && debug.error(`Failed to report error: ${await res.text()}`);
}

throw err;
}

/**
* Starts the Sentry tunnel.
*/
public startSentryTunnel(): void {
const server = http.createServer(async (req, res) => {
if (req.method === 'POST' && req.url?.startsWith('/envelope')) {
try {
const buf = await buffer(req);
// Extract the actual bytes from the Buffer by slicing its underlying ArrayBuffer
// This ensures we get only the data portion without any padding or offset
const envelopeBytes = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const envelope = new TextDecoder().decode(envelopeBytes);
const piece = envelope.split('\n')[0];
const header = JSON.parse(piece || '{}') as { dsn?: string };
if (!header.dsn) {
throw new Error('DSN is not set');
}
const dsn = dsnFromString(header.dsn);
if (!dsn) {
throw new Error('Invalid DSN');
}
const upstreamSentryUrl = getEnvelopeEndpointWithUrlEncodedAuth(dsn);

fetch(upstreamSentryUrl, {
method: 'POST',
body: envelopeBytes,
}).catch(err => {
DEBUG_BUILD && debug.error('Error sending envelope to Sentry', err);
});

res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({}));
} catch (e) {
DEBUG_BUILD && debug.error('Error tunneling to Sentry', e);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Error tunneling to Sentry' }));
}
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
}
});

server.listen(9000, () => {
DEBUG_BUILD && debug.log('Sentry proxy listening on port 9000');
});

server.on('error', err => {
DEBUG_BUILD && debug.error('Error starting Sentry proxy', err);
process.exit(1);
});
}
}
8 changes: 8 additions & 0 deletions packages/aws-serverless/src/lambda-extension/debug-build.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare const __DEBUG_BUILD__: boolean;

/**
* This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
*
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
*/
export const DEBUG_BUILD = __DEBUG_BUILD__;
21 changes: 21 additions & 0 deletions packages/aws-serverless/src/lambda-extension/index.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env node
import { debug } from '@sentry/core';
import { AwsLambdaExtension } from './aws-lambda-extension';
import { DEBUG_BUILD } from './debug-build';

async function main(): Promise<void> {
const extension = new AwsLambdaExtension();

await extension.register();

extension.startSentryTunnel();

// eslint-disable-next-line no-constant-condition
while (true) {
await extension.next();
}
}

main().catch(err => {
DEBUG_BUILD && debug.error('Error in Lambda Extension', err);
});
Loading
Loading

AltStyle によって変換されたページ (->オリジナル) /