Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
3793baf
fix(aws-serverless): Keep the Lambda extension polling past 300s invo…
LuccaRebelloToledo Sep 8, 2026
8abefea
fix(aws-serverless): Never let the extension stop polling or exit mid…
LuccaRebelloToledo Sep 17, 2026
9bda050
test(aws-serverless): Cover a gzipped envelope through the layer tunnel
LuccaRebelloToledo Sep 17, 2026
1764199
chore(e2e): Ignore what a test application writes when it runs locally
LuccaRebelloToledo Sep 17, 2026
cf16fd5
fix(aws-serverless): Read the envelope header of a gzipped body a chu…
LuccaRebelloToledo Sep 17, 2026
9ebab6d
ref(aws-serverless): Drop the array branch from the tunnel's content-…
LuccaRebelloToledo Sep 17, 2026
13820d4
docs(aws-serverless): Correct what opens the init gate
LuccaRebelloToledo Sep 17, 2026
1323981
test(aws-serverless): Derive the gzip test sizes from the threshold t…
LuccaRebelloToledo Sep 17, 2026
6b33889
test(aws-serverless): Size the gzip tests from our own bound, not the…
LuccaRebelloToledo Sep 17, 2026
c05f350
fix(aws-serverless): Measure poll health by delivery, not by elapsed …
LuccaRebelloToledo Sep 24, 2026
286a8f7
fix(aws-serverless): Park on a rejection that escaped main
LuccaRebelloToledo Sep 24, 2026
7b83675
fix(aws-serverless): Stop trusting what the envelope tunnel is handed
LuccaRebelloToledo Sep 24, 2026
56d2a3f
test(aws-serverless): Restore two rationales the module split orphaned
LuccaRebelloToledo Sep 24, 2026
c697247
test(aws-serverless): Score the poll loop by its own clock, not the d…
LuccaRebelloToledo Sep 24, 2026
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
6 changes: 6 additions & 0 deletions dev-packages/e2e-tests/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ tmp
pnpm-lock.yaml
.last-run.json
packed

# Written by Playwright on a failing run, in whichever application failed
test-results

# Synthesised from the CDK stack on every run of this one application
test-applications/aws-serverless-layer/sam.template.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const zlib = require('node:zlib');

function makeHex(length) {
return Array.from({ length }, () => Math.floor(Math.random() * 16).toString(16)).join('');
}
Expand All @@ -15,17 +17,25 @@ exports.handler = async event => {
event_id: makeHex(32),
message: event?.marker ?? 'lambda-extension-tunnel-test',
level: 'info',
// `makeNodeTransport` only gzips past 32KiB, so a compressed envelope smaller than that never
// exercises what the tunnel does with the ones the SDK actually compresses.
...(event?.padTo ? { padding: 'x'.repeat(Number(event.padTo)) } : {}),
};
const envelope = `${JSON.stringify(envelopeHeader)}\n${JSON.stringify(envelopeItemHeader)}\n${JSON.stringify(
envelopeItemPayload,
)}\n`;

// `makeNodeTransport` gzips any body over 32KiB, so the tunnel has to read a compressed
// envelope header. It could not, and answered 500 — silently dropping every large event.
const compressed = event?.gzip ? zlib.gzipSync(Buffer.from(envelope)) : undefined;

const response = await fetch('http://localhost:9000/envelope', {
method: 'POST',
headers: {
'Content-Type': 'application/x-sentry-envelope',
...(compressed ? { 'content-encoding': event.gzip } : {}),
},
body: envelope,
body: compressed ?? envelope,
});

const responseBody = await response.text();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,62 @@ test.describe('Lambda layer', () => {
expect(missingDsnResult.responseBody).toContain('missing DSN');
});

test('extension tunnel forwards a gzipped envelope', async ({ lambdaClient }) => {
// The tunnel read the envelope header off the raw bytes, which throws on the gzip magic bytes,
// so every large event was answered 500 and dropped. `makeNodeTransport` only gzips past this
// size — a private const in `@sentry/node`, so it is named rather than imported — and doubling
// it keeps the envelope over the line whatever the header costs. Only the encoding the SDK
// actually sends is exercised: the case and list-valued forms are covered in the extension's
// unit tests, and the event proxy decompresses this exact value only.
const sdkGzipThreshold = 32 * 1024;
const marker = `extension-tunnel-gzip-${Date.now()}`;
const requestPromise = waitForRequest('aws-serverless-layer', requestData => {
return requestData.rawProxyRequestBody.includes(marker);
});

const response = await lambdaClient.send(
new InvokeCommand({
FunctionName: 'LayerTunnel',
Payload: JSON.stringify({ gzip: 'gzip', marker, padTo: sdkGzipThreshold * 2 }),
}),
);

expect(parseLambdaPayload(response.Payload).status).toBe(200);
await requestPromise;
});

test('extension tunnel rejects a gzipped envelope carrying an unauthorized DSN', async ({ lambdaClient }) => {
// The allowlist has to survive compression, or it is bypassed by setting one header.
const probe = parseLambdaPayload(
(
await lambdaClient.send(
new InvokeCommand({
FunctionName: 'LayerTunnel',
Payload: JSON.stringify({ marker: `gzip-dsn-probe-${Date.now()}` }),
}),
)
).Payload,
);
// Asserted, not assumed: without a real DSN to mangle, the tunnel answers 403 `Invalid DSN`
// and this test would pass without the allowlist ever being consulted.
expect(probe.status).toBe(200);
expect(probe.attemptedDsn).toContain('://public@');

const response = await lambdaClient.send(
new InvokeCommand({
FunctionName: 'LayerTunnel',
Payload: JSON.stringify({
gzip: 'gzip',
dsn: probe.attemptedDsn!.replace('://public@', '://unauthorized@'),
}),
}),
);

const result = parseLambdaPayload(response.Payload);
expect(result.status).toBe(403);
expect(result.responseBody).toContain('DSN not allowed');
});

test('extension tunnel forwards requests when SENTRY_DSN is missing', async ({ lambdaClient }) => {
const marker = `extension-tunnel-no-sentry-dsn-${Date.now()}`;
const noDsnRequestPromise = waitForRequest('aws-serverless-layer', requestData => {
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-serverless/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export function init(options: AwsServerlessOptions = {}): NodeClient | undefined
}
} else {
DEBUG_BUILD && debug.log('Proxying Sentry events through the Sentry Lambda extension');
// Kept literal: importing it from the extension's tree would ship that module in the SDK
// bundle. `test/init.test.ts` asserts the two halves still agree.
opts.tunnel = 'http://localhost:9000/envelope';
}
}
Expand Down
Loading
Loading