From ab36aee08589b92ad7c40a44005f51d62b55a6ab Mon Sep 17 00:00:00 2001 From: Clara Poncet Date: Thu, 20 Aug 2026 16:39:32 +0200 Subject: [PATCH 1/6] Skip AppSec for non-HTTP Lambda triggers and report _dd.appsec.unsupported_event_type Co-Authored-By: Claude Opus 5 --- .../LambdaHandlerInstrumentationTest.java | 31 +++- .../trace/lambda/LambdaAppSecHandler.java | 31 +++- .../trace/lambda/LambdaEventParser.java | 84 +++------- .../trace/lambda/LambdaAppSecHandlerTest.java | 147 ++++++++++++------ .../trace/lambda/LambdaEventParserTest.java | 12 +- 5 files changed, 179 insertions(+), 126 deletions(-) diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/LambdaHandlerInstrumentationTest.java b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/LambdaHandlerInstrumentationTest.java index a59aafc2710..614aa40c0ff 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/LambdaHandlerInstrumentationTest.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/LambdaHandlerInstrumentationTest.java @@ -288,6 +288,34 @@ void appSecCallbacksAreNotInvokedWhenAppSecIsDisabled() throws IOException { assertTraces(trace(span().type(DDSpanTypes.SERVERLESS).error(false))); } + @Test + void appSecIsSkippedAndReportedUnsupportedForNonHttpEvent() throws IOException { + String eventJson = "{\"Records\": [{\"eventSource\": \"aws:sqs\", \"body\": \"hello\"}]}"; + + ByteArrayInputStream input = + new ByteArrayInputStream(eventJson.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + new HandlerStreaming().handleRequest(input, output, newContext()); + + assertFalse(appSecStarted); + assertNull(capturedMethod); + assertNull(capturedPath); + assertTrue(capturedHeaders.isEmpty()); + assertNull(capturedBody); + assertFalse(appSecEnded); + assertNull(capturedResponseStatus); + // Tag matching is exhaustive, so this also asserts the span carries no http.* tag + assertTraces( + trace( + span() + .type(DDSpanTypes.SERVERLESS) + .error(false) + .tags( + defaultTags(), + tag("request_id", is(REQUEST_ID)), + tag("_dd.appsec.unsupported_event_type", is(1))))); + } + @Test void responseCallbacksAreInvokedForJsonEncodedResponse() throws IOException { String eventJson = @@ -382,7 +410,8 @@ void responseCallbacksSkipNonApiGatewayResponseForNonHttpEvent() throws IOExcept assertTrue(capturedResponseHeaders.isEmpty()); assertNull(capturedResponseBody); assertFalse(responseHeaderDoneCalled); - assertTrue(appSecEnded); + // AppSec skipped the invocation entirely, so there is no request context to end + assertFalse(appSecEnded); assertTraces(trace(span().type(DDSpanTypes.SERVERLESS).error(false))); } diff --git a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java index 84d25ccdf8e..47567cd03e9 100644 --- a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java +++ b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java @@ -57,6 +57,12 @@ public class LambdaAppSecHandler { private static final Logger log = LoggerFactory.getLogger(LambdaAppSecHandler.class); private static final RatelimitedLogger rlLog = new RatelimitedLogger(log, 5, TimeUnit.MINUTES); + /** + * Marks an invocation AppSec did not process because the trigger is not HTTP. Mirrors {@code + * _dd.appsec.unsupported_event_type} in the Python and Node.js Lambda tracers. + */ + private static final String UNSUPPORTED_EVENT_TYPE_METRIC = "_dd.appsec.unsupported_event_type"; + // Carries the detected trigger type from processRequestStart to processResponseData within the // same Lambda invocation. Cleared in processRequestEnd. private static final ThreadLocal CURRENT_TRIGGER_TYPE = new ThreadLocal<>(); @@ -66,9 +72,13 @@ public class LambdaAppSecHandler { * gateway callbacks on the parsed event, and, for recognised HTTP triggers, applies the HTTP tags * to the returned context so they land on the invocation span at creation. * + *

Invocations whose trigger is not an HTTP one are skipped entirely, as in the Python and + * Node.js Lambda tracers; {@link #processRequestEnd(AgentSpan)} marks them on the span instead. + * * @param event the Lambda event object * @return a {@link TagContext} carrying the AppSec request context and the HTTP tags, or null if - * AppSec is disabled, the event is not a parseable payload, or processing fails + * AppSec is disabled, the trigger is not HTTP, the event is not a parseable payload, or + * processing fails */ public static AgentSpanContext processRequestStart(Object event) { if (!ActiveSubsystems.APPSEC_ACTIVE) { @@ -91,6 +101,10 @@ public static AgentSpanContext processRequestStart(Object event) { return null; } CURRENT_TRIGGER_TYPE.set(eventData.triggerType); + if (!eventData.triggerType.isHttp()) { + log.debug("Trigger type {} is not HTTP, skipping AppSec processing", eventData.triggerType); + return null; + } // v2 payloads carry the request line verbatim; the others expose the path and a decoded // parameter map only, so the query string has to be rebuilt from them String fullPath = eventData.rawUri; @@ -100,7 +114,7 @@ public static AgentSpanContext processRequestStart(Object event) { LambdaURIDataAdapter uriAdapter = new LambdaURIDataAdapter(fullPath, eventData.headers, eventData.host); AgentSpanContext context = processAppSecRequestData(eventData, uriAdapter); - if (context instanceof TagContext && eventData.triggerType.isHttp()) { + if (context instanceof TagContext) { applyHttpTags((TagContext) context, eventData, uriAdapter); } return context; @@ -112,17 +126,28 @@ public static AgentSpanContext processRequestStart(Object event) { /** * Invokes the requestEnded gateway callback to add AppSec data to the span, propagates the - * sampling decision of trace-tagging rules, and clears the per-invocation state. + * sampling decision of trace-tagging rules, and clears the per-invocation state. Invocations + * skipped by {@link #processRequestStart(Object)} because their trigger is not HTTP are marked + * with {@value #UNSUPPORTED_EVENT_TYPE_METRIC} instead. * * @param span the current span */ public static void processRequestEnd(AgentSpan span) { + LambdaTriggerType triggerType = CURRENT_TRIGGER_TYPE.get(); CURRENT_TRIGGER_TYPE.remove(); if (!ActiveSubsystems.APPSEC_ACTIVE || span == null) { return; } + // A null trigger type means processRequestStart never ran for this invocation, which is not an + // HTTP request either. Both cases are reported the same way, and nothing below applies since + // no AppSec request context was created. + if (triggerType == null || !triggerType.isHttp()) { + span.setMetric(UNSUPPORTED_EVENT_TYPE_METRIC, 1); + return; + } + RequestContext requestContext = span.getRequestContext(); if (requestContext != null) { AgentTracer.TracerAPI tracer = AgentTracer.get(); diff --git a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaEventParser.java b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaEventParser.java index fdb03e6edbb..1848e78b620 100644 --- a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaEventParser.java +++ b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaEventParser.java @@ -94,8 +94,9 @@ static LambdaRequestData parseEvent(String json) { case ALB_MULTI_VALUE: return extractAlbData(event, triggerType); default: - log.debug("Unknown trigger type, attempting generic extraction"); - return extractGenericData(event); + // Unsupported trigger: AppSec skips the invocation entirely, so there is nothing to + // extract. The trigger type is carried by the caller, not by this result. + return LambdaRequestData.EMPTY; } } catch (Exception e) { log.debug("Failed to parse event data from JSON", e); @@ -444,69 +445,6 @@ private static LambdaRequestData extractAlbData( null); } - /** Generic data extraction for unknown trigger types (fallback) */ - private static LambdaRequestData extractGenericData(Map event) { - Map headers = extractHeadersWithCookies(event); - Map pathParameters = extractPathParameters(event.get("pathParameters")); - Map> queryParameters = - extractQueryParameters(event.get("queryStringParameters")); - Object body = extractBody(event); - - String method = null; - String path = null; - String sourceIp = null; - - // Try to extract from requestContext if available - Object requestContextObj = event.get("requestContext"); - if (requestContextObj instanceof Map) { - Map requestContext = (Map) requestContextObj; - - Object httpObj = requestContext.get("http"); - if (httpObj instanceof Map) { - Map http = (Map) httpObj; - method = (String) http.get("method"); - path = (String) http.get("path"); - sourceIp = (String) http.get("sourceIp"); - } else { - Object methodObj = requestContext.get("httpMethod"); - if (methodObj != null) { - method = String.valueOf(methodObj); - } - - Object identityObj = requestContext.get("identity"); - if (identityObj instanceof Map) { - Map identity = (Map) identityObj; - sourceIp = (String) identity.get("sourceIp"); - } - } - } - - // Try root level fields - if (method == null) { - Object methodObj = event.get("httpMethod"); - if (methodObj != null) { - method = String.valueOf(methodObj); - } - } - if (path == null) { - Object pathObj = event.get("path"); - if (pathObj != null) { - path = String.valueOf(pathObj); - } - } - - return new LambdaRequestData( - headers, - method, - path, - sourceIp, - null, - LambdaTriggerType.UNKNOWN, - pathParameters, - queryParameters, - body); - } - /** * Looks a header up in a map produced by {@link #extractHeaders}, whose keys are already * lowercased, so {@code lowerCaseName} must be lowercase for a match. @@ -790,8 +728,22 @@ enum LambdaTriggerType { LAMBDA_URL, // Lambda Function URL UNKNOWN; // Unknown or unsupported trigger + /** + * Whitelist rather than {@code != UNKNOWN} so a trigger type added later defaults to non-HTTP, + * and therefore to being skipped by AppSec, until it is deliberately listed here. + */ boolean isHttp() { - return this != UNKNOWN; + switch (this) { + case API_GATEWAY_V1_REST: + case API_GATEWAY_V2_HTTP: + case API_GATEWAY_V2_WEBSOCKET: + case ALB: + case ALB_MULTI_VALUE: + case LAMBDA_URL: + return true; + default: + return false; + } } } diff --git a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java index 431fcf833be..f0d72d6b62e 100644 --- a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import datadog.trace.api.Config; @@ -138,6 +139,27 @@ void processRequestStartReturnsNullForMalformedJson() { assertNull(LambdaAppSecHandler.processRequestStart(event)); } + @Test + @SuppressWarnings("unchecked") + void processRequestStartSkipsAppSecForNonHttpTrigger() { + String sqsEvent = + "{\"Records\": [{\"eventSource\": \"aws:sqs\", \"body\": \"hello\"," + + " \"messageAttributes\": {}}]}"; + ByteArrayInputStream event = createInputStream(sqsEvent); + + Supplier> requestStartedCallback = mock(Supplier.class); + CallbackProvider mockCallbackProvider = mock(CallbackProvider.class); + when(mockCallbackProvider.getCallback(EVENTS.requestStarted())) + .thenReturn(requestStartedCallback); + AgentTracer.TracerAPI mockTracer = mock(AgentTracer.TracerAPI.class); + when(mockTracer.getCallbackProvider(RequestContextSlot.APPSEC)) + .thenReturn(mockCallbackProvider); + AgentTracer.forceRegister(mockTracer); + + assertNull(LambdaAppSecHandler.processRequestStart(event)); + verifyNoInteractions(requestStartedCallback); + } + @Test void streamCanBeReadMultipleTimesAfterProcessing() throws IOException { String jsonData = "{\"test\": \"data\", \"requestContext\": {\"httpMethod\": \"GET\"}}"; @@ -903,47 +925,11 @@ void handlesBodyWithSpecialCharacters() { } // ============================================================================ - // Generic Data Extraction Tests + // Partial Payload Extraction Tests // ============================================================================ @Test - void extractsDataFromUnknownTriggerTypeUsingGenericExtraction() { - String eventJson = - "{" - + "\"path\": \"/generic/path\"," - + "\"httpMethod\": \"PATCH\"," - + "\"headers\": {\"x-custom-header\": \"generic-value\"}," - + "\"unknownField\": \"should be ignored\"," - + "\"requestContext\": {\"identity\": {\"sourceIp\": \"203.0.113.1\"}}" - + "}"; - ByteArrayInputStream event = createInputStream(eventJson); - - String[] capturedMethod = {null}; - String[] capturedPath = {null}; - Map capturedHeaders = new HashMap<>(); - String[] capturedSourceIp = {null}; - - setupMockCallbacks( - new Callbacks() - .onMethodUri( - (method, uri) -> { - capturedMethod[0] = method; - capturedPath[0] = uri.path(); - }) - .onHeader(capturedHeaders::put) - .onSocketAddress((ip, port) -> capturedSourceIp[0] = ip)); - - AgentSpanContext result = LambdaAppSecHandler.processRequestStart(event); - - assertNotNull(result); - assertEquals("PATCH", capturedMethod[0]); - assertEquals("/generic/path", capturedPath[0]); - assertEquals("generic-value", capturedHeaders.get("x-custom-header")); - assertEquals("203.0.113.1", capturedSourceIp[0]); - } - - @Test - void extractsDataFromUnknownTriggerWithHttpInRequestContext() { + void extractsDataFromLambdaUrlWithHttpInRequestContext() { String eventJson = "{" + "\"requestContext\": {" @@ -974,7 +960,7 @@ void extractsDataFromUnknownTriggerWithHttpInRequestContext() { } @Test - void genericExtractionUsesHttpMethodFromRequestContext() { + void extractsHttpMethodFromRequestContextOfApiGatewayV1Payload() { String eventJson = "{\"path\": \"/ctx-method\", \"requestContext\": {\"httpMethod\": \"DELETE\"}}"; ByteArrayInputStream event = createInputStream(eventJson); @@ -1059,6 +1045,7 @@ void processRequestEndDoesNothingWhenAppSecIsDisabled() { @Test void processRequestEndDoesNothingWhenSpanHasNoRequestContext() { + LambdaAppSecHandler.setCurrentTriggerType(LambdaTriggerType.API_GATEWAY_V1_REST); AgentSpan span = mock(AgentSpan.class); when(span.getRequestContext()).thenReturn(null); LambdaAppSecHandler.processRequestEnd(span); @@ -1068,6 +1055,7 @@ void processRequestEndDoesNothingWhenSpanHasNoRequestContext() { @Test @SuppressWarnings("unchecked") void processRequestEndInvokesCallbackAndSkipsAsmTagsWhenNotManuallyKept() { + LambdaAppSecHandler.setCurrentTriggerType(LambdaTriggerType.API_GATEWAY_V1_REST); AppSecContext mockAppSecContext = mock(AppSecContext.class); when(mockAppSecContext.isManuallyKept()).thenReturn(false); TraceSegment mockTraceSegment = mock(TraceSegment.class); @@ -1097,6 +1085,7 @@ void processRequestEndInvokesCallbackAndSkipsAsmTagsWhenNotManuallyKept() { @Test void processRequestEndHandlesNullRequestEndedCallbackGracefully() { + LambdaAppSecHandler.setCurrentTriggerType(LambdaTriggerType.API_GATEWAY_V1_REST); RequestContext mockRequestContext = mock(RequestContext.class); AgentSpan span = mock(AgentSpan.class); when(span.getRequestContext()).thenReturn(mockRequestContext); @@ -1115,6 +1104,7 @@ void processRequestEndHandlesNullRequestEndedCallbackGracefully() { @Test @SuppressWarnings("unchecked") void processRequestEndSetsAsmKeepTagWhenAppSecContextIsManuallyKept() { + LambdaAppSecHandler.setCurrentTriggerType(LambdaTriggerType.API_GATEWAY_V1_REST); AppSecContext manuallyKeptCtx = mock(AppSecContext.class); when(manuallyKeptCtx.isManuallyKept()).thenReturn(true); @@ -1145,6 +1135,75 @@ void processRequestEndSetsAsmKeepTagWhenAppSecContextIsManuallyKept() { verify(mockTraceSegment).setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); } + @Test + void processRequestEndSetsUnsupportedEventTypeMetricForNonHttpTrigger() { + LambdaAppSecHandler.setCurrentTriggerType(LambdaTriggerType.UNKNOWN); + AgentSpan span = mock(AgentSpan.class); + + LambdaAppSecHandler.processRequestEnd(span); + + verify(span).setMetric("_dd.appsec.unsupported_event_type", 1); + verifyNoMoreInteractions(span); + } + + @Test + void processRequestEndSetsUnsupportedEventTypeMetricWhenNoTriggerTypeWasRecorded() { + AgentSpan span = mock(AgentSpan.class); + + LambdaAppSecHandler.processRequestEnd(span); + + verify(span).setMetric("_dd.appsec.unsupported_event_type", 1); + verifyNoMoreInteractions(span); + } + + @Test + void processRequestEndSetsNoUnsupportedEventTypeMetricWhenAppSecIsDisabled() { + ActiveSubsystems.APPSEC_ACTIVE = false; + LambdaAppSecHandler.setCurrentTriggerType(LambdaTriggerType.UNKNOWN); + AgentSpan span = mock(AgentSpan.class); + + LambdaAppSecHandler.processRequestEnd(span); + + verifyNoInteractions(span); + } + + @Test + @SuppressWarnings("unchecked") + void processRequestEndSetsNoUnsupportedEventTypeMetricForHttpTrigger() { + LambdaAppSecHandler.setCurrentTriggerType(LambdaTriggerType.API_GATEWAY_V1_REST); + RequestContext mockRequestContext = mock(RequestContext.class); + when(mockRequestContext.getTraceSegment()).thenReturn(mock(TraceSegment.class)); + AgentSpan span = mock(AgentSpan.class); + when(span.getRequestContext()).thenReturn(mockRequestContext); + + BiFunction> requestEndedCallback = + mock(BiFunction.class); + when(requestEndedCallback.apply(any(), any())).thenReturn(new Flow.ResultFlow<>(null)); + CallbackProvider mockCallbackProvider = mock(CallbackProvider.class); + when(mockCallbackProvider.getCallback(EVENTS.requestEnded())).thenReturn(requestEndedCallback); + AgentTracer.TracerAPI mockTracer = mock(AgentTracer.TracerAPI.class); + when(mockTracer.getCallbackProvider(RequestContextSlot.APPSEC)) + .thenReturn(mockCallbackProvider); + AgentTracer.forceRegister(mockTracer); + + LambdaAppSecHandler.processRequestEnd(span); + + verify(requestEndedCallback).apply(mockRequestContext, span); + verify(span, never()).setMetric(anyString(), anyInt()); + } + + @Test + void processRequestEndSetsUnsupportedEventTypeMetricAfterAnUnparseablePayload() { + ByteArrayInputStream event = createInputStream("{invalid json"); + assertNull(LambdaAppSecHandler.processRequestStart(event)); + + AgentSpan span = mock(AgentSpan.class); + LambdaAppSecHandler.processRequestEnd(span); + + verify(span).setMetric("_dd.appsec.unsupported_event_type", 1); + verifyNoMoreInteractions(span); + } + // ============================================================================ // mergeContexts Tests // ============================================================================ @@ -2236,16 +2295,6 @@ void omitsRouteTagForAlbEvent() { assertEquals("alb-agent", tags.get(Tags.HTTP_USER_AGENT)); } - @Test - void appliesNoHttpTagsForNonHttpEvent() { - setupMockCallbacks(new Callbacks()); - AgentSpanContext context = - LambdaAppSecHandler.processRequestStart( - createInputStream("{\"Records\": [{\"eventSource\": \"aws:sqs\", \"body\": \"hi\"}]}")); - - assertTrue(tagsOf(context).isEmpty()); - } - @Test void mergeContextsCopiesHttpTagsIntoExtensionContext() { TagContext appSecContext = new TagContext(); diff --git a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaEventParserTest.java b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaEventParserTest.java index 9afd9d9ffa4..7811ab23c97 100644 --- a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaEventParserTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaEventParserTest.java @@ -270,13 +270,11 @@ void queryParametersKeepEventOrder() { // ============================================================================ @Test - void nonHttpEventHasNoHostOrRoute() { - LambdaRequestData data = - parseEvent("{\"Records\": [{\"eventSource\": \"aws:sqs\", \"body\": \"hello\"}]}"); - - assertEquals(LambdaTriggerType.UNKNOWN, data.triggerType); - assertNull(data.host); - assertNull(data.route); + void nonHttpEventIsNotExtracted() { + // AppSec skips non-HTTP triggers entirely, so nothing is extracted from their payload + assertSame( + LambdaRequestData.EMPTY, + parseEvent("{\"Records\": [{\"eventSource\": \"aws:sqs\", \"body\": \"hello\"}]}")); } @Test From 2ffcab51ef9c65cd6794614a0e834897c3bc9e4c Mon Sep 17 00:00:00 2001 From: Clara Poncet Date: Thu, 20 Aug 2026 17:43:29 +0200 Subject: [PATCH 2/6] Do not report an unsupported event type when AppSec was inactive at request start Co-Authored-By: Claude Opus 5 --- .../trace/lambda/LambdaAppSecHandler.java | 20 ++++++------------- .../trace/lambda/LambdaAppSecHandlerTest.java | 6 +++--- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java index 47567cd03e9..fdc8fe67637 100644 --- a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java +++ b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java @@ -57,10 +57,7 @@ public class LambdaAppSecHandler { private static final Logger log = LoggerFactory.getLogger(LambdaAppSecHandler.class); private static final RatelimitedLogger rlLog = new RatelimitedLogger(log, 5, TimeUnit.MINUTES); - /** - * Marks an invocation AppSec did not process because the trigger is not HTTP. Mirrors {@code - * _dd.appsec.unsupported_event_type} in the Python and Node.js Lambda tracers. - */ + /** Marks an invocation AppSec did not process because the trigger is not HTTP. */ private static final String UNSUPPORTED_EVENT_TYPE_METRIC = "_dd.appsec.unsupported_event_type"; // Carries the detected trigger type from processRequestStart to processResponseData within the @@ -72,9 +69,6 @@ public class LambdaAppSecHandler { * gateway callbacks on the parsed event, and, for recognised HTTP triggers, applies the HTTP tags * to the returned context so they land on the invocation span at creation. * - *

Invocations whose trigger is not an HTTP one are skipped entirely, as in the Python and - * Node.js Lambda tracers; {@link #processRequestEnd(AgentSpan)} marks them on the span instead. - * * @param event the Lambda event object * @return a {@link TagContext} carrying the AppSec request context and the HTTP tags, or null if * AppSec is disabled, the trigger is not HTTP, the event is not a parseable payload, or @@ -103,6 +97,7 @@ public static AgentSpanContext processRequestStart(Object event) { CURRENT_TRIGGER_TYPE.set(eventData.triggerType); if (!eventData.triggerType.isHttp()) { log.debug("Trigger type {} is not HTTP, skipping AppSec processing", eventData.triggerType); + // unsupported event metric is added on request end since span doesn't exist yet return null; } // v2 payloads carry the request line verbatim; the others expose the path and a decoded @@ -126,9 +121,7 @@ public static AgentSpanContext processRequestStart(Object event) { /** * Invokes the requestEnded gateway callback to add AppSec data to the span, propagates the - * sampling decision of trace-tagging rules, and clears the per-invocation state. Invocations - * skipped by {@link #processRequestStart(Object)} because their trigger is not HTTP are marked - * with {@value #UNSUPPORTED_EVENT_TYPE_METRIC} instead. + * sampling decision of trace-tagging rules, and clears the per-invocation state. * * @param span the current span */ @@ -140,10 +133,9 @@ public static void processRequestEnd(AgentSpan span) { return; } - // A null trigger type means processRequestStart never ran for this invocation, which is not an - // HTTP request either. Both cases are reported the same way, and nothing below applies since - // no AppSec request context was created. - if (triggerType == null || !triggerType.isHttp()) { + // A null trigger type means processRequestStart never ran, so the invocation was not analysed + // at all, which is not the same as an unsupported trigger. + if (triggerType != null && !triggerType.isHttp()) { span.setMetric(UNSUPPORTED_EVENT_TYPE_METRIC, 1); return; } diff --git a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java index f0d72d6b62e..33e68f528e2 100644 --- a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java @@ -1147,13 +1147,13 @@ void processRequestEndSetsUnsupportedEventTypeMetricForNonHttpTrigger() { } @Test - void processRequestEndSetsUnsupportedEventTypeMetricWhenNoTriggerTypeWasRecorded() { + void processRequestEndSetsNoUnsupportedEventTypeMetricWhenNoTriggerTypeWasRecorded() { + // AppSec was inactive at request start and enabled mid-invocation AgentSpan span = mock(AgentSpan.class); LambdaAppSecHandler.processRequestEnd(span); - verify(span).setMetric("_dd.appsec.unsupported_event_type", 1); - verifyNoMoreInteractions(span); + verify(span, never()).setMetric(anyString(), anyInt()); } @Test From 652ee8de98320518aaaebd5254b36e97caac4e8b Mon Sep 17 00:00:00 2001 From: Clara Poncet Date: Fri, 28 Aug 2026 13:32:02 +0200 Subject: [PATCH 3/6] clarify cases covered by unsupported metric --- .../main/java/datadog/trace/lambda/LambdaAppSecHandler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java index fdc8fe67637..68b0fb774b5 100644 --- a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java +++ b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java @@ -57,7 +57,9 @@ public class LambdaAppSecHandler { private static final Logger log = LoggerFactory.getLogger(LambdaAppSecHandler.class); private static final RatelimitedLogger rlLog = new RatelimitedLogger(log, 5, TimeUnit.MINUTES); - /** Marks an invocation AppSec did not process because the trigger is not HTTP. */ + /** Marks an invocation AppSec did not process because the trigger is not HTTP, or if + * the even is unreadable (not a {@code ByteArrayInputStream}, empty, oversized, or + * unparseable). */ private static final String UNSUPPORTED_EVENT_TYPE_METRIC = "_dd.appsec.unsupported_event_type"; // Carries the detected trigger type from processRequestStart to processResponseData within the From 4059ca16da9d216be27b478af57b1788dee6641a Mon Sep 17 00:00:00 2001 From: Clara Poncet Date: Fri, 28 Aug 2026 13:52:15 +0200 Subject: [PATCH 4/6] formatting --- .../java/datadog/trace/lambda/LambdaAppSecHandler.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java index 68b0fb774b5..37ff0a0d735 100644 --- a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java +++ b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java @@ -57,9 +57,10 @@ public class LambdaAppSecHandler { private static final Logger log = LoggerFactory.getLogger(LambdaAppSecHandler.class); private static final RatelimitedLogger rlLog = new RatelimitedLogger(log, 5, TimeUnit.MINUTES); - /** Marks an invocation AppSec did not process because the trigger is not HTTP, or if - * the even is unreadable (not a {@code ByteArrayInputStream}, empty, oversized, or - * unparseable). */ + /** + * Marks an invocation AppSec did not process because the trigger is not HTTP, or if the even is + * unreadable (not a {@code ByteArrayInputStream}, empty, oversized, or unparseable). + */ private static final String UNSUPPORTED_EVENT_TYPE_METRIC = "_dd.appsec.unsupported_event_type"; // Carries the detected trigger type from processRequestStart to processResponseData within the From 7cb7f8d8716fe31bf998a07458810a1a46238073 Mon Sep 17 00:00:00 2001 From: Clara Poncet Date: Fri, 28 Aug 2026 17:45:55 +0200 Subject: [PATCH 5/6] Update dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java Co-authored-by: Joey Zhao <5253430+joeyzhao2018@users.noreply.github.com> --- .../src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java index 37ff0a0d735..467d57f11f0 100644 --- a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java +++ b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java @@ -132,7 +132,7 @@ public static void processRequestEnd(AgentSpan span) { LambdaTriggerType triggerType = CURRENT_TRIGGER_TYPE.get(); CURRENT_TRIGGER_TYPE.remove(); - if (!ActiveSubsystems.APPSEC_ACTIVE || span == null) { +if (triggerType==null || !ActiveSubsystems.APPSEC_ACTIVE || span == null) { return; } From ed790671fb75f8b16ca45d1085ca706cc8576de5 Mon Sep 17 00:00:00 2001 From: Clara Poncet Date: Fri, 28 Aug 2026 17:49:50 +0200 Subject: [PATCH 6/6] move triggerType == null guard --- .../main/java/datadog/trace/lambda/LambdaAppSecHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java index 467d57f11f0..4aac101e98a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java +++ b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java @@ -132,13 +132,13 @@ public static void processRequestEnd(AgentSpan span) { LambdaTriggerType triggerType = CURRENT_TRIGGER_TYPE.get(); CURRENT_TRIGGER_TYPE.remove(); -if (triggerType==null || !ActiveSubsystems.APPSEC_ACTIVE || span == null) { + if (!ActiveSubsystems.APPSEC_ACTIVE || span == null || triggerType == null) { return; } // A null trigger type means processRequestStart never ran, so the invocation was not analysed // at all, which is not the same as an unsupported trigger. - if (triggerType != null && !triggerType.isHttp()) { + if (!triggerType.isHttp()) { span.setMetric(UNSUPPORTED_EVENT_TYPE_METRIC, 1); return; }