From 09f713f1c97fccc7bbdd11ff5a2ee7f8637d5676 Mon Sep 17 00:00:00 2001 From: Carles Capell Date: Sun, 4 Jan 2026 19:37:07 +0100 Subject: [PATCH 1/4] Standarize Span type with 'web' value --- src/trace/span-inferrer.spec.ts | 32 ++++++++++++++++---------------- src/trace/span-inferrer.ts | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/trace/span-inferrer.spec.ts b/src/trace/span-inferrer.spec.ts index 19eb5709..84ac2b86 100644 --- a/src/trace/span-inferrer.spec.ts +++ b/src/trace/span-inferrer.spec.ts @@ -899,7 +899,7 @@ describe("SpanInferrer", () => { service: "08se3mvh28.execute-api.eu-west-1.amazonaws.com", "service.name": "08se3mvh28.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", }, }); }); @@ -926,7 +926,7 @@ describe("SpanInferrer", () => { service: "id.execute-api.us-east-1.amazonaws.com", "service.name": "id.execute-api.us-east-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "$default", }, }); @@ -954,7 +954,7 @@ describe("SpanInferrer", () => { service: "r3pmxmplak.execute-api.us-east-2.amazonaws.com", "service.name": "r3pmxmplak.execute-api.us-east-2.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "default", }, }); @@ -982,7 +982,7 @@ describe("SpanInferrer", () => { service: "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com", "service.name": "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }); @@ -1010,7 +1010,7 @@ describe("SpanInferrer", () => { service: "9vj54we5ih.execute-api.sa-east-1.amazonaws.com", "service.name": "9vj54we5ih.execute-api.sa-east-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "$default", }, }); @@ -1126,7 +1126,7 @@ describe("Authorizer Spans", () => { service: "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "service.name": "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1151,7 +1151,7 @@ describe("Authorizer Spans", () => { service: "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "service.name": "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1181,7 +1181,7 @@ describe("Authorizer Spans", () => { service: "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "service.name": "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1211,7 +1211,7 @@ describe("Authorizer Spans", () => { service: "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "service.name": "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1236,7 +1236,7 @@ describe("Authorizer Spans", () => { service: "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "service.name": "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1266,7 +1266,7 @@ describe("Authorizer Spans", () => { service: "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "service.name": "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1296,7 +1296,7 @@ describe("Authorizer Spans", () => { service: "l9flvsey83.execute-api.eu-west-1.amazonaws.com", "service.name": "l9flvsey83.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "$default", }, }, @@ -1326,7 +1326,7 @@ describe("Authorizer Spans", () => { service: "l9flvsey83.execute-api.eu-west-1.amazonaws.com", "service.name": "l9flvsey83.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "$default", }, }, @@ -1357,7 +1357,7 @@ describe("Authorizer Spans", () => { service: "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "service.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", }, }, ]); @@ -1382,7 +1382,7 @@ describe("Authorizer Spans", () => { service: "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "service.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", }, }, ]); @@ -1412,7 +1412,7 @@ describe("Authorizer Spans", () => { service: "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "service.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", }, }, ]); diff --git a/src/trace/span-inferrer.ts b/src/trace/span-inferrer.ts index 34466006..c9d1f82c 100644 --- a/src/trace/span-inferrer.ts +++ b/src/trace/span-inferrer.ts @@ -127,7 +127,7 @@ export class SpanInferrer { request_id: context?.awsRequestId, service: serviceName, "service.name": serviceName, - "span.type": "http", + "span.type": "web", "resource.name": resourceName, "peer.service": this.service, "span.kind": "server", From b46de0415ec1b15e682caed7c6998d2f2c326070 Mon Sep 17 00:00:00 2001 From: Carles Capell Date: Sun, 4 Jan 2026 19:52:09 +0100 Subject: [PATCH 2/4] Remove operation_name for API GW inferred spans --- src/trace/span-inferrer.spec.ts | 16 ---------------- src/trace/span-inferrer.ts | 4 +--- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/trace/span-inferrer.spec.ts b/src/trace/span-inferrer.spec.ts index 84ac2b86..d5b22ca7 100644 --- a/src/trace/span-inferrer.spec.ts +++ b/src/trace/span-inferrer.spec.ts @@ -891,7 +891,6 @@ describe("SpanInferrer", () => { event_type: "CONNECT", "http.url": "https://08se3mvh28.execute-api.eu-west-1.amazonaws.com$connect", message_direction: "IN", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "08se3mvh28.execute-api.eu-west-1.amazonaws.com $connect", @@ -917,7 +916,6 @@ describe("SpanInferrer", () => { endpoint: "/my/path", "http.url": "https://id.execute-api.us-east-1.amazonaws.com/my/path", domain_name: "id.execute-api.us-east-1.amazonaws.com", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "http.method": "GET", @@ -945,7 +943,6 @@ describe("SpanInferrer", () => { endpoint: "/default/nodejs-apig-function-1G3XMPLZXVXYI", "http.url": "https://r3pmxmplak.execute-api.us-east-2.amazonaws.com/default/nodejs-apig-function-1G3XMPLZXVXYI", domain_name: "r3pmxmplak.execute-api.us-east-2.amazonaws.com", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "http.method": "GET", @@ -973,7 +970,6 @@ describe("SpanInferrer", () => { endpoint: "/dev/user/42", "http.url": "https://mcwkra0ya4.execute-api.sa-east-1.amazonaws.com/dev/user/42", domain_name: "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "http.method": "GET", @@ -1001,7 +997,6 @@ describe("SpanInferrer", () => { endpoint: "/user/42", "http.url": "https://9vj54we5ih.execute-api.sa-east-1.amazonaws.com/user/42", domain_name: "9vj54we5ih.execute-api.sa-east-1.amazonaws.com", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "http.method": "GET", @@ -1118,7 +1113,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hello", "http.method": "POST", "http.url": "https://3gsxz7lha4.execute-api.eu-west-1.amazonaws.com/dev/hello", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "POST /hello", @@ -1143,7 +1137,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hello", "http.method": "POST", "http.url": "https://3gsxz7lha4.execute-api.eu-west-1.amazonaws.com/dev/hello", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "POST /hello", @@ -1173,7 +1166,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hello", "http.method": "POST", "http.url": "https://3gsxz7lha4.execute-api.eu-west-1.amazonaws.com/dev/hello", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "POST /hello", @@ -1203,7 +1195,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hi", "http.method": "GET", "http.url": "https://4dyr9xqip7.execute-api.eu-west-1.amazonaws.com/dev/hi", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hi", @@ -1228,7 +1219,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hi", "http.method": "GET", "http.url": "https://4dyr9xqip7.execute-api.eu-west-1.amazonaws.com/dev/hi", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hi", @@ -1258,7 +1248,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hi", "http.method": "GET", "http.url": "https://4dyr9xqip7.execute-api.eu-west-1.amazonaws.com/dev/hi", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hi", @@ -1288,7 +1277,6 @@ describe("Authorizer Spans", () => { endpoint: "/hello", "http.method": "GET", "http.url": "https://l9flvsey83.execute-api.eu-west-1.amazonaws.com/hello", - operation_name: "aws.httpapi", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hello", @@ -1318,7 +1306,6 @@ describe("Authorizer Spans", () => { endpoint: "/hello", "http.method": "GET", "http.url": "https://l9flvsey83.execute-api.eu-west-1.amazonaws.com/hello", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hello", @@ -1349,7 +1336,6 @@ describe("Authorizer Spans", () => { event_type: "CONNECT", "http.url": "https://85fj5nw29d.execute-api.eu-west-1.amazonaws.com$connect", message_direction: "IN", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com $connect", @@ -1374,7 +1360,6 @@ describe("Authorizer Spans", () => { event_type: "CONNECT", "http.url": "https://85fj5nw29d.execute-api.eu-west-1.amazonaws.com$connect", message_direction: "IN", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com $connect", @@ -1404,7 +1389,6 @@ describe("Authorizer Spans", () => { event_type: "MESSAGE", "http.url": "https://85fj5nw29d.execute-api.eu-west-1.amazonaws.comhello", message_direction: "IN", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com hello", diff --git a/src/trace/span-inferrer.ts b/src/trace/span-inferrer.ts index c9d1f82c..649f5804 100644 --- a/src/trace/span-inferrer.ts +++ b/src/trace/span-inferrer.ts @@ -120,7 +120,6 @@ export class SpanInferrer { const serviceName = SpanInferrer.determineServiceName(apiId, "lambda_api_gateway", domain, domain); options.tags = { - operation_name: "aws.apigateway", "http.url": httpUrl, endpoint: path, resource_names: resourceName, @@ -160,12 +159,11 @@ export class SpanInferrer { // getting an approximated endTime if (eventSourceSubType === HTTPEventSubType.ApiGatewayV2) { options.startTime = startTime; // not inserting authorizer span - options.tags.operation_name = "aws.httpapi"; } else { upstreamSpanOptions = { startTime, childOf: parentSpanContext, - tags: { operation_name: "aws.apigateway.authorizer", ...options.tags }, + tags: { ...options.tags }, }; upstreamAuthorizerSpan = new SpanWrapper( this.traceWrapper.startSpan("aws.apigateway.authorizer", upstreamSpanOptions), From 41e3af8b1be29e195a9db5b1afe0ad72e13a1a3a Mon Sep 17 00:00:00 2001 From: Carles Capell Date: Sun, 4 Jan 2026 20:55:13 +0100 Subject: [PATCH 3/4] Appsec enabled metric --- src/trace/span-inferrer.spec.ts | 30 ++++++++++++++++++++++++++++++ src/trace/span-inferrer.ts | 8 ++++++++ 2 files changed, 38 insertions(+) diff --git a/src/trace/span-inferrer.spec.ts b/src/trace/span-inferrer.spec.ts index d5b22ca7..674eb1a3 100644 --- a/src/trace/span-inferrer.spec.ts +++ b/src/trace/span-inferrer.spec.ts @@ -1066,6 +1066,36 @@ describe("SpanInferrer", () => { }, }); }); + + it("includes _dd.appsec.enabled tag when DD_APPSEC_ENABLED is true", () => { + process.env.DD_APPSEC_ENABLED = "true"; + const inferrer = new SpanInferrer(mockWrapper as unknown as TracerWrapper); + inferrer.createInferredSpan(apiGatewayV1, {} as any, {} as SpanContext); + + const callArgs = mockWrapper.startSpan.mock.calls[0]; + expect(callArgs[1].tags["_dd.appsec.enabled"]).toBe(1); + delete process.env.DD_APPSEC_ENABLED; + }); + + it("does not include _dd.appsec.enabled tag when DD_APPSEC_ENABLED is not set", () => { + delete process.env.DD_APPSEC_ENABLED; + delete process.env.DD_SERVERLESS_APPSEC_ENABLED; + const inferrer = new SpanInferrer(mockWrapper as unknown as TracerWrapper); + inferrer.createInferredSpan(apiGatewayV1, {} as any, {} as SpanContext); + + const callArgs = mockWrapper.startSpan.mock.calls[0]; + expect(callArgs[1].tags["_dd.appsec.enabled"]).toBeUndefined(); + }); + + it("includes _dd.appsec.enabled tag when DD_SERVERLESS_APPSEC_ENABLED is true", () => { + process.env.DD_SERVERLESS_APPSEC_ENABLED = "true"; + const inferrer = new SpanInferrer(mockWrapper as unknown as TracerWrapper); + inferrer.createInferredSpan(apiGatewayV1, {} as any, {} as SpanContext); + + const callArgs = mockWrapper.startSpan.mock.calls[0]; + expect(callArgs[1].tags["_dd.appsec.enabled"]).toBe(1); + delete process.env.DD_SERVERLESS_APPSEC_ENABLED; + }); }); const mockFinish = () => undefined; diff --git a/src/trace/span-inferrer.ts b/src/trace/span-inferrer.ts index 649f5804..c1bc5719 100644 --- a/src/trace/span-inferrer.ts +++ b/src/trace/span-inferrer.ts @@ -97,6 +97,10 @@ export class SpanInferrer { return extractedKey?.trim() ? extractedKey : fallback; } + private static isAppsecEnabled(): boolean { + return process.env.DD_APPSEC_ENABLED === "true" || process.env.DD_SERVERLESS_APPSEC_ENABLED === "true"; + } + createInferredSpanForApiGateway( event: any, context: Context | undefined, @@ -148,6 +152,10 @@ export class SpanInferrer { options.tags.connection_id = event.requestContext.connectionId; options.tags.event_type = event.requestContext.eventType; } + + if (SpanInferrer.isAppsecEnabled()) { + options.tags["_dd.appsec.enabled"] = 1; + } let upstreamAuthorizerSpan: SpanWrapper | undefined; const eventSourceSubType: HTTPEventSubType = HTTPEventTraceExtractor.getEventSubType(event); if (decodeAuthorizerContext) { From 78e4da2cbd5d81fb7a1c9cea30031a17d79610fb Mon Sep 17 00:00:00 2001 From: Carles Capell Date: Mon, 5 Jan 2026 06:59:04 +0100 Subject: [PATCH 4/4] Include appsec events in inferred spans --- src/trace/listener.spec.ts | 98 ++++++++++++++++++++++++++++++++++++++ src/trace/listener.ts | 9 ++++ 2 files changed, 107 insertions(+) diff --git a/src/trace/listener.spec.ts b/src/trace/listener.spec.ts index 1e8dc189..37bd4aa4 100644 --- a/src/trace/listener.spec.ts +++ b/src/trace/listener.spec.ts @@ -16,6 +16,7 @@ let mockExtract: jest.Mock; let mockSpanContextWrapper: any; let mockSpanContext: any; let mockTraceSource: TraceSource | undefined = undefined; +let mockCurrentSpan: any = null; jest.mock("./tracer-wrapper", () => { mockWrap = jest.fn().mockImplementation((name, options, func) => func); @@ -25,6 +26,10 @@ jest.mock("./tracer-wrapper", () => { constructor() {} + get currentSpan(): any { + return mockCurrentSpan; + } + wrap(name: any, options: any, fn: any): any { return mockWrap(name, options, fn); } @@ -560,4 +565,97 @@ describe("TraceListener", () => { ); }); }); + + describe("AppSec tag propagation", () => { + beforeEach(() => { + mockWrap.mockClear(); + mockExtract.mockClear(); + mockSpanContext = undefined; + mockSpanContextWrapper = undefined; + mockTraceSource = undefined; + mockCurrentSpan = null; + process.env = { ...oldEnv }; + }); + + afterEach(() => { + process.env = oldEnv; + mockCurrentSpan = null; + }); + + it("copies _dd.appsec.json from lambda span to inferred span when present", async () => { + const mockSetTag = jest.fn(); + const appsecJsonValue = '{"triggers":[{"rule":{"id":"rule-1"}}]}'; + + mockCurrentSpan = { + _tags: { + "_dd.appsec.json": appsecJsonValue, + }, + }; + + const listener = new TraceListener(defaultConfig); + await listener.onStartInvocation({}, context as any); + + const mockInferredSpan = { + isAsync: () => false, + setTag: mockSetTag, + finish: jest.fn(), + }; + (listener as any).inferredSpan = mockInferredSpan; + + const unwrappedFunc = () => {}; + const wrappedFunc = listener.onWrap(unwrappedFunc); + wrappedFunc(); + await listener.onCompleteInvocation(); + + expect(mockSetTag).toHaveBeenCalledWith("_dd.appsec.json", appsecJsonValue); + }); + + it("does not set _dd.appsec.json on inferred span when not present on lambda span", async () => { + const mockSetTag = jest.fn(); + + mockCurrentSpan = { + _tags: {}, + }; + + const listener = new TraceListener(defaultConfig); + await listener.onStartInvocation({}, context as any); + + const mockInferredSpan = { + isAsync: () => false, + setTag: mockSetTag, + finish: jest.fn(), + }; + (listener as any).inferredSpan = mockInferredSpan; + + const unwrappedFunc = () => {}; + const wrappedFunc = listener.onWrap(unwrappedFunc); + wrappedFunc(); + await listener.onCompleteInvocation(); + + expect(mockSetTag).not.toHaveBeenCalledWith("_dd.appsec.json", expect.anything()); + }); + + it("does not set _dd.appsec.json when lambda span is not available", async () => { + const mockSetTag = jest.fn(); + + mockCurrentSpan = null; + + const listener = new TraceListener(defaultConfig); + await listener.onStartInvocation({}, context as any); + + const mockInferredSpan = { + isAsync: () => false, + setTag: mockSetTag, + finish: jest.fn(), + }; + (listener as any).inferredSpan = mockInferredSpan; + + const unwrappedFunc = () => {}; + const wrappedFunc = listener.onWrap(unwrappedFunc); + wrappedFunc(); + await listener.onCompleteInvocation(); + + expect(mockSetTag).not.toHaveBeenCalledWith("_dd.appsec.json", expect.anything()); + }); + }); }); diff --git a/src/trace/listener.ts b/src/trace/listener.ts index 1e312a71..2be06a2b 100644 --- a/src/trace/listener.ts +++ b/src/trace/listener.ts @@ -267,6 +267,15 @@ export class TraceListener { logDebug("Setting error tag to inferred span"); this.inferredSpan.setTag("error", error); } + + const lambdaSpan = this.tracerWrapper.currentSpan; + if (lambdaSpan) { + const appsecJson = lambdaSpan._tags?.["_dd.appsec.json"]; + if (appsecJson) { + this.inferredSpan.setTag("_dd.appsec.json", appsecJson); + } + } + if (this.inferredSpan.isAsync()) { finishTime = this.wrappedCurrentSpan?.startTime() || Date.now(); } else {