Skip to content

🐛 Fix #3935: Include pre-start context in first view event#4304

Open
allspain wants to merge 1 commit intomainfrom
rick.klein/3935/missing-global-context-init
Open

🐛 Fix #3935: Include pre-start context in first view event#4304
allspain wants to merge 1 commit intomainfrom
rick.klein/3935/missing-global-context-init

Conversation

@allspain
Copy link
Collaborator

@allspain allspain commented Mar 10, 2026

RFC: Include Pre-Start Context in First RUM View Event

Status: Complete

Branch: rick.klein/3935/missing-global-context-init

Last updated: 2026-03-10

PR: PR #4304

Summary

The first RUM view event emitted after DD_RUM.init() is missing global context, user context, and account context that were set before initialization. This RFC describes a fix that passes pre-start context values through the initialization chain so they are applied to the real context managers before the first view event fires.

Motivation

Users of the Browser SDK commonly set context before calling init():

DD_RUM.setGlobalContext({ team: 'checkout' })
DD_RUM.setUser({ id: 'user-123' })
DD_RUM.init({ applicationId: '...', clientToken: '...' })

The first view event emitted during init() is missing this context. All subsequent events include it correctly. This is issue #3935.

Root cause: During init(), startRumEventCollection() creates new, empty context managers, then starts assembly (which registers context hooks), then starts view collection. The first view event fires synchronously during startViewCollection(). The pre-start buffered API calls (including setContext()) are drained after startRum() returns — too late for the first view event.

The initialization sequence:

  1. tryStartRum() calls doStartRum() (which calls startRum()startRumEventCollection())
  2. Inside startRumEventCollection(): new empty context managers are created → assembly hooks registered → startViewCollection() fires the first view event synchronously
  3. doStartRum() returns
  4. bufferApiCalls.drain() replays buffered setContext() calls — but the first view event already shipped

Solution

Approach

Snapshot the pre-start context values in tryStartRum() and pass them through the initialization chain (doStartRumstartRumstartRumEventCollection). The real context managers are initialized with these values before startViewCollection() is called, ensuring the first view event includes them.

This is a synchronous, forward-pass approach — no timing changes, no microtask delays, no changes to the buffer drain order.

Architecture

The fix threads an InitialContexts object through three functions:

  1. preStartRum.ts (tryStartRum): Snapshots pre-start context values into an InitialContexts object and passes it to doStartRum()
  2. rumPublicApi.ts: Forwards initialContexts from doStartRum callback to startRum()
  3. startRum.ts (startRumEventCollection): After creating context managers but before starting view collection, calls setContext() on each manager with the initial values

Schema

The InitialContexts interface added in packages/rum-core/src/boot/preStartRum.ts:

export interface InitialContexts {
  globalContext: Context
  userContext: Context
  accountContext: Context
}

The DoStartRum type signature updated to include initialContexts:

export type DoStartRum = (
  configuration: RumConfiguration,
  deflateWorker: DeflateWorker | undefined,
  initialViewOptions: ViewOptions | undefined,
  telemetry: Telemetry,
  hooks: Hooks,
  initialContexts: InitialContexts
) => StartRumResult

Testing

Run yarn test:unit --spec packages/rum-core/src/boot/preStartRum.spec.ts and yarn test:unit --spec packages/rum-core/src/boot/startRum.spec.ts to validate the fix.

Unit tests (4 new tests in preStartRum.spec.ts): Verify that doStartRum receives the correct initial context values for global context, user context, account context, and the empty-context case.

Integration tests (2 new tests in startRum.spec.ts): Verify the first collected view event includes global context (context field) and user context (usr field) when initialContexts is passed to startRumEventCollection.

References

  • Issue #3935 — Initial version of RUM view event is missing global context
  • PR #3597 — Alternative approach using enqueueMicroTask() to delay the first view update

@allspain allspain requested a review from a team as a code owner March 10, 2026 12:02
@github-actions
Copy link


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@datadog-datadog-prod-us1-2
Copy link

datadog-datadog-prod-us1-2 bot commented Mar 10, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 71.43%
Overall Coverage: 76.75% (-0.47%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 338234a | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

@cit-pr-commenter-54b7da
Copy link

cit-pr-commenter-54b7da bot commented Mar 10, 2026

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 174.43 KiB 174.66 KiB +232 B +0.13%
Rum Profiler 6.16 KiB 6.16 KiB 0 B 0.00%
Rum Recorder 25.24 KiB 25.24 KiB 0 B 0.00%
Logs 56.84 KiB 56.84 KiB 0 B 0.00%
Flagging 944 B 944 B 0 B 0.00%
Rum Slim 130.12 KiB 130.34 KiB +226 B +0.17%
Worker 23.63 KiB 23.63 KiB 0 B 0.00%
🚀 CPU Performance
Action Name Base CPU Time (ms) Local CPU Time (ms) 𝚫%
RUM - add global context 0.004 0.0053 +32.50%
RUM - add action 0.0131 0.0143 +9.16%
RUM - add error 0.0134 0.0131 -2.24%
RUM - add timing 0.0032 0.003 -6.25%
RUM - start view 0.013 0.0127 -2.31%
RUM - start/stop session replay recording 0.0008 0.0007 -12.50%
Logs - log message 0.0185 0.015 -18.92%
🧠 Memory Performance
Action Name Base Memory Consumption Local Memory Consumption 𝚫
RUM - add global context 27.26 KiB 24.49 KiB -2.76 KiB
RUM - add action 51.75 KiB 50.67 KiB -1.08 KiB
RUM - add timing 26.51 KiB 25.78 KiB -746 B
RUM - add error 55.32 KiB 55.40 KiB +86 B
RUM - start/stop session replay recording 25.65 KiB 25.70 KiB +55 B
RUM - start view 452.33 KiB 456.31 KiB +3.98 KiB
Logs - log message 45.05 KiB 43.97 KiB -1.08 KiB

🔗 RealWorld

@allspain
Copy link
Collaborator Author

Comparison: This PR vs PR #3597

Both PRs fix #3935 (first view event missing pre-start context), but take fundamentally different approaches.

This PR: Synchronous context forwarding

Approach: Snapshot pre-start context values in tryStartRum(), pass them through doStartRumstartRumstartRumEventCollection, and call setContext() on each real context manager before startViewCollection() fires the first view event.

Pros:

  • Synchronous — no timing changes, no microtask delays
  • Minimal diff (5 files, ~165 lines including tests)
  • No changes to view collection or buffer drain order
  • Easy to reason about: context is set before views start, period

Cons:

  • Only handles context set before init() — context set in the same microtask as init() but after it (e.g., init(); setGlobalContext({...})) still misses the first view event
  • Adds a new InitialContexts interface threading through 3 function signatures

PR #3597: Microtask-delayed first view update

Approach: Wraps the first view update emission in enqueueMicroTask() (Promise.resolve().then()), giving the synchronous buffer drain time to complete before the first view event is assembled.

Pros:

  • Handles context set both before AND immediately after init() in the same synchronous execution block
  • No new interfaces or parameter threading needed
  • More general fix — any buffered API call (not just context) benefits from the delay

Cons:

  • Introduces async behavior into what was previously a synchronous initialization path
  • Requires significant test refactoring (~197 deletions, ~141 additions in trackViews.spec.ts) because tests must now account for the microtask delay
  • The first view event timing changes subtly, which could affect other behaviors or tests that depend on synchronous view availability after init()

Recommendation

PR #3597's approach is more comprehensive and handles a broader set of edge cases. If the team is comfortable with the async timing change and the test refactoring, it's the stronger long-term solution. This PR's approach is lower-risk and simpler but narrower in scope.

Capture global, user, and account contexts set before init() and restore
them after startRum() so the first view event includes pre-start context.

Skip restoring empty account context to avoid triggering a spurious
"property id of account is required" warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@allspain allspain force-pushed the rick.klein/3935/missing-global-context-init branch from 3ac6e4e to 338234a Compare March 10, 2026 16:04
// Initialize context managers with pre-start values so the first view event
// includes any context set before init() was called (see #3935)
if (initialContexts) {
globalContext.setContext(initialContexts.globalContext)
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a regression here for storeContextsAcrossPages users.

When that flag is on, startGlobalContext calls storeContextManager internally, which synchronously loads the previous session's context from localStorage before returning. So by the time we get here, the managers already have that data.

If nothing was set before init(), initialContexts.globalContext and initialContexts.userContext are both {}. Calling setContext({}) wipes what was just loaded — and since changeObservable fires right away, dumpToStorage runs and writes {} back to localStorage. The stored data is gone permanently.

accountContext is fine because the isEmptyObject guard below protects it, but the other two aren't guarded.

For a user with storeContextsAcrossPages who doesn't set any pre-start context:

  • before this PR: first view = {} (the bug), subsequent events = { storedKey: 'value' }
  • after this PR: first view = {} (still wrong), subsequent = {}, localStorage wiped

Adding the same guard to all three should fix it:

if (!isEmptyObject(initialContexts.globalContext)) {
  globalContext.setContext(initialContexts.globalContext)
}
if (!isEmptyObject(initialContexts.userContext)) {
  userContext.setContext(initialContexts.userContext)
}
if (!isEmptyObject(initialContexts.accountContext)) {
  accountContext.setContext(initialContexts.accountContext)
}

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.

2 participants