RUM-13023: Propagate trace and session replay sample rates to RUM ViewEvents#3247
Conversation
This comment has been minimized.
This comment has been minimized.
d5d38dd to
a7e166b
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 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
🚀 New features to boost your workflow:
|
|
@codex review |
There was a problem hiding this comment.
💡 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".
| val tracingContext = datadogContext.featuresContext[Feature.TRACING_FEATURE_NAME] | ||
| return tracingContext?.get(TRACE_SAMPLE_RATE) as? Float |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
203d11f to
3451d16
Compare
|
|
||
| val tracesConfig = TraceConfiguration.Builder().build() | ||
| Trace.enable(tracesConfig) | ||
| Trace.enable(tracesConfig, instance) |
There was a problem hiding this comment.
💡 This was a previous bug as it should use the Datadog instance created in the LocalServer
There was a problem hiding this comment.
💡 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) |
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]>
What does this PR do?
Adds
traceSampleRateandsessionReplaySampleRatefields toViewEvent.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
ViewEventpayloads, 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 sharedfeaturesContextmap and propagates them into the event schema.Additional Notes
sessionReplaySampleRateis read asLong(matchingSessionReplayFeaturestorage format).traceSampleRateis read asFloat(matchingTracingInterceptorstorage format).Feature.TRACING_FEATURE_NAMEwas added to thewithFeatureContextsset inDatadogRumMonitorso the tracing context is available when writing view events._common-schema.json) was updated with the newtrace_sample_ratefield; the generated model and API surface files were regenerated accordingly.How to test
To manually verify that
session_replay_sample_rateandtrace_sample_rateare present in RUM ViewEvent payloads, enable request body logging in the sample app and inspect logcat.1. Enable body logging in
CoreFeature.ktIn
dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt, change:2. Build and install the sample app
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:
Look for
_dd.configurationin the output — both fields should be present:Review checklist (to be filled by reviewers)