Skip to content

Commit 0041bc6

Browse files
msonnbcodex
andauthored
feat(node): Add awsLambdaWebIntegration for Lambda Web Functions (#25355)
This adds a new `awsLambdaWebIntegration` to the Node SDK for instrumenting [Lambda Web Functions](https://docs.aws.amazon.com/lambda/latest/dg/web-functions.html). Web Functions run standard Node HTTP servers like Express or Hono, without Lambda-specific handlers or adapters. Enriches event contexts and segment spans with AWS metadata, request IDs, endpoint names, and cold-start information. Closes #25317 conventions PR for new attributes: getsentry/sentry-conventions#715 --------- Co-authored-by: Codex <codex@openai.com>
1 parent 7aca628 commit 0041bc6

14 files changed

Lines changed: 421 additions & 3 deletions

File tree

‎dev-packages/bun-integration-tests/node-suites/excludes.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// `node:sqlite`, which `flue` needs, and the Vercel keep-alive, which needs `http.server.response.finish`.
88
const NODE_ONLY = [
99
'suites/anr/test.ts',
10+
'suites/aws-lambda-web/**',
1011
'suites/aws-serverless/**',
1112
'suites/breadcrumbs/**',
1213
'suites/child-process/test.ts',

‎dev-packages/deno-integration-tests/node-suites/excludes.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
// fails on Deno runs only on the other runtimes, with `test.runIf(supports({ runtimes }))` in the
33
// Node suite, and is not listed here.
44

5-
// Node-only features: ANR and native thread watchdogs, child process and worker thread breadcrumbs.
6-
const NODE_ONLY = ['suites/anr/test.ts', 'suites/breadcrumbs/**', 'suites/thread-blocked-native/test.ts'];
5+
// Node-only features: ANR and native thread watchdogs, child process and worker thread breadcrumbs, and the AWS Lambda Node runtime.
6+
const NODE_ONLY = [
7+
'suites/anr/test.ts',
8+
'suites/aws-lambda-web/**',
9+
'suites/breadcrumbs/**',
10+
'suites/thread-blocked-native/test.ts',
11+
];
712

813
// `@sentry/node` instruments `fetch` through undici's diagnostics channels, which Deno's `fetch`
914
// does not publish. `@sentry/deno` has its own `fetchIntegration` for this. These suites check

‎dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const DEPENDENTS: Dependent[] = [
5151
exports: Object.keys(SentryBun),
5252
ignoreExports: [
5353
// not supported in bun:
54+
'awsLambdaWebIntegration',
5455
'NodeClient',
5556
'NODE_VERSION',
5657
'childProcessIntegration',
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@example.com/1',
6+
tracesSampleRate: process.env.NO_TRACING ? undefined : 1,
7+
transport: loggingTransport,
8+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as Sentry from '@sentry/node';
2+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
3+
import express from 'express';
4+
5+
const app = express();
6+
let releaseFirst;
7+
const secondRequestStarted = new Promise(resolve => {
8+
releaseFirst = resolve;
9+
});
10+
11+
app.get('/:request', async (req, res) => {
12+
if (req.params.request === 'first') {
13+
process.stdout.write('FIRST_STARTED\n');
14+
await secondRequestStarted;
15+
} else {
16+
releaseFirst();
17+
}
18+
19+
Sentry.captureException(new Error(req.params.request));
20+
Sentry.logger.info('Request handled');
21+
Sentry.metrics.count('request_count', 1);
22+
res.send({ ok: true });
23+
setImmediate(() => {
24+
void Sentry.flush();
25+
});
26+
});
27+
28+
startExpressServerAndSendPortToRunner(app);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {
2+
AWS_LAMBDA_WEB_ENDPOINT_NAME,
3+
CLOUD_PLATFORM,
4+
CLOUD_PROVIDER,
5+
CLOUD_REGION,
6+
FAAS_COLDSTART,
7+
FAAS_INVOCATION_ID,
8+
FAAS_MAX_MEMORY,
9+
SENTRY_OP,
10+
} from '@sentry/conventions/attributes';
11+
import { expect, vi } from 'vitest';
12+
import { createEsmAndCjsTests } from '../../../utils/runner';
13+
14+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
15+
test.each([
16+
{ name: 'default tracing', tracing: true },
17+
{ name: 'tracing disabled', tracing: false },
18+
])('$name: isolates concurrent requests and leaves logs and metrics unenriched', async ({ tracing }) => {
19+
const runner = createRunner()
20+
.withEnv({
21+
AWS_LAMBDA_HTTP_ENDPOINT: '0.0.0.0:3000',
22+
AWS_REGION: 'us-east-1',
23+
AWS_LAMBDA_FUNCTION_MEMORY_SIZE: '2048',
24+
AWS_LAMBDA_INITIALIZATION_TYPE: 'native-http',
25+
AWS_EXECUTION_ENV: 'AWS_Lambda_nodejs24.x-lite',
26+
NO_TRACING: tracing ? '' : '1',
27+
})
28+
.unordered();
29+
30+
const spans = [];
31+
32+
for (const request of ['first', 'second']) {
33+
runner.expect({
34+
event: {
35+
exception: { values: [expect.objectContaining({ value: request })] },
36+
contexts: {
37+
cloud_resource: {
38+
[CLOUD_PROVIDER]: 'aws',
39+
[CLOUD_PLATFORM]: 'aws_lambda',
40+
[CLOUD_REGION]: 'us-east-1',
41+
},
42+
'aws.lambda': {
43+
memory_size: 2_147_483_648,
44+
initialization_type: 'native-http',
45+
cold_start: request === 'first',
46+
aws_request_id: request,
47+
endpoint_name: `${request}-endpoint`,
48+
},
49+
},
50+
},
51+
});
52+
53+
if (tracing) {
54+
spans.push({
55+
request,
56+
collected: runner.collectStreamedSpansUntilSegment(
57+
span => span.attributes[FAAS_INVOCATION_ID]?.value === request,
58+
),
59+
});
60+
}
61+
}
62+
63+
const assertUnenriched = (items: { attributes?: Record<string, unknown> }[]): void => {
64+
expect(items).toHaveLength(2);
65+
for (const item of items) {
66+
expect(Object.keys(item.attributes || {}).filter(key => /^(aws\.lambda\.|faas\.|cloud\.)/.test(key))).toEqual(
67+
[],
68+
);
69+
}
70+
};
71+
72+
runner.expect({ log: container => assertUnenriched(container.items) });
73+
runner.expect({ trace_metric: container => assertUnenriched(container.items) });
74+
75+
const running = runner.start();
76+
const first = running.makeRequest('get', '/first', {
77+
headers: { 'lambda-web-request-id': 'first', 'lambda-web-endpoint-name': 'first-endpoint' },
78+
});
79+
await vi.waitFor(() => expect(running.getLogs().join('\n')).toContain('FIRST_STARTED'), { timeout: 10_000 });
80+
81+
await Promise.all([
82+
first,
83+
running.makeRequest('get', '/second', {
84+
headers: { 'lambda-web-request-id': 'second', 'lambda-web-endpoint-name': 'second-endpoint' },
85+
}),
86+
]);
87+
await running.completed();
88+
89+
const segmentSpans = [];
90+
91+
for (const { request, collected } of spans) {
92+
const items = await collected;
93+
segmentSpans.push(...items.filter(item => item.is_segment));
94+
95+
expect(items.find(item => item.is_segment)).toMatchObject({
96+
name: 'GET /:request',
97+
attributes: {
98+
[SENTRY_OP]: { type: 'string', value: 'http.server' },
99+
[CLOUD_PLATFORM]: { type: 'string', value: 'aws_lambda' },
100+
[CLOUD_REGION]: { type: 'string', value: 'us-east-1' },
101+
[FAAS_MAX_MEMORY]: { type: 'integer', value: 2_147_483_648 },
102+
[FAAS_COLDSTART]: { type: 'boolean', value: request === 'first' },
103+
[FAAS_INVOCATION_ID]: { type: 'string', value: request },
104+
[AWS_LAMBDA_WEB_ENDPOINT_NAME]: { type: 'string', value: `${request}-endpoint` },
105+
},
106+
});
107+
}
108+
109+
expect(segmentSpans).toHaveLength(tracing ? 2 : 0);
110+
});
111+
});

‎packages/astro/src/index.server.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export {
9595
nativeNodeFetchIntegration,
9696
NodeClient,
9797
nodeContextIntegration,
98+
awsLambdaWebIntegration,
9899
onUncaughtExceptionIntegration,
99100
onUnhandledRejectionIntegration,
100101
mistralAIIntegration,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export {
7878
type NodeRuntimeMetricsOptions,
7979
contextLinesIntegration,
8080
nodeContextIntegration,
81+
awsLambdaWebIntegration,
8182
localVariablesIntegration,
8283
requestDataIntegration,
8384
functionToStringIntegration,

‎packages/google-cloud-serverless/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export {
7878
type NodeRuntimeMetricsOptions,
7979
contextLinesIntegration,
8080
nodeContextIntegration,
81+
awsLambdaWebIntegration,
8182
localVariablesIntegration,
8283
requestDataIntegration,
8384
fsIntegration,

‎packages/node/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { httpIntegration } from './integrations/http';
2+
export { awsLambdaWebIntegration } from './integrations/awsLambdaWeb';
23
export { nativeNodeFetchIntegration } from './integrations/node-fetch';
34
export { fsIntegration } from './integrations/fs';
45
// oxlint-disable-next-line typescript/no-deprecated

0 commit comments

Comments
 (0)