diff --git a/packages/aws-lambda/src/wrapper.js b/packages/aws-lambda/src/wrapper.js index 8ba73be0e0..a66fe0dbaf 100644 --- a/packages/aws-lambda/src/wrapper.js +++ b/packages/aws-lambda/src/wrapper.js @@ -73,7 +73,7 @@ function shimmedHandler(originalHandler, originalThis, originalArgs, _config) { const arnInfo = arnParser(context); const tracingEnabled = init(event, arnInfo, _config); - if (!tracingEnabled) { + if (!tracingEnabled || skipAssetCalls(event)) { return originalHandler.apply(originalThis, originalArgs); } @@ -544,6 +544,46 @@ function postHandlerForTimeout(entrySpan, remainingMillis) { }); } +/** + * Determines whether a Lambda invocation corresponds to a browser-generated asset request + * (such as CSS, JS, images, favicon, or source maps). + * These requests typically do not require tracing and can be safely skipped + * to reduce noise in tracing spans. + */ +function skipAssetCalls(event) { + if (!event) return false; + + const isAssetTracingDisabled = + process.env.INSTANA_TRACING_DISABLE_LAMBDA_ASSET_CALLS && + process.env.INSTANA_TRACING_DISABLE_LAMBDA_ASSET_CALLS === 'true'; + + const requestContext = event.requestContext || {}; + const httpContext = requestContext.http || {}; + const path = httpContext.path || event.rawPath || event.path; + + if (!path || typeof path !== 'string') return false; + + const assetPattern = /^\/favicon\.ico$|\/.*\.(ico|png|jpg|jpeg|gif|svg|css|js|map)$/i; + + if (assetPattern.test(path)) { + // Skip asset calls only if explicitly configured to do so, for safe migration. + // In the next major release, this behavior will become the default. + if (isAssetTracingDisabled) { + logger.debug(`Skipping browser-generated asset request: ${path}`); + return true; + } + + logger.warn( + `Asset request detected: ${path}. Starting from the next major release, asset calls will be skipped by default. + To enable this behavior now, set the environment variable 'INSTANA_TRACING_DISABLE_LAMBDA_ASSET_CALLS'=true.` + ); + + return false; + } + + return false; +} + exports.currentSpan = function getHandleForCurrentSpan() { return tracing.getHandleForCurrentSpan(); }; diff --git a/packages/aws-lambda/test/integration_test/test_definition.js b/packages/aws-lambda/test/integration_test/test_definition.js index 673bc27ff5..33c306cfb3 100644 --- a/packages/aws-lambda/test/integration_test/test_definition.js +++ b/packages/aws-lambda/test/integration_test/test_definition.js @@ -110,6 +110,10 @@ function prelude(opts) { env.SERVER_TIMING_HEADER = opts.serverTiming; } + if (opts.disableAssetCalls) { + env.INSTANA_TRACING_DISABLE_LAMBDA_ASSET_CALLS = true; + } + // The option useExtension controls whether the Lambda under test should try to talk to the extension or to the back // end directly. if (opts.useExtension) { @@ -3222,6 +3226,111 @@ function registerTests(handlerDefinitionPath, reduced) { }); }); }); + + describeOrSkipIfReduced(reduced)('when handling browser asset requests', function () { + const assetPaths = [ + '/favicon.ico', + '/assets/style.css', + '/js/bundle.js', + '/images/logo.png', + '/assets/main.js.map' + ]; + describeOrSkipIfReduced(reduced)('default case', function () { + let control; + before(async () => { + const env = prelude.bind(this)({ + handlerDefinitionPath, + trigger: 'function-url', + instanaAgentKey + }); + + control = new Control({ + faasRuntimePath: path.join(__dirname, '../runtime_mock'), + handlerDefinitionPath, + startBackend: true, + env + }); + + await control.start(); + }); + + beforeEach(async () => { + await control.reset(); + await control.resetBackendSpansAndMetrics(); + }); + + after(async () => { + await control.stop(); + }); + + assetPaths.forEach(assetPath => { + it('should trace browser asset requests', () => { + return verify( + control, + { + error: false, + expectMetrics: true, + expectSpans: true, + trigger: 'aws:lambda.function.url' + }, + { + payloadFormatVersion: '2.0', + assetPath: assetPath + } + ); + }); + }); + }); + + describeOrSkipIfReduced(reduced)('when INSTANA_TRACING_DISABLE_LAMBDA_ASSET_CALLS=true', function () { + let control; + before(async () => { + const env = prelude.bind(this)({ + handlerDefinitionPath, + trigger: 'function-url', + instanaAgentKey, + disableAssetCalls: true + }); + + control = new Control({ + faasRuntimePath: path.join(__dirname, '../runtime_mock'), + handlerDefinitionPath, + startBackend: true, + env + }); + + await control.start(); + }); + + beforeEach(async () => { + await control.reset(); + await control.resetBackendSpansAndMetrics(); + }); + + after(async () => { + await control.stop(); + }); + + assetPaths.forEach(assetPath => { + it('must skip tracing for browser asset requests', () => { + return verify( + control, + { + error: false, + expectMetrics: false, + expectSpans: false, + trigger: 'aws:lambda.function.url' + }, + { + payloadFormatVersion: '2.0', + assetPath: assetPath + } + ); + }); + }); + }); + }); + describeOrSkipIfReduced(reduced)('triggered by AWS lambda function url', function () { const env = prelude.bind(this)({ handlerDefinitionPath, diff --git a/packages/aws-lambda/test/runtime_mock/index.js b/packages/aws-lambda/test/runtime_mock/index.js index 30d893cf4a..3710234a36 100644 --- a/packages/aws-lambda/test/runtime_mock/index.js +++ b/packages/aws-lambda/test/runtime_mock/index.js @@ -622,12 +622,12 @@ function createEvent(error, trigger, eventOpts) { case 'function-url': event.version = '2.0'; event.rawQueryString = ''; - event.rawPath = '/path/to'; + event.rawPath = eventOpts && eventOpts.assetPath ? eventOpts.assetPath : '/path/to'; event.routeKey = '$default'; event.requestContext = { http: { method: 'GET', - path: '/path/to' + path: eventOpts && eventOpts.assetPath ? eventOpts.assetPath : '/path/to' }, domainName: 'xxxxxx.lambda-url.region.on.aws' };