Skip to content

Commit ac84f49

Browse files
committed
RUM-15138: Fix _dd.agent_psr and _dd.rule_psr to report rebased rate in legacy interceptors
Make DeterministicTraceSampler.getSampleRate(DatadogSpan) public so the legacy TracingInterceptor and DatadogInterceptor paths can call the span-aware overload at the PSR reporting sites. The type-check pattern mirrors the existing effectiveSampleRate approach in ApmNetworkInstrumentationExt. The session_sample_rate tag is already written onto the span by extractRumContext before the PSR metric is set, so the tag-based fallback fires correctly for samplers created via the legacy public constructors. getSampleRate(DatadogSpan) will be removed when the legacy interceptor path is retired in v4.
1 parent 3adbf40 commit ac84f49

9 files changed

Lines changed: 169 additions & 3 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ open class com.datadog.android.trace.DeterministicTraceSampler : com.datadog.and
2424
constructor(Double)
2525
override fun getSampleRate(): Float
2626
override fun sample(com.datadog.android.trace.api.span.DatadogSpan): Boolean
27+
DEPRECATED fun getSampleRate(com.datadog.android.trace.api.span.DatadogSpan): Float
2728
annotation com.datadog.android.trace.ExperimentalTraceApi
2829
object com.datadog.android.trace.GlobalDatadogTracer
2930
fun registerIfAbsent(com.datadog.android.trace.api.tracer.DatadogTracer): Boolean

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class com/datadog/android/trace/DeterministicTraceSampler : com/datadog/a
3333
public fun <init> (F)V
3434
public fun <init> (Lkotlin/jvm/functions/Function0;)V
3535
public fun getSampleRate ()Ljava/lang/Float;
36+
public final fun getSampleRate (Lcom/datadog/android/trace/api/span/DatadogSpan;)F
3637
public fun sample (Lcom/datadog/android/trace/api/span/DatadogSpan;)Z
3738
public synthetic fun sample (Ljava/lang/Object;)Z
3839
}

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.datadog.android.trace.internal.net.SpanSamplingIdProvider
3131
* @param sessionSampleRateProvider Provider for the RUM session sample rate used to rebase the
3232
* trace sample rate. Defaults to 100 (no rebasing).
3333
*/
34+
// TODO RUM-13454 -> Make this class internal in V4 (RUM-15590)
3435
open class DeterministicTraceSampler private constructor(
3536
private val sampleRateProvider: () -> Float,
3637
private val sessionSampleRateProvider: () -> Float
@@ -98,10 +99,27 @@ open class DeterministicTraceSampler private constructor(
9899
}
99100

100101
/** @inheritDoc */
102+
// TODO RUM-13454 -> Remove the @Suppress when getSampleRate(DatadogSpan) is removed in V4 (RUM-15590)
103+
@Suppress("DEPRECATION")
101104
override fun sample(item: DatadogSpan): Boolean =
102105
computeSamplingDecision(getSampleRate(item), SpanSamplingIdProvider.provideId(item))
103106

104-
internal fun getSampleRate(item: DatadogSpan): Float {
107+
/**
108+
* Returns the effective sample rate for [item], applying cross-product rebasing when a RUM
109+
* session rate is available.
110+
*
111+
* When this sampler was created via [ApmNetworkInstrumentationConfiguration] the session rate
112+
* is read from the injected [sessionSampleRateProvider]. For samplers created via the public
113+
* constructors (no provider) the rate is read from the `session_sample_rate` span tag written
114+
* by [com.datadog.android.trace.internal.RumContextPropagator], providing compatibility with
115+
* [com.datadog.android.okhttp.trace.TracingInterceptor] and related legacy integrations.
116+
*/
117+
// TODO RUM-13454 -> Remove this in V4 when deprecating legacy paths (RUM-15590)
118+
@Deprecated(
119+
"Use ApmNetworkInstrumentationConfiguration for session-aware sampling. Will be removed in v4.",
120+
level = DeprecationLevel.WARNING
121+
)
122+
fun getSampleRate(item: DatadogSpan): Float {
105123
// Compatibility path for integrations creating this sampler via public constructors
106124
// (no sessionSampleRateProvider). In that case the session sample rate is propagated on
107125
// the span context as `session_sample_rate` and must be used for rebasing.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ internal fun DatadogSpan.applyPriority(isSampled: Boolean, traceSampler: Sampler
4141
}
4242
}
4343

44+
// TODO RUM-13454 -> Remove the cast to DeterministicTraceSampler - V4 (RUM-15590)
45+
@Suppress("DEPRECATION")
4446
internal fun Sampler<DatadogSpan>.effectiveSampleRate(span: DatadogSpan): Float? {
4547
return when (this) {
4648
is DeterministicTraceSampler -> getSampleRate(span)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,8 @@ internal class DeterministicTraceSamplerTest {
322322
}
323323

324324
@Test
325+
// TODO RUM-13454 -> Remove this test when getSampleRate(DatadogSpan) is removed in V4 (RUM-15590)
326+
@Suppress("DEPRECATION")
325327
fun `M use context session sample rate W getSampleRate(item) {no sessionSampleRateProvider configured}`(
326328
@FloatForgery(min = 0f, max = 100f) traceSampleRate: Float,
327329
@FloatForgery(min = 0f, max = 99f) sessionSampleRate: Float,
@@ -352,6 +354,8 @@ internal class DeterministicTraceSamplerTest {
352354
}
353355

354356
@Test
357+
// TODO RUM-13454 -> Remove this test when getSampleRate(DatadogSpan) is removed in V4 (RUM-15590)
358+
@Suppress("DEPRECATION")
355359
fun `M use raw trace rate W getSampleRate(item) {session sample rate unavailable}`(
356360
@FloatForgery(min = 0f, max = 100f) traceSampleRate: Float,
357361
@LongForgery fakeTraceIdLong: Long

integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/DatadogInterceptor.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.datadog.android.rum._RumInternalProxy
3131
import com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor
3232
import com.datadog.android.rum.resource.ResourceHeadersExtractor
3333
import com.datadog.android.rum.tracking.ViewTrackingStrategy
34+
import com.datadog.android.trace.DeterministicTraceSampler
3435
import com.datadog.android.trace.TraceContextInjection
3536
import com.datadog.android.trace.TracingHeaderType
3637
import com.datadog.android.trace.api.span.DatadogSpan
@@ -242,7 +243,16 @@ open class DatadogInterceptor internal constructor(
242243
buildMap {
243244
put(RumAttributes.TRACE_ID, span.context().traceId.toHexString())
244245
put(RumAttributes.SPAN_ID, span.context().spanId.toString())
245-
put(RumAttributes.RULE_PSR, (traceSampler.getSampleRate() ?: ZERO_SAMPLE_RATE) / ALL_IN_SAMPLE_RATE)
246+
// TODO RUM-13454 -> Remove the cast to DeterministicTraceSampler - V4 (RUM-15590)
247+
@Suppress("DEPRECATION")
248+
put(
249+
RumAttributes.RULE_PSR,
250+
(
251+
(traceSampler as? DeterministicTraceSampler)?.getSampleRate(span)
252+
?: traceSampler.getSampleRate()
253+
?: ZERO_SAMPLE_RATE
254+
) / ALL_IN_SAMPLE_RATE
255+
)
246256
putAll(graphqlAttributes)
247257
putAll(graphqlErrorAttributes)
248258
}

integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracingInterceptor.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,15 @@ internal constructor(
258258

259259
val spanContext = span.context()
260260
if (spanContext.setSamplingPriority(samplingPriority)) {
261+
// TODO RUM-13454 -> Remove the cast to DeterministicTraceSampler - V4 (RUM-15590)
262+
@Suppress("DEPRECATION")
261263
spanContext.setMetric(
262264
AGENT_PSR_ATTRIBUTE,
263-
(traceSampler.getSampleRate() ?: ZERO_SAMPLE_RATE) / ALL_IN_SAMPLE_RATE
265+
(
266+
(traceSampler as? DeterministicTraceSampler)?.getSampleRate(span)
267+
?: traceSampler.getSampleRate()
268+
?: ZERO_SAMPLE_RATE
269+
) / ALL_IN_SAMPLE_RATE
264270
)
265271
}
266272
}

integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,5 +1381,55 @@ internal class DatadogInterceptorTest : TracingInterceptorNotSendingSpanTest() {
13811381
assertThat(resHeaders).isEqualTo(fakeActualResponseHeaders)
13821382
}
13831383

1384+
// region rule_psr rebasing
1385+
1386+
@Test
1387+
fun `M set rebased rule_psr W intercept() {DeterministicTraceSampler, span has session_sample_rate tag}`(
1388+
@FloatForgery(min = 1f, max = 100f) fakeTraceSampleRate: Float,
1389+
@FloatForgery(min = 1f, max = 99f) fakeSessionSampleRate: Float,
1390+
@IntForgery(min = 200, max = 300) statusCode: Int
1391+
) {
1392+
// Given — force upstream sampling-priority KEEP so isSampled=true regardless of rate
1393+
fakeRequest = forgeRequest {
1394+
it.addHeader(
1395+
TracingInterceptor.DATADOG_SAMPLING_PRIORITY_HEADER,
1396+
com.datadog.android.trace.api.DatadogTracingConstants.PrioritySampling.SAMPLER_KEEP.toString()
1397+
)
1398+
}
1399+
val deterministicSampler = DeterministicTraceSampler(fakeTraceSampleRate)
1400+
whenever(mockSpanContext.tags) doReturn mapOf("session_sample_rate" to fakeSessionSampleRate)
1401+
stubChain(mockChain, statusCode)
1402+
1403+
whenever(rumMonitor.mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mock()
1404+
whenever(rumMonitor.mockSdkCore.firstPartyHostResolver) doReturn mockResolver
1405+
val datadogInterceptor = DatadogInterceptor(
1406+
sdkInstanceName = null,
1407+
tracedHosts = fakeLocalHosts,
1408+
tracedRequestListener = mockRequestListener,
1409+
rumResourceAttributesProvider = mockRumAttributesProvider,
1410+
traceSampler = deterministicSampler,
1411+
traceContextInjection = TraceContextInjection.ALL,
1412+
redacted404ResourceName = fakeRedacted404Resources,
1413+
localTracerFactory = { _, _ -> mockLocalTracer },
1414+
globalTracerProvider = { mockTracer }
1415+
)
1416+
1417+
// When
1418+
datadogInterceptor.intercept(mockChain)
1419+
1420+
// Then
1421+
val expectedRulePsr = fakeTraceSampleRate * fakeSessionSampleRate / 100f / 100f
1422+
val attrsCaptor = argumentCaptor<Map<String, Any?>>()
1423+
verify(rumMonitor.mockInstance).stopResource(
1424+
any<ResourceId>(),
1425+
any(),
1426+
anyOrNull(),
1427+
any(),
1428+
attrsCaptor.capture()
1429+
)
1430+
assertThat(attrsCaptor.firstValue[RumAttributes.RULE_PSR])
1431+
.isEqualTo(expectedRulePsr)
1432+
}
1433+
13841434
// endregion
13851435
}

integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,80 @@ internal open class TracingInterceptorTest {
16521652
)
16531653
}
16541654

1655+
// region agent_psr rebasing
1656+
1657+
@Test
1658+
fun `M set rebased agent_psr W intercept() {DeterministicTraceSampler, span has session_sample_rate tag}`(
1659+
@FloatForgery(min = 1f, max = 100f) fakeTraceSampleRate: Float,
1660+
@FloatForgery(min = 1f, max = 99f) fakeSessionSampleRate: Float
1661+
) {
1662+
// Given
1663+
val deterministicSampler = DeterministicTraceSampler(fakeTraceSampleRate)
1664+
whenever(mockSpanContext.tags) doReturn mapOf("session_sample_rate" to fakeSessionSampleRate)
1665+
whenever(mockSpan.isRootSpan) doReturn true
1666+
whenever(mockSpanContext.setSamplingPriority(any())) doReturn true
1667+
whenever(mockResolver.isFirstPartyUrl(fakeUrl.toHttpUrl())).thenReturn(true)
1668+
stubChain(mockChain)
1669+
1670+
testedInterceptor = TracingInterceptor(
1671+
sdkInstanceName = null,
1672+
tracedHosts = fakeLocalHosts,
1673+
tracedRequestListener = mockRequestListener,
1674+
traceOrigin = fakeOrigin,
1675+
traceSampler = deterministicSampler,
1676+
localTracerFactory = { _, _ -> mockLocalTracer },
1677+
redacted404ResourceName = fakeRedacted404Resources,
1678+
traceContextInjection = TraceContextInjection.ALL,
1679+
globalTracerProvider = { mockTracer }
1680+
)
1681+
1682+
// When
1683+
testedInterceptor.intercept(mockChain)
1684+
1685+
// Then
1686+
val expectedRebasedRate = fakeTraceSampleRate * fakeSessionSampleRate / 100f
1687+
verify(mockSpanContext).setMetric(
1688+
eq("_dd.agent_psr"),
1689+
eq(expectedRebasedRate.toDouble() / 100.0)
1690+
)
1691+
}
1692+
1693+
@Test
1694+
fun `M set raw agent_psr W intercept() {DeterministicTraceSampler, span has no session_sample_rate tag}`(
1695+
@FloatForgery(min = 1f, max = 100f) fakeTraceSampleRate: Float
1696+
) {
1697+
// Given
1698+
val deterministicSampler = DeterministicTraceSampler(fakeTraceSampleRate)
1699+
whenever(mockSpanContext.tags) doReturn emptyMap()
1700+
whenever(mockSpan.isRootSpan) doReturn true
1701+
whenever(mockSpanContext.setSamplingPriority(any())) doReturn true
1702+
whenever(mockResolver.isFirstPartyUrl(fakeUrl.toHttpUrl())).thenReturn(true)
1703+
stubChain(mockChain)
1704+
1705+
testedInterceptor = TracingInterceptor(
1706+
sdkInstanceName = null,
1707+
tracedHosts = fakeLocalHosts,
1708+
tracedRequestListener = mockRequestListener,
1709+
traceOrigin = fakeOrigin,
1710+
traceSampler = deterministicSampler,
1711+
localTracerFactory = { _, _ -> mockLocalTracer },
1712+
redacted404ResourceName = fakeRedacted404Resources,
1713+
traceContextInjection = TraceContextInjection.ALL,
1714+
globalTracerProvider = { mockTracer }
1715+
)
1716+
1717+
// When
1718+
testedInterceptor.intercept(mockChain)
1719+
1720+
// Then
1721+
verify(mockSpanContext).setMetric(
1722+
eq("_dd.agent_psr"),
1723+
eq(fakeTraceSampleRate.toDouble() / 100.0)
1724+
)
1725+
}
1726+
1727+
// endregion
1728+
16551729
// region Internal
16561730

16571731
internal fun stubChain(chain: Interceptor.Chain, statusCode: Int = forge.anInt(min = 200, max = 600)) {

0 commit comments

Comments
 (0)