Skip to content
Draft
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
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