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

Commit d98074a

Browse files
committed
feat(aws): Add AWS Lambda extension
1 parent 9e70a5a commit d98074a

File tree

12 files changed

+403
-9
lines changed

12 files changed

+403
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/aws-serverless';
2+
3+
Sentry.init({
4+
dsn: process.env.SENTRY_DSN,
5+
tracesSampleRate: 1,
6+
debug: true,
7+
_experiments: {
8+
enableLambdaExtension: true,
9+
},
10+
});
11+
12+
export const handler = async (event, context) => {
13+
Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => {
14+
return 'Hello, world!';
15+
});
16+
};

‎dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,52 @@ test.describe('Lambda layer', () => {
242242
}),
243243
);
244244
});
245+
246+
test('experimental extension works', async ({ lambdaClient }) => {
247+
const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
248+
return transactionEvent?.transaction === 'LayerExperimentalExtension';
249+
});
250+
251+
await lambdaClient.send(
252+
new InvokeCommand({
253+
FunctionName: 'LayerExperimentalExtension',
254+
Payload: JSON.stringify({}),
255+
}),
256+
);
257+
258+
const transactionEvent = await transactionEventPromise;
259+
260+
expect(transactionEvent.transaction).toEqual('LayerExperimentalExtension');
261+
expect(transactionEvent.contexts?.trace).toEqual({
262+
data: {
263+
'sentry.sample_rate': 1,
264+
'sentry.source': 'custom',
265+
'sentry.origin': 'auto.otel.aws-lambda',
266+
'sentry.op': 'function.aws.lambda',
267+
'cloud.account.id': '012345678912',
268+
'faas.execution': expect.any(String),
269+
'faas.id': 'arn:aws:lambda:us-east-1:012345678912:function:LayerExperimentalExtension',
270+
'faas.coldstart': true,
271+
'otel.kind': 'SERVER',
272+
},
273+
op: 'function.aws.lambda',
274+
origin: 'auto.otel.aws-lambda',
275+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
276+
status: 'ok',
277+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
278+
});
279+
280+
expect(transactionEvent.spans).toHaveLength(1);
281+
282+
expect(transactionEvent.spans).toContainEqual(
283+
expect.objectContaining({
284+
data: expect.objectContaining({
285+
'sentry.op': 'test',
286+
'sentry.origin': 'manual',
287+
}),
288+
description: 'manual-span',
289+
op: 'test',
290+
}),
291+
);
292+
});
245293
});

‎dev-packages/rollup-utils/bundleHelpers.mjs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function makeBaseBundleConfig(options) {
5353
},
5454
},
5555
context: 'window',
56-
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin],
56+
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin,licensePlugin],
5757
};
5858

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

9393
const workerBundleConfig = {
9494
output: {
9595
format: 'esm',
9696
},
97-
plugins: [commonJSPlugin, makeTerserPlugin()],
97+
plugins: [commonJSPlugin, makeTerserPlugin(), licensePlugin],
98+
// Don't bundle any of Node's core modules
99+
external: builtinModules,
100+
};
101+
102+
const awsLambdaExtensionBundleConfig = {
103+
output: {
104+
format: 'esm',
105+
},
106+
plugins: [commonJSPlugin, makeIsDebugBuildPlugin(true), makeTerserPlugin()],
98107
// Don't bundle any of Node's core modules
99108
external: builtinModules,
100109
};
@@ -110,14 +119,15 @@ export function makeBaseBundleConfig(options) {
110119
strict: false,
111120
esModule: false,
112121
},
113-
plugins: [sucrasePlugin, nodeResolvePlugin, cleanupPlugin,licensePlugin],
122+
plugins: [sucrasePlugin, nodeResolvePlugin, cleanupPlugin],
114123
treeshake: 'smallest',
115124
};
116125

117126
const bundleTypeConfigMap = {
118127
standalone: standAloneBundleConfig,
119128
addon: addOnBundleConfig,
120129
'node-worker': workerBundleConfig,
130+
'lambda-extension': awsLambdaExtensionBundleConfig,
121131
};
122132

123133
return deepMerge.all([sharedBundleConfig, bundleTypeConfigMap[bundleType], packageSpecificConfig || {}], {

‎packages/aws-serverless/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
},
8080
"scripts": {
8181
"build": "run-p build:transpile build:types",
82-
"build:layer": "yarn ts-node scripts/buildLambdaLayer.ts",
82+
"build:layer": "rollup -c rollup.lambda-extension.config.mjs && yarn ts-node scripts/buildLambdaLayer.ts",
8383
"build:dev": "run-p build:transpile build:types",
8484
"build:transpile": "rollup -c rollup.npm.config.mjs && yarn build:layer",
8585
"build:types": "run-s build:types:core build:types:downlevel",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { makeBaseBundleConfig } from '@sentry-internal/rollup-utils';
2+
3+
export default [
4+
makeBaseBundleConfig({
5+
bundleType: 'lambda-extension',
6+
entrypoints: ['src/lambda-extension/index.ts'],
7+
outputFileBase: 'index.mjs',
8+
packageSpecificConfig: {
9+
output: {
10+
dir: 'build/aws/dist-serverless/sentry-extension',
11+
sourcemap: false,
12+
},
13+
},
14+
}),
15+
];

‎packages/aws-serverless/scripts/buildLambdaLayer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ async function buildLambdaLayer(): Promise<void> {
4545

4646
replaceSDKSource();
4747

48+
fsForceMkdirSync('./build/aws/dist-serverless/extensions');
49+
fs.copyFileSync('./src/lambda-extension/sentry-extension', './build/aws/dist-serverless/extensions/sentry-extension');
50+
fs.chmodSync('./build/aws/dist-serverless/extensions/sentry-extension', 0o755);
51+
fs.chmodSync('./build/aws/dist-serverless/sentry-extension/index.mjs', 0o755);
52+
4853
const zipFilename = `sentry-node-serverless-${version}.zip`;
4954
console.log(`Creating final layer zip file ${zipFilename}.`);
5055
// need to preserve the symlink above with -y

‎packages/aws-serverless/src/init.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Integration, Options } from '@sentry/core';
2-
import { applySdkMetadata, getSDKSource } from '@sentry/core';
2+
import { applySdkMetadata, debug,getSDKSource } from '@sentry/core';
33
import type { NodeClient, NodeOptions } from '@sentry/node';
44
import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node';
5+
import { DEBUG_BUILD } from './debug-build';
56
import { awsIntegration } from './integration/aws';
67
import { awsLambdaIntegration } from './integration/awslambda';
7-
88
/**
99
* Get the default integrations for the AWSLambda SDK.
1010
*/
@@ -14,18 +14,34 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
1414
return [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()];
1515
}
1616

17+
export interface AwsServerlessOptions extends NodeOptions {
18+
_experiments?: NodeOptions['_experiments'] & {
19+
/**
20+
* If proxying Sentry events through the Sentry Lambda extension should be enabled.
21+
*/
22+
enableLambdaExtension?: boolean;
23+
};
24+
}
25+
1726
/**
1827
* Initializes the Sentry AWS Lambda SDK.
1928
*
2029
* @param options Configuration options for the SDK, @see {@link AWSLambdaOptions}.
2130
*/
22-
export function init(options: NodeOptions = {}): NodeClient | undefined {
31+
export function init(options: AwsServerlessOptions = {}): NodeClient | undefined {
2332
const opts = {
2433
defaultIntegrations: getDefaultIntegrations(options),
2534
...options,
2635
};
2736

28-
applySdkMetadata(opts, 'aws-serverless', ['aws-serverless'], getSDKSource());
37+
const sdkSource = getSDKSource();
38+
39+
if (opts._experiments?.enableLambdaExtension && sdkSource === 'aws-lambda-layer' && !opts.tunnel) {
40+
DEBUG_BUILD && debug.log('Proxying Sentry events through the Sentry Lambda extension');
41+
opts.tunnel = 'http://localhost:9000/envelope';
42+
}
43+
44+
applySdkMetadata(opts, 'aws-serverless', ['aws-serverless'], sdkSource);
2945

3046
return initWithoutDefaultIntegrations(opts);
3147
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import * as http from 'node:http';
2+
import { buffer } from 'node:stream/consumers';
3+
import { debug, dsnFromString, getEnvelopeEndpointWithUrlEncodedAuth } from '@sentry/core';
4+
import { DEBUG_BUILD } from './debug-build';
5+
6+
/**
7+
* The Extension API Client.
8+
*/
9+
export class AwsLambdaExtension {
10+
private readonly _baseUrl: string;
11+
private _extensionId: string | null;
12+
13+
public constructor() {
14+
this._baseUrl = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension`;
15+
this._extensionId = null;
16+
}
17+
18+
/**
19+
* Register this extension as an external extension with AWS.
20+
*/
21+
public async register(): Promise<void> {
22+
const res = await fetch(`${this._baseUrl}/register`, {
23+
method: 'POST',
24+
body: JSON.stringify({
25+
events: ['INVOKE', 'SHUTDOWN'],
26+
}),
27+
headers: {
28+
'Content-Type': 'application/json',
29+
'Lambda-Extension-Name': 'sentry-extension',
30+
},
31+
});
32+
33+
if (!res.ok) {
34+
throw new Error(`Failed to register with the extension API: ${await res.text()}`);
35+
}
36+
37+
this._extensionId = res.headers.get('lambda-extension-identifier');
38+
}
39+
40+
/**
41+
* Advances the extension to the next event.
42+
*/
43+
public async next(): Promise<void> {
44+
if (!this._extensionId) {
45+
throw new Error('Extension ID is not set');
46+
}
47+
48+
const res = await fetch(`${this._baseUrl}/event/next`, {
49+
headers: {
50+
'Lambda-Extension-Identifier': this._extensionId,
51+
'Content-Type': 'application/json',
52+
},
53+
});
54+
55+
if (!res.ok) {
56+
throw new Error(`Failed to advance to next event: ${await res.text()}`);
57+
}
58+
59+
const event = (await res.json()) as { eventType: string };
60+
61+
if (event.eventType === 'SHUTDOWN') {
62+
await new Promise(resolve => setTimeout(resolve, 1000));
63+
}
64+
}
65+
66+
/**
67+
* Reports an error to the extension API.
68+
* @param phase The phase of the extension.
69+
* @param err The error to report.
70+
*/
71+
public async error(phase: 'init' | 'exit', err: Error): Promise<never> {
72+
if (!this._extensionId) {
73+
throw new Error('Extension ID is not set');
74+
}
75+
76+
const errorType = `Extension.${err.name || 'UnknownError'}`;
77+
78+
const res = await fetch(`${this._baseUrl}/${phase}/error`, {
79+
method: 'POST',
80+
body: JSON.stringify({
81+
errorMessage: err.message || err.toString(),
82+
errorType,
83+
stackTrace: [err.stack],
84+
}),
85+
headers: {
86+
'Content-Type': 'application/json',
87+
'Lambda-Extension-Identifier': this._extensionId,
88+
'Lambda-Extension-Function-Error': errorType,
89+
},
90+
});
91+
92+
if (!res.ok) {
93+
DEBUG_BUILD && debug.error(`Failed to report error: ${await res.text()}`);
94+
}
95+
96+
throw err;
97+
}
98+
99+
/**
100+
* Starts the Sentry tunnel.
101+
*/
102+
public startSentryTunnel(): void {
103+
const server = http.createServer(async (req, res) => {
104+
if (req.url?.startsWith('/envelope')) {
105+
try {
106+
const buf = await buffer(req);
107+
// Extract the actual bytes from the Buffer by slicing its underlying ArrayBuffer
108+
// This ensures we get only the data portion without any padding or offset
109+
const envelopeBytes = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
110+
const envelope = new TextDecoder().decode(envelopeBytes);
111+
const piece = envelope.split('\n')[0];
112+
const header = JSON.parse(piece ?? '{}') as { dsn?: string };
113+
if (!header.dsn) {
114+
throw new Error('DSN is not set');
115+
}
116+
const dsn = dsnFromString(header.dsn);
117+
if (!dsn) {
118+
throw new Error('Invalid DSN');
119+
}
120+
const upstreamSentryUrl = getEnvelopeEndpointWithUrlEncodedAuth(dsn);
121+
122+
fetch(upstreamSentryUrl, {
123+
method: 'POST',
124+
body: envelopeBytes,
125+
}).catch(err => {
126+
DEBUG_BUILD && debug.error('Error sending envelope to Sentry', err);
127+
});
128+
129+
res.writeHead(200, { 'Content-Type': 'application/json' });
130+
res.end(JSON.stringify({}));
131+
} catch (e) {
132+
DEBUG_BUILD && debug.error('Error tunneling to Sentry', e);
133+
res.writeHead(500, { 'Content-Type': 'application/json' });
134+
res.end(JSON.stringify({ error: 'Error tunneling to Sentry' }));
135+
}
136+
}
137+
});
138+
139+
server.listen(9000, () => {
140+
DEBUG_BUILD && debug.log('Sentry proxy listening on port 9000');
141+
});
142+
}
143+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
declare const __DEBUG_BUILD__: boolean;
2+
3+
/**
4+
* 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.
5+
*
6+
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
7+
*/
8+
export const DEBUG_BUILD = __DEBUG_BUILD__;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env node
2+
import { debug } from '@sentry/core';
3+
import { AwsLambdaExtension } from './aws-lambda-extension';
4+
import { DEBUG_BUILD } from './debug-build';
5+
6+
async function main(): Promise<void> {
7+
const extension = new AwsLambdaExtension();
8+
9+
await extension.register();
10+
11+
extension.startSentryTunnel();
12+
13+
// eslint-disable-next-line no-constant-condition
14+
while (true) {
15+
await extension.next();
16+
}
17+
}
18+
19+
main().catch(err => {
20+
DEBUG_BUILD && debug.error('Error in Lambda Extension', err);
21+
});

0 commit comments

Comments
(0)

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