Skip to content

Commit f51c50e

Browse files
committed
RUM-16886: Mark sampled-out network spans as droppable while preserving stats
CoreTracer needs to see all spans to calculate APM client-side stats. Since network spans have their own sampling rate, sampled-out spans are now finished with the `_dd.local.force_drop` tag instead of being dropped. This tells downstream systems to discard the span while still letting CoreTracer process it for stats calculations. This only applies when the active tracer is the SDK's own `DatadogTracerAdapter`, which routes through `CoreTraceWriter` — the only implementation that honors `_dd.local.force_drop`. When a custom or global `DatadogTracer` is in use, sampled-out spans are dropped via `drop()` as before, preserving backward compatibility.
1 parent 0795ece commit f51c50e

23 files changed

Lines changed: 426 additions & 135 deletions

File tree

features/dd-sdk-android-trace/api/apiSurface

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,15 @@ object com.datadog.android.trace.internal._TraceInternalProxy
9090
fun setSdkV2Compatible(com.datadog.android.trace.api.tracer.DatadogTracerBuilder): com.datadog.android.trace.api.tracer.DatadogTracerBuilder
9191
fun addThrowable(com.datadog.android.trace.api.span.DatadogSpan, Throwable, Byte)
9292
fun activateSpan(com.datadog.android.trace.api.tracer.DatadogTracer, com.datadog.android.trace.api.span.DatadogSpan, Boolean): com.datadog.android.trace.api.scope.DatadogScope?
93+
fun isSDKBackedTracer(com.datadog.android.trace.api.tracer.DatadogTracer): Boolean
9394
fun mergeBaggage(String?, String): String
9495
fun createApmNetworkInstrumentation(String, com.datadog.android.trace.ApmNetworkInstrumentationConfiguration)
96+
const val FORCE_DROP_SPAN: String
9597
fun com.datadog.android.core.sampling.Sampler<com.datadog.android.trace.api.span.DatadogSpan>.effectiveSampleRate(com.datadog.android.trace.api.span.DatadogSpan): Float?
9698
fun com.datadog.android.trace.api.span.DatadogSpanContext?.isDropped(): Boolean
9799
fun Int?.isDroppedPriority(): Boolean
98100
data class com.datadog.android.trace.internal.net.RequestTracingState
99-
constructor(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder, Boolean = false, com.datadog.android.trace.api.span.DatadogSpan? = null, Float? = null)
101+
constructor(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder, Boolean = false, com.datadog.android.trace.api.span.DatadogSpan? = null, Float? = null, Boolean)
100102
fun createRequestInfo(): com.datadog.android.api.instrumentation.network.HttpRequestInfo
101103
fun RequestTracingState?.toAttributesMap(String, String, String): Map<String, Any?>
102104
class com.datadog.android.trace.internal.net.SessionRebasedSampler : com.datadog.android.core.sampling.Sampler<com.datadog.android.trace.api.span.DatadogSpan>, SpanAwareSampler

features/dd-sdk-android-trace/api/dd-sdk-android-trace.api

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,33 +167,40 @@ public final class com/datadog/android/trace/internal/_TraceInternalProxy {
167167
public static final fun addThrowable (Lcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Throwable;B)V
168168
public final fun createApmNetworkInstrumentation (Ljava/lang/String;Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration;)Lcom/datadog/android/trace/internal/ApmNetworkInstrumentation;
169169
public final fun getPropagationHelper ()Lcom/datadog/android/trace/internal/DatadogPropagationHelper;
170+
public final fun isSDKBackedTracer (Lcom/datadog/android/trace/api/tracer/DatadogTracer;)Z
170171
public final fun mergeBaggage (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
171172
public final fun setSdkV2Compatible (Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder;)Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder;
172173
public final fun setTraceId128BitGenerationEnabled (Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder;)Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder;
173174
public final fun setTracingSamplingPriorityIfNecessary (Lcom/datadog/android/trace/api/span/DatadogSpanContext;)V
174175
}
175176

177+
public final class com/datadog/android/trace/internal/domain/event/MetaKeysKt {
178+
public static final field FORCE_DROP_SPAN Ljava/lang/String;
179+
}
180+
176181
public final class com/datadog/android/trace/internal/net/ApmNetworkInstrumentationExtKt {
177182
public static final fun effectiveSampleRate (Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/api/span/DatadogSpan;)Ljava/lang/Float;
178183
public static final fun isDropped (Lcom/datadog/android/trace/api/span/DatadogSpanContext;)Z
179184
public static final fun isDroppedPriority (Ljava/lang/Integer;)Z
180185
}
181186

182187
public final class com/datadog/android/trace/internal/net/RequestTracingState {
183-
public fun <init> (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;)V
184-
public synthetic fun <init> (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
188+
public fun <init> (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;Z)V
189+
public synthetic fun <init> (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
185190
public final fun component1 ()Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;
186191
public final fun component2 ()Z
187192
public final fun component3 ()Lcom/datadog/android/trace/api/span/DatadogSpan;
188193
public final fun component4 ()Ljava/lang/Float;
189-
public final fun copy (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;)Lcom/datadog/android/trace/internal/net/RequestTracingState;
190-
public static synthetic fun copy$default (Lcom/datadog/android/trace/internal/net/RequestTracingState;Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;ILjava/lang/Object;)Lcom/datadog/android/trace/internal/net/RequestTracingState;
194+
public final fun component5 ()Z
195+
public final fun copy (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;Z)Lcom/datadog/android/trace/internal/net/RequestTracingState;
196+
public static synthetic fun copy$default (Lcom/datadog/android/trace/internal/net/RequestTracingState;Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;ZILjava/lang/Object;)Lcom/datadog/android/trace/internal/net/RequestTracingState;
191197
public final fun createRequestInfo ()Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;
192198
public fun equals (Ljava/lang/Object;)Z
193199
public final fun getRequestInfoBuilder ()Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;
194200
public final fun getSampleRate ()Ljava/lang/Float;
195201
public final fun getSpan ()Lcom/datadog/android/trace/api/span/DatadogSpan;
196202
public fun hashCode ()I
203+
public final fun isSDKBacked ()Z
197204
public final fun isSampled ()Z
198205
public fun toString ()Ljava/lang/String;
199206
}

features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/ApmNetworkInstrumentation.kt

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class ApmNetworkInstrumentation internal constructor(
115115
}
116116

117117
if (sdkCore == null) {
118-
return RequestTracingState(requestInfoBuilder)
118+
return RequestTracingState(requestInfoBuilder = requestInfoBuilder, isSDKBacked = false)
119119
}
120120

121121
val tracer = tracerProvider.provideTracer(
@@ -125,7 +125,7 @@ class ApmNetworkInstrumentation internal constructor(
125125
)
126126

127127
if (tracer == null || !request.isTraceable(sdkCore)) {
128-
return RequestTracingState(requestInfoBuilder)
128+
return RequestTracingState(requestInfoBuilder = requestInfoBuilder, isSDKBacked = false)
129129
}
130130

131131
val span = tracer.buildSpan(
@@ -155,7 +155,8 @@ class ApmNetworkInstrumentation internal constructor(
155155
span = span,
156156
isSampled = isSampled,
157157
sampleRate = traceSampler.effectiveSampleRate(span),
158-
requestInfoBuilder = tracedRequestInfoBuilder
158+
requestInfoBuilder = tracedRequestInfoBuilder,
159+
isSDKBacked = tracer is DatadogTracerAdapter
159160
)
160161
}
161162

@@ -167,17 +168,19 @@ class ApmNetworkInstrumentation internal constructor(
167168
* @param response the HTTP response information.
168169
*/
169170
fun onResponseSucceeded(requestTracingState: RequestTracingState, response: HttpResponseInfo) {
170-
if (requestTracingState.isSampled) {
171-
requestTracingState.span?.setTag(DatadogTracingConstants.Tags.KEY_HTTP_STATUS, response.statusCode)
172-
if (response.statusCode in HttpURLConnection.HTTP_BAD_REQUEST until HttpURLConnection.HTTP_INTERNAL_ERROR) {
173-
requestTracingState.span?.isError = true
174-
}
175-
if (response.statusCode == HttpURLConnection.HTTP_NOT_FOUND && redacted404ResourceName) {
176-
requestTracingState.span?.resourceName = RESOURCE_NAME_404
177-
}
171+
requestTracingState.span?.setTag(DatadogTracingConstants.Tags.KEY_HTTP_STATUS, response.statusCode)
172+
if (response.statusCode in HttpURLConnection.HTTP_BAD_REQUEST until HttpURLConnection.HTTP_INTERNAL_ERROR) {
173+
requestTracingState.span?.isError = true
174+
}
175+
if (response.statusCode == HttpURLConnection.HTTP_NOT_FOUND && redacted404ResourceName) {
176+
requestTracingState.span?.resourceName = RESOURCE_NAME_404
178177
}
179178
requestTracingState.onRequestIntercepted(response, null)
180-
requestTracingState.span?.finishRumAware(requestTracingState.isSampled, canSendSpan)
179+
requestTracingState.span?.finishRumAware(
180+
requestTracingState.isSampled,
181+
canSendSpan,
182+
requestTracingState.isSDKBacked
183+
)
181184
}
182185

183186
/**
@@ -188,8 +191,8 @@ class ApmNetworkInstrumentation internal constructor(
188191
* @param throwable the exception that caused the failure.
189192
*/
190193
fun onResponseFailed(requestTracingState: RequestTracingState, throwable: Throwable) {
194+
requestTracingState.span?.isError = true
191195
if (requestTracingState.isSampled) {
192-
requestTracingState.span?.isError = true
193196
requestTracingState.span?.setTag(DatadogTracingConstants.Tags.KEY_ERROR_MSG, throwable.message)
194197
requestTracingState.span?.setTag(DatadogTracingConstants.Tags.KEY_ERROR_TYPE, throwable.javaClass.name)
195198
requestTracingState.span?.setTag(
@@ -198,7 +201,11 @@ class ApmNetworkInstrumentation internal constructor(
198201
)
199202
}
200203
requestTracingState.onRequestIntercepted(null, throwable)
201-
requestTracingState.span?.finishRumAware(requestTracingState.isSampled, canSendSpan)
204+
requestTracingState.span?.finishRumAware(
205+
requestTracingState.isSampled,
206+
canSendSpan,
207+
requestTracingState.isSDKBacked
208+
)
202209
}
203210

204211
/**

features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/_TraceInternalProxy.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ object _TraceInternalProxy {
9393
return (tracer as? DatadogTracerAdapter)?.activateSpan(span, asyncPropagating)
9494
}
9595

96+
/**
97+
* Returns true if [tracer] is the SDK's built-in tracer, meaning it routes spans through
98+
* CoreTraceWriter which honors the FORCE_DROP_SPAN tag. Custom tracer implementations
99+
* do not honor that tag, so callers must call drop() instead of setTag+finish() for them.
100+
*/
101+
fun isSDKBackedTracer(tracer: DatadogTracer): Boolean = tracer is DatadogTracerAdapter
102+
96103
/**
97104
* Merges two baggage headers into a single string representation. The [oldHeader] represents the
98105
* previously existing baggage header, and the [newHeader] represents the new baggage information

features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/data/CoreTraceWriter.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.datadog.android.event.NoOpEventMapper
1919
import com.datadog.android.trace.internal.RumContextPropagator
2020
import com.datadog.android.trace.internal.RumContextPropagator.Companion.extractRumContext
2121
import com.datadog.android.trace.internal.domain.event.ContextAwareMapper
22+
import com.datadog.android.trace.internal.domain.event.FORCE_DROP_SPAN
2223
import com.datadog.android.trace.internal.storage.ContextAwareSerializer
2324
import com.datadog.android.trace.model.SpanEvent
2425
import com.datadog.trace.api.sampling.PrioritySampling
@@ -45,7 +46,10 @@ internal class CoreTraceWriter(
4546
sdkCore.getFeature(Feature.TRACING_FEATURE_NAME)
4647
?.withWriteContext { datadogContext, writeScope ->
4748
val writeSpans = trace
48-
.filter { it.getTraceSamplingPriority() !in DROP_SAMPLING_PRIORITIES }
49+
.filter {
50+
it.getTraceSamplingPriority() !in DROP_SAMPLING_PRIORITIES &&
51+
it.getTag(FORCE_DROP_SPAN) == null
52+
}
4953
.map { it.extractRumContext(rumContextPropagator) }
5054
// TODO RUM-4092 Add the capability in the serializer to handle multiple spans in one payload
5155
writeScope {

features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/domain/event/MetaKeys.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
package com.datadog.android.trace.internal.domain.event
88

9+
import com.datadog.android.lint.InternalApi
10+
11+
/**
12+
* Internal tag which indicates the span should be created locally but never sent to the backend.
13+
*/
14+
@InternalApi
15+
const val FORCE_DROP_SPAN: String = "_dd.local.force_drop"
916
internal const val TRACE_ID_META_KEY = "_dd.p.id"
1017
internal const val APPLICATION_VARIANT_KEY = "variant"
1118
internal const val COMPUTE_STATS_META_KEY = "_dd.compute_stats"

features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/ApmNetworkInstrumentationExt.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.datadog.android.trace.internal.ApmNetworkInstrumentation.Companion.ZE
2323
import com.datadog.android.trace.internal.ParentContextSource
2424
import com.datadog.android.trace.internal._TraceInternalProxy
2525
import com.datadog.android.trace.internal._TraceInternalProxy.propagationHelper
26+
import com.datadog.android.trace.internal.domain.event.FORCE_DROP_SPAN
2627
import java.util.Locale
2728

2829
internal val FeatureSdkCore?.isRumEnabled: Boolean
@@ -71,8 +72,12 @@ internal fun DatadogSpan.sample(
7172
}
7273
}
7374

74-
internal fun DatadogSpan.finishRumAware(isSampled: Boolean, canSendSpan: Boolean) {
75-
if (canSendSpan && isSampled) {
75+
internal fun DatadogSpan.finishRumAware(isSampled: Boolean, canSendSpan: Boolean, isSDKBacked: Boolean) {
76+
if (canSendSpan && (isSDKBacked || isSampled)) {
77+
if (isSDKBacked && !isSampled) {
78+
// SDK-backed tracer: tag the span so CoreTraceWriter suppresses it instead of calling drop()
79+
setTag(FORCE_DROP_SPAN, true)
80+
}
7681
finish()
7782
} else {
7883
drop()

features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/RequestTracingState.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ import com.datadog.android.trace.api.span.DatadogSpan
2323
* @property isSampled whether the request trace was sampled.
2424
* @property span the trace span created for this request, or null if not sampled or tracing is disabled.
2525
* @property sampleRate the sample rate used for the sampling decision.
26+
* @property isSDKBacked whether the tracer is the SDK's built-in implementation
2627
*/
2728
@InternalApi
2829
data class RequestTracingState(
2930
val requestInfoBuilder: HttpRequestInfoBuilder,
3031
val isSampled: Boolean = false,
3132
val span: DatadogSpan? = null,
32-
val sampleRate: Float? = null
33+
val sampleRate: Float? = null,
34+
val isSDKBacked: Boolean
3335
) {
3436

3537
/**

features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/TraceInternalProxyTest.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package com.datadog.android.trace.internal
88

99
import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration
1010
import com.datadog.android.trace.TracingHeaderType
11+
import com.datadog.android.trace.api.tracer.DatadogTracer
1112
import com.datadog.android.utils.forge.Configurator
1213
import fr.xgouchet.elmyr.Forge
1314
import fr.xgouchet.elmyr.annotation.StringForgery
@@ -18,8 +19,10 @@ import org.junit.jupiter.api.BeforeEach
1819
import org.junit.jupiter.api.Test
1920
import org.junit.jupiter.api.extension.ExtendWith
2021
import org.junit.jupiter.api.extension.Extensions
22+
import org.mockito.Mock
2123
import org.mockito.junit.jupiter.MockitoExtension
2224
import org.mockito.junit.jupiter.MockitoSettings
25+
import org.mockito.kotlin.mock
2326
import org.mockito.quality.Strictness
2427

2528
@Extensions(
@@ -30,6 +33,9 @@ import org.mockito.quality.Strictness
3033
@ForgeConfiguration(Configurator::class)
3134
internal class TraceInternalProxyTest {
3235

36+
@Mock
37+
lateinit var mockCustomTracer: DatadogTracer
38+
3339
private lateinit var fakeTracedHosts: Map<String, Set<TracingHeaderType>>
3440

3541
@StringForgery
@@ -83,4 +89,16 @@ internal class TraceInternalProxyTest {
8389
assertThat(result.sdkInstanceName).isEqualTo(fakeSdkInstanceName)
8490
assertThat(result.traceOrigin).isEqualTo(fakeTraceOrigin)
8591
}
92+
93+
@Test
94+
fun `M return false W isSDKBackedTracer() {custom tracer}`() {
95+
assertThat(_TraceInternalProxy.isSDKBackedTracer(mockCustomTracer)).isFalse()
96+
}
97+
98+
@Test
99+
fun `M return true W isSDKBackedTracer() {SDK DatadogTracerAdapter}`() {
100+
val sdkTracer: DatadogTracer = mock<DatadogTracerAdapter>()
101+
102+
assertThat(_TraceInternalProxy.isSDKBackedTracer(sdkTracer)).isTrue()
103+
}
86104
}

features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/data/CoreTraceWriterTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.datadog.android.event.EventMapper
1919
import com.datadog.android.log.LogAttributes
2020
import com.datadog.android.trace.internal.RumContextPropagator
2121
import com.datadog.android.trace.internal.domain.event.ContextAwareMapper
22+
import com.datadog.android.trace.internal.domain.event.FORCE_DROP_SPAN
2223
import com.datadog.android.trace.internal.storage.ContextAwareSerializer
2324
import com.datadog.android.trace.model.SpanEvent
2425
import com.datadog.android.trace.utils.RumContextTestsUtils.RUM_CONTEXT_ACTION_ID
@@ -319,6 +320,24 @@ internal class CoreTraceWriterTest {
319320
}
320321
}
321322

323+
@Test
324+
fun `M not write spans with force drop tag W write()`(forge: Forge) {
325+
// GIVEN
326+
val ddSpans = createNonEmptyDdSpans(
327+
forge = forge,
328+
includeDropSamplingPriority = false
329+
)
330+
ddSpans.forEach { whenever(it.getTag(FORCE_DROP_SPAN)) doReturn true }
331+
332+
// WHEN
333+
testedWriter.write(ddSpans)
334+
335+
// THEN
336+
verifyNoInteractions(mockEventBatchWriter)
337+
338+
ddSpans.forEach { it.finish() }
339+
}
340+
322341
@Test
323342
fun `M not write non-mapped spans W write()`(forge: Forge) {
324343
// GIVEN

0 commit comments

Comments
 (0)