Skip to content

RUM-13023: Propagate trace and session replay sample rates to RUM ViewEvents#3247

Merged
hamorillo merged 5 commits into
developfrom
hector.morilloprieto/RUM-13023
Mar 13, 2026
Merged

RUM-13023: Propagate trace and session replay sample rates to RUM ViewEvents#3247
hamorillo merged 5 commits into
developfrom
hector.morilloprieto/RUM-13023

Conversation

@hamorillo

@hamorillo hamorillo commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds traceSampleRate and sessionReplaySampleRate fields to ViewEvent.Configuration, populating them from the tracing and session replay feature contexts when a RUM view event is written.

Motivation

RUM-13023: These sample rates were not previously included in ViewEvent payloads, making it impossible for downstream consumers to know the effective sampling configuration at the time of the event. This change reads the rates from the shared featuresContext map and propagates them into the event schema.

Additional Notes

  • sessionReplaySampleRate is read as Long (matching SessionReplayFeature storage format).
  • traceSampleRate is read as Float (matching TracingInterceptor storage format).
  • Feature.TRACING_FEATURE_NAME was added to the withFeatureContexts set in DatadogRumMonitor so the tracing context is available when writing view events.
  • The JSON schema (_common-schema.json) was updated with the new trace_sample_rate field; the generated model and API surface files were regenerated accordingly.

How to test

To manually verify that session_replay_sample_rate and trace_sample_rate are present in RUM ViewEvent payloads, enable request body logging in the sample app and inspect logcat.

1. Enable body logging in CoreFeature.kt

In dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt, change:

// Before
builder.addNetworkInterceptor(CurlInterceptor())

// After
builder.addNetworkInterceptor(CurlInterceptor(printBody = true))

This is gated by BuildConfig.DEBUG — safe for debug builds only. Do not commit this change.

2. Build and install the sample app

./gradlew :sample:kotlin:installUs1Debug
adb shell am start -n com.datadog.android.sample/.NavActivity

3. Navigate between screens and check logcat

Navigate to any screen and back to trigger a view stop event, then wait ~15s for the upload cycle:

adb logcat -s "Curl" | grep '"type":"view"'

Look for _dd.configuration in the output — both fields should be present:

"configuration": {
  "session_sample_rate": 100.0,
  "session_replay_sample_rate": 100,
  "trace_sample_rate": 100.0
}

⚠️📦 A skill for this workflow is available in .claude/skills/android-sdk-event-inspection/ — it covers the full setup including credentials and logcat filtering.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc). Be concise.

@datadog-datadog-prod-us1

This comment has been minimized.

@hamorillo
hamorillo force-pushed the hector.morilloprieto/RUM-13023 branch from d5d38dd to a7e166b Compare March 11, 2026 13:29
@codecov-commenter

codecov-commenter commented Mar 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.32%. Comparing base (f2674e0) to head (caccc81).
⚠️ Report is 52 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3247      +/-   ##
===========================================
- Coverage    71.35%   71.32%   -0.04%     
===========================================
  Files          940      940              
  Lines        34764    34773       +9     
  Branches      5893     5893              
===========================================
- Hits         24805    24799       -6     
+ Misses        8312     8308       -4     
- Partials      1647     1666      +19     
Files with missing lines Coverage Δ
.../android/rum/internal/domain/scope/RumViewScope.kt 94.14% <100.00%> (-0.27%) ⬇️
.../android/rum/internal/monitor/DatadogRumMonitor.kt 88.50% <100.00%> (+0.03%) ⬆️
...datadog/android/okhttp/trace/TracingInterceptor.kt 81.82% <100.00%> (+0.05%) ⬆️

... and 36 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hamorillo

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56cac519de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1622 to +1623
val tracingContext = datadogContext.featuresContext[Feature.TRACING_FEATURE_NAME]
return tracingContext?.get(TRACE_SAMPLE_RATE) as? Float

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle late tracing context when reading trace sample rate

resolveTraceSampleRate() assumes Feature.TRACING_FEATURE_NAME already contains okhttp_interceptor_sample_rate, but that key is currently written by TracingInterceptor during interceptor construction; if an app builds the interceptor before Datadog.initialize() (the sample app used to do this before the lazy-init change), the context entry is never populated and view events will silently miss _dd.configuration.trace_sample_rate even though tracing is enabled. Please add a late-initialization path (for example, updating the context from onSdkInstanceReady) so this new field is reliably emitted.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 203d11f5 addresses this by moving the context update into onSdkInstanceReady, so it fires regardless of whether the interceptor is constructed before or after Datadog.initialize().

There is still a narrow edge case: if the interceptor is created before Datadog.initialize(), the first RUM view event emitted before the first call to intercept() may still miss trace_sample_rate, because onSdkInstanceReady is triggered lazily on the first interception. Once intercept() is called for the first time the context is populated and subsequent view events will carry the field correctly.

I've been thinking about how to fully close this gap, but haven't found a solution that doesn't require modifying the public API. I think the current state is an acceptable trade-off for now — but happy to hear other opinions from anyone with more context on this!

@aleksandr-gringauz aleksandr-gringauz Mar 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an option. We could introduce some global static HashMap<sdkInstanceName, OkHttpSampleRate> where we put the value when TracingInterceptor is created. The map itself will have to live in dd-sdk-android-rum module somewhere so that we could get the value from it there.

There is actually another kind of related problem. Theoretically there could exist multiple instances of TracingInterceptor with different sampling rates (our SDK allows it I belive). Although I expect this to be rare or even non-existing in practice. I'm not asking to fix it, just mentioning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could introduce some global static HashMap<sdkInstanceName, OkHttpSampleRate> where we put the value when TracingInterceptor is created. The map itself will have to live in dd-sdk-android-rum module somewhere so that we could get the value from it there.

That could be an option. I’m not a big fan of introducing a global static variable, though. (But I didn’t come up with anything better, so thanks!) Adding the map to dd-sdk-android-rum would “pollute” the public API a bit, but it looks doable.

What I’m thinking about is whether it’s worth it, as IIUC the backend will keep the last received value. So sessions will save the last received value, which in general should contain a trace_sample_rate value.

Anyway, you have deeper knowledge here, so if you think we need to cover this case, I’ll prepare the changes.

Theoretically there could exist multiple instances of TracingInterceptor with different sampling rates (our SDK allows it I belive). Although I expect this to be rare or even non-existing in practice. I'm not asking to fix it, just mentioning.

That’s a good point. However, I would say that concern goes further than the scope of this PR, as I’m not sure whether our code has been developed with that consideration in mind.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the map to dd-sdk-android-rum would “pollute” the public API a bit, but it looks doable.

You can add InternalApi annotation to it. Yes, it will end up in the public API but the linter will not allow the customers to use it.

Anyway, you have deeper knowledge here, so if you think we need to cover this case, I’ll prepare the changes.

I don't think I actually have deeper knowledge here :) This is the first time I look at this code. But the solution seems doable.

I'm not a fan of statics either. Maybe we need to have some sort of PreInitSDKCore - a place that holds things before SDKCore is created. Not a call to action though.

From what I see current architecture doesn't support well what you want.

I would say - if you can come up with a solution - great. If not - I am ok with approving the PR the way it is now. We already had this problem for OKHTTP_INTERCEPTOR_SAMPLE_RATE, you didn't introduce any regressions here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep things simple and avoid introducing a singleton.

the first RUM view event emitted before the first call to intercept() may still miss trace_sample_rate, because onSdkInstanceReady is triggered lazily on the first interception.

RUM view events are reduced, so even if first view update miss it, if the following view updates for the given view populate this field, it will be there overall once view if finalized.

We can check with backend if this property is aggregated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep things simple and avoid introducing a singleton.

OK from me.

…wEvents

Add traceSampleRate and sessionReplaySampleRate fields to ViewEvent.Configuration,
reading them from the tracing and session replay feature contexts respectively.

- sessionReplaySampleRate is read as Long (matching SessionReplayFeature storage)
- traceSampleRate is read as Float (matching TracingInterceptor storage)
- Feature.TRACING_FEATURE_NAME added to withFeatureContexts in DatadogRumMonitor
- Unit tests cover session replay/trace rates on StopView, null key values, and missing context
- Integration tests verify the full flow from featuresContext to ViewEvent JSON
TracingInterceptor writes okhttp_interceptor_sample_rate into the
tracing feature context during its init block, which DatadogRumMonitor
later reads to populate trace_sample_rate on ViewEvents.

Two issues prevented this:

1. okHttpClient (and TracingInterceptor) were eagerly initialized as
   class fields in SampleApplication, before onCreate() ran and before
   Datadog.initialize() was called. SdkReference.get() returned null,
   so updateFeatureContext() was never invoked. Making okHttpClient lazy
   defers TracingInterceptor construction until initializeImageLoaders(),
   which runs after initializeDatadog().

2. LocalServer.init() called Trace.enable(tracesConfig) without passing
   its own SDK instance, targeting the default instance instead. Since
   registerFeature() replaces any existing SdkFeature entry with a fresh
   one (empty context), this wiped the okhttp_interceptor_sample_rate
   written in step 1. Passing the correct instance to Trace.enable()
   prevents the default instance's tracing context from being replaced.
Previously, the tracing feature context (sample rate and header types) was
populated directly in the TracingInterceptor `init` block.

This was fragile: if the SDK was not yet fully initialized at construction
time, the context update could silently fail or use a stale SDK reference.

The update is now moved into a new `updateTracingFeatureContext` helper,
called from `onSdkInstanceReady`. This guarantees the context is populated
only after the SDK instance is confirmed ready, whether the SDK was already
initialized at construction time or initialized later.

A unit test is added to verify the feature context is NOT updated at
construction time but IS updated when `onSdkInstanceReady` is invoked.
@hamorillo
hamorillo force-pushed the hector.morilloprieto/RUM-13023 branch from 203d11f to 3451d16 Compare March 12, 2026 12:03

val tracesConfig = TraceConfiguration.Builder().build()
Trace.enable(tracesConfig)
Trace.enable(tracesConfig, instance)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 This was a previous bug as it should use the Datadog instance created in the LocalServer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch!

@hamorillo
hamorillo marked this pull request as ready for review March 12, 2026 14:02
@hamorillo
hamorillo requested review from a team as code owners March 12, 2026 14:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3451d16112

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


val tracesConfig = TraceConfiguration.Builder().build()
Trace.enable(tracesConfig)
Trace.enable(tracesConfig, instance)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch!

Comment thread sample/kotlin/src/main/kotlin/com/datadog/android/sample/SampleApplication.kt Outdated
Comment thread features/dd-sdk-android-rum/src/main/json/rum/_common-schema.json
hamorillo and others added 2 commits March 12, 2026 15:53
Reverts `by lazy` on okHttpClient, retrofitClient, and retrofitBaseDataSource
in SampleApplication to preserve the original eager initialization, which
intentionally simulates the real-world scenario where the network stack is
created before the Datadog SDK is initialized.
Replace assertThat(actual).isNotNull() + non-null assertion operator (!!)
with checkNotNull(actual) for a cleaner null check and smart cast.

Co-authored-by: Nikita Ogorodnikov <[email protected]>
@hamorillo
hamorillo merged commit 6772dc6 into develop Mar 13, 2026
26 checks passed
@hamorillo
hamorillo deleted the hector.morilloprieto/RUM-13023 branch March 13, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants