Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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!'; | ||
| }); | ||
| }; |
| 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, | ||
| }, | ||
| }, | ||
| }), | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
andreiborza marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const zipFilename = `sentry-node-serverless-${version}.zip`; | ||
| console.log(`Creating final layer zip file ${zipFilename}.`); | ||
| // need to preserve the symlink above with -y | ||
| 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' })); | ||
| } | ||
| }); | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| server.listen(9000, () => { | ||
| DEBUG_BUILD && debug.log('Sentry proxy listening on port 9000'); | ||
msonnb marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }); | ||
| server.on('error', err => { | ||
| DEBUG_BUILD && debug.error('Error starting Sentry proxy', err); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| } | ||
| 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__; |
| 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); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
awsLambdaExtensionBundleConfigis missing thelicensePlugin. 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.