[RUM-15238] [FIX] RUM events associated to previous view#1220
[RUM-15238] [FIX] RUM events associated to previous view#1220marco-saia-datadog wants to merge 5 commits intodevelopfrom
Conversation
e08ad8f to
37adee8
Compare
There was a problem hiding this comment.
Pull request overview
Adds a navigation-transition buffering mechanism so RUM events emitted during in-flight navigation are attributed to the new view rather than the previous one.
Changes:
- Introduces
NavigationBufferin core and wires it intoBufferSingletonafter SDK initialization. - Updates React Navigation RUM tracking to start buffering on
__unsafe_action__, thenprepareEndNavigation()beforeDdRum.startViewandflush()after it settles; addsuseNavigationBufferoption. - Adds/updates tests covering buffer lifecycle, teardown, and back-to-back navigation behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react-navigation/src/rum/instrumentation/DdRumReactNavigationTracking.tsx | Wires navigation lifecycle to the buffer and backdates startView using navigationStartTime; adds useNavigationBuffer. |
| packages/react-navigation/src/tests/rum/instrumentation/DdRumReactNavigationTracking.test.tsx | Adds integration tests for the buffer wiring and updates RUM mocks to return promises. |
| packages/core/src/sdk/DatadogProvider/Buffer/NavigationBuffer.ts | Implements the buffering decorator with startNavigation / prepareEndNavigation / flush / endNavigation and a safety timeout. |
| packages/core/src/sdk/DatadogProvider/Buffer/BufferSingleton.ts | Installs NavigationBuffer(PassThroughBuffer) as the active buffer on initialization and exposes getNavigationBuffer(). |
| packages/core/src/sdk/DatadogProvider/Buffer/tests/NavigationBuffer.test.ts | Adds unit tests for buffering, draining, timeouts, and the two-phase flush pattern. |
| packages/core/src/sdk/DatadogProvider/Buffer/tests/BufferSingleton.test.ts | Verifies NavigationBuffer wiring through BufferSingleton. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2552a9b to
eb49967
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } else { | ||
| DdRum.startView(customKey, screenName); | ||
| } | ||
| const context = params ? { params } : undefined; |
There was a problem hiding this comment.
Since the default context is always {}, perhaps we could set it as such here and remove the nested ternary below and just pass it forward?
const context = params ? { params } : {};
const startViewPromise =
navigationStartTime !== undefined
? DdRum.startView(
customKey,
screenName,
context,
navigationStartTime
)
: DdRum.startView(customKey, screenName, context);
Not sure if I'm missing an edge case here but it certainly looks easier to read 🤔
| } | ||
| } else { | ||
| // App is in background — no startView, drain buffer immediately | ||
| this.getNavBuffer()?.endNavigation(); |
There was a problem hiding this comment.
Is this actually reachable in that scenario?, handleAppStateChange does not call handleRouteNavigation when the app is in background (it calls stopView) 🤔
sbarrio
left a comment
There was a problem hiding this comment.
Left some minor nits and questions. Nice job!
cdn34dd
left a comment
There was a problem hiding this comment.
I'm guessing while we're fixing this for react-navigation, the issue will still exist for react-native-navigation correct ?
| activeView: route.name, | ||
| trackingState: this.trackingState | ||
| } | ||
| ); |
There was a problem hiding this comment.
Why does this condition not drain the buffer like the other ones ?
I think we're missing this:
this.getNavBuffer()?.endNavigation();
| if (event.data?.noop) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
When does this happen ? It would be nice to add a comment explaining it.
| this.getNavBuffer()?.startNavigation(); | ||
| }; | ||
| navigationRef.addListener( | ||
| '__unsafe_action__', |
There was a problem hiding this comment.
This is a private event are we sure this exists/behaves the same in all the versions we support 6/7/8 ? I did a quick search for some documentation on it and there is none, which is probably normal since it's a private field, but still, we should ensure it exists and behaves the same in all the versions.
What does this PR do?
Introduces a
NavigationBufferthat queues RUM events during in-flight navigation and flushes them once the new view is confirmed.Motivation
RUM events (resources, actions) fired inside a screen's
useEffectduring a navigation transition were being attributed to the previous view instead of the new one. This happened because theonStateChangecallback — which triggersDdRum.startView— fires after the new screen mounts and its effects run, leaving a window where events have no active view to attach to.How it works
__unsafe_action__listener (startNavigation) — fires synchronously when any navigation action is dispatched, before the new screen mounts. Starts buffering all incoming RUM events and recordsnavigationStartTime.prepareEndNavigation— called just beforeDdRum.startView. Stops accepting new events (sostartViewitself passes through immediately) but keeps the queue intact.flush— called afterstartViewresolves. Drains queued events to the now-active view.endNavigation(stop + drain) — used as a fail-safe on timeout (500 ms), teardown, background state, and any path wherestartViewis skipped.DdRum.startViewis also backdated tonavigationStartTimeso the view's start timestamp reflects when the user triggered navigation, not whenonStateChangefired.New option:
useNavigationBufferAdded to
NavigationTrackingOptions(defaulttrue). Set tofalseto bypass the buffer entirely if it causes issues.Back-to-back navigation race condition (in 5c925f3)
A second
__unsafe_action__can fire beforeflush()resolves from the first navigation (e.g. rapid taps). Without a fix, nav-2 events would land in the queue being flushed and be attributed to view 1.prepareEndNavigation()now snapshotscallbackQueueinto_pendingFlushQueueand resets the live queue to[].flush()drains only the snapshot, so any events enqueued by a concurrent nav-2 are isolated and will be flushed in their own cycle.endNavigation()anddrain()calldrainAllQueues()which drains both, so nothing leaks on teardown/timeoutKey files
core/.../Buffer/NavigationBuffer.tsDatadogBufferdecorator implementing the buffer lifecyclecore/.../Buffer/BufferSingleton.tsonInitializationnow installsNavigationBufferwrappingPassThroughBufferas the active SDK bufferreact-navigation/.../DdRumReactNavigationTracking.tsx__unsafe_action__listener; callsprepareEndNavigation/flusharoundstartView; addsuseNavigationBufferoptioncore/src/index.tsxBufferSingletonremoved from public exports — react-navigation accesses it viagetGlobalInstanceusing the shared globalThis keyAdditional Notes
NavigationBufferis accessed fromreact-navigationviagetGlobalInstance('com.datadog.reactnative.buffer_singleton', ...)— no cross-package import. The key is the same oneBufferSingletonregisters under. A comment in both files marks this coupling.NAVIGATION_BUFFER_TIMEOUT_MS) guarantees the buffer is always drained even ifonStateChangenever fires (e.g. navigation cancelled).__unsafe_action__wiring.LOGcalls are temporary and markedTODO: remove before shipping- I will remove them after some further testing on my side, and before merging the PRReview checklist (to be filled by reviewers)