diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index b4763f675a35..ae86d94f73e1 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -71,6 +71,7 @@ "@opentelemetry/semantic-conventions": "^1.37.0", "@sentry/core": "10.23.0", "@sentry/node": "10.23.0", + "@sentry/node-core": "10.23.0", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { diff --git a/packages/aws-serverless/src/init.ts b/packages/aws-serverless/src/init.ts index e19cc41baf46..25180a41f6e6 100644 --- a/packages/aws-serverless/src/init.ts +++ b/packages/aws-serverless/src/init.ts @@ -2,6 +2,7 @@ import type { Integration, Options } from '@sentry/core'; import { applySdkMetadata, debug, getSDKSource } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node'; +import { envToBool } from '@sentry/node-core'; import { DEBUG_BUILD } from './debug-build'; import { awsIntegration } from './integration/aws'; import { awsLambdaIntegration } from './integration/awslambda'; @@ -54,7 +55,10 @@ export function getDefaultIntegrations(_options: Options): Integration[] { export interface AwsServerlessOptions extends NodeOptions { /** - * If Sentry events should be proxied through the Lambda extension when using the Lambda layer. Defaults to `true` when using the Lambda layer. + * If Sentry events should be proxied through the Lambda extension when using the Lambda layer. + * Defaults to `true` when using the Lambda layer. + * + * Can also be configured via the `SENTRY_LAYER_EXTENSION` environment variable. */ useLayerExtension?: boolean; } @@ -68,31 +72,41 @@ export function init(options: AwsServerlessOptions = {}): NodeClient | undefined const sdkSource = getSDKSource(); const proxyWouldInterfere = shouldDisableLayerExtensionForProxy(); + // Determine useLayerExtension value with the following priority: + // 1. Explicit option value (if provided) + // 2. Environment variable SENTRY_LAYER_EXTENSION (if set) + // 3. Default logic based on sdkSource, tunnel, and proxy settings + const useLayerExtensionFromEnv = envToBool(process.env.SENTRY_LAYER_EXTENSION, { strict: true }); + const defaultUseLayerExtension = sdkSource === 'aws-lambda-layer' && !options.tunnel && !proxyWouldInterfere; + const useLayerExtension = options.useLayerExtension ?? useLayerExtensionFromEnv ?? defaultUseLayerExtension; + const opts = { defaultIntegrations: getDefaultIntegrations(options), - useLayerExtension: sdkSource === 'aws-lambda-layer' && !options.tunnel && !proxyWouldInterfere, + useLayerExtension, ...options, }; if (opts.useLayerExtension) { - if (sdkSource === 'aws-lambda-layer') { - if (!opts.tunnel) { - DEBUG_BUILD && debug.log('Proxying Sentry events through the Sentry Lambda extension'); - opts.tunnel = 'http://localhost:9000/envelope'; - } else { + if (sdkSource !== 'aws-lambda-layer') { + DEBUG_BUILD && debug.warn('The Sentry Lambda extension is only supported when using the AWS Lambda layer.'); + } else if (opts.tunnel || proxyWouldInterfere) { + if (opts.tunnel) { DEBUG_BUILD && debug.warn( `Using a custom tunnel with the Sentry Lambda extension is not supported. Events will be tunnelled to ${opts.tunnel} and not through the extension.`, ); } + + if (proxyWouldInterfere) { + DEBUG_BUILD && + debug.warn( + 'Sentry Lambda extension is disabled due to proxy environment variables (http_proxy/https_proxy). Consider adding localhost to no_proxy to re-enable.', + ); + } } else { - DEBUG_BUILD && debug.warn('The Sentry Lambda extension is only supported when using the AWS Lambda layer.'); + DEBUG_BUILD && debug.log('Proxying Sentry events through the Sentry Lambda extension'); + opts.tunnel = 'http://localhost:9000/envelope'; } - } else if (sdkSource === 'aws-lambda-layer' && proxyWouldInterfere) { - DEBUG_BUILD && - debug.warn( - 'Sentry Lambda extension disabled due to proxy environment variables (http_proxy/https_proxy). Consider adding localhost to no_proxy to re-enable.', - ); } applySdkMetadata(opts, 'aws-serverless', ['aws-serverless'], sdkSource); diff --git a/packages/aws-serverless/test/init.test.ts b/packages/aws-serverless/test/init.test.ts index e6a675ecc43f..500338dc7144 100644 --- a/packages/aws-serverless/test/init.test.ts +++ b/packages/aws-serverless/test/init.test.ts @@ -19,9 +19,13 @@ const mockInitWithoutDefaultIntegrations = vi.mocked(initWithoutDefaultIntegrati describe('init', () => { beforeEach(() => { + // Clear all mocks between tests + vi.clearAllMocks(); + // Clean up environment variables between tests delete process.env.http_proxy; delete process.env.no_proxy; + delete process.env.SENTRY_LAYER_EXTENSION; }); describe('Lambda extension setup', () => { @@ -386,4 +390,96 @@ describe('init', () => { ); }); }); + + describe('SENTRY_LAYER_EXTENSION environment variable', () => { + test('should enable useLayerExtension when SENTRY_LAYER_EXTENSION=true', () => { + process.env.SENTRY_LAYER_EXTENSION = 'true'; + mockGetSDKSource.mockReturnValue('aws-lambda-layer'); + const options: AwsServerlessOptions = {}; + + init(options); + + expect(mockInitWithoutDefaultIntegrations).toHaveBeenCalledWith( + expect.objectContaining({ + useLayerExtension: true, + tunnel: 'http://localhost:9000/envelope', + }), + ); + }); + + test('should disable useLayerExtension when SENTRY_LAYER_EXTENSION=false', () => { + process.env.SENTRY_LAYER_EXTENSION = 'false'; + mockGetSDKSource.mockReturnValue('aws-lambda-layer'); + const options: AwsServerlessOptions = {}; + + init(options); + + expect(mockInitWithoutDefaultIntegrations).toHaveBeenCalledWith( + expect.objectContaining({ + useLayerExtension: false, + }), + ); + expect(mockInitWithoutDefaultIntegrations).toHaveBeenCalledWith( + expect.not.objectContaining({ + tunnel: expect.any(String), + }), + ); + }); + + test('should fall back to default behavior when SENTRY_LAYER_EXTENSION is not set', () => { + mockGetSDKSource.mockReturnValue('aws-lambda-layer'); + const options: AwsServerlessOptions = {}; + + init(options); + + expect(mockInitWithoutDefaultIntegrations).toHaveBeenCalledWith( + expect.objectContaining({ + useLayerExtension: true, + tunnel: 'http://localhost:9000/envelope', + }), + ); + }); + + test('should prioritize explicit option over environment variable', () => { + process.env.SENTRY_LAYER_EXTENSION = 'true'; + mockGetSDKSource.mockReturnValue('aws-lambda-layer'); + const options: AwsServerlessOptions = { + useLayerExtension: false, + }; + + init(options); + + expect(mockInitWithoutDefaultIntegrations).toHaveBeenCalledWith( + expect.objectContaining({ + useLayerExtension: false, + }), + ); + expect(mockInitWithoutDefaultIntegrations).toHaveBeenCalledWith( + expect.not.objectContaining({ + tunnel: expect.any(String), + }), + ); + }); + + test('should not set tunnel even tho useLayerExtension is set via env var when proxy is explicitly set', () => { + process.env.http_proxy = 'http://proxy.example.com:8080'; + process.env.SENTRY_LAYER_EXTENSION = 'true'; + mockGetSDKSource.mockReturnValue('aws-lambda-layer'); + const options: AwsServerlessOptions = {}; + + init(options); + + // useLayerExtension is respected but tunnel is not set due to proxy interference + expect(mockInitWithoutDefaultIntegrations).toHaveBeenCalledWith( + expect.objectContaining({ + useLayerExtension: true, + }), + ); + expect(mockInitWithoutDefaultIntegrations).toHaveBeenCalledWith( + expect.not.objectContaining({ + tunnel: expect.any(String), + }), + ); + }); + }); });