Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions docs/plans/2026-03-03-init-user-global-context-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Design: Initial User and Global Context via `RumInitConfiguration`

**Date:** 2026-03-03
**Author:** Adrian de la Rosa
**Branch:** `adlrb/ssi-breaking`
**Status:** Approved

## Problem

The `@datadog/browser-remote-config` schema defines `rum.user` (`ContextItem[]`) and `rum.context` (`ContextItem[]`) fields that carry dynamic key-value pairs for populating user context and global context at SDK init time. These fields are embedded verbatim in bundles generated by `@datadog/browser-sdk-endpoint`, but `validateAndBuildRumConfiguration` in `rum-core` does not recognize them β€” they are silently dropped. `resolveDynamicValues` is never called, and `setUser`/`setGlobalContextProperty` are never invoked.

## Decision

Extend `RumInitConfiguration` with two new optional fields:

- `user?: User` β€” applied via the user context manager after `startRum()`, equivalent to calling `datadogRum.setUser()` at init time
- `globalContext?: Context` β€” applied via the global context manager after `startRum()`, equivalent to calling `datadogRum.setGlobalContextProperty()` for each key

The bundle generator (`generateCombinedBundle`) resolves `DynamicOption` values from `user[]` and `context[]` arrays at browser runtime using inlined vanilla JS helpers, then passes the resolved plain objects to `DD_RUM.init()` as `user` and `globalContext`.

## Architecture

### rum-core changes

**`packages/rum-core/src/domain/configuration/configuration.ts`**

Add to `RumInitConfiguration`:
```typescript
user?: User
globalContext?: Context
```

These types already exist in the RUM public API (`setUser` accepts `User`, `setGlobalContextProperty` accepts `Context`). No new shapes.

**`packages/rum-core/src/boot/rumPublicApi.ts`**

In `doStartRum` (the callback passed to `createPreStartStrategy`), after `startRum()` returns, check `cachedInitConfiguration` for `user` and `globalContext` and apply them through the returned context managers:

```typescript
if (cachedInitConfiguration.user) {
startRumResult.userContextManager.setContext(cachedInitConfiguration.user)
}
if (cachedInitConfiguration.globalContext) {
Object.entries(cachedInitConfiguration.globalContext).forEach(([key, value]) => {
startRumResult.globalContextManager.setContextProperty(key, value)
})
}
```

This runs after the context managers are initialized but before any views are tracked, so the first view event carries the correct user and global context.

### endpoint changes

**`packages/endpoint/src/bundleGenerator.ts`**

`generateCombinedBundle` no longer passes the raw `RumRemoteConfiguration` object directly to `DD_RUM.init()`. Instead it emits:

1. Inline resolution helpers (`__dd_resolveContextValue`, `__dd_getCookie`, `__dd_resolveJsPath`, `__dd_resolveDom`) β€” vanilla ES5, namespaced to avoid collisions, covering all four `DynamicOption` strategies plus regex `extractor` support.

2. Resolution loop that builds `user` and `globalContext` plain objects from the `ContextItem[]` arrays.

3. A single `DD_RUM.init()` call with a spread of the scalar remote config fields plus the resolved `user` and `globalContext`:

```js
var __dd_user = {};
(__DATADOG_REMOTE_CONFIG__.user || []).forEach(function(item) {
__dd_user[item.key] = __dd_resolveContextValue(item.value);
});

var __dd_ctx = {};
(__DATADOG_REMOTE_CONFIG__.context || []).forEach(function(item) {
__dd_ctx[item.key] = __dd_resolveContextValue(item.value);
});

window.DD_RUM.init(Object.assign({}, __DATADOG_REMOTE_CONFIG__, {
user: Object.keys(__dd_user).length ? __dd_user : undefined,
globalContext: Object.keys(__dd_ctx).length ? __dd_ctx : undefined,
}));
```

The resolution helpers are defined in a separate source file `packages/endpoint/src/contextResolutionHelpers.ts` that exports the JS string constant, keeping `bundleGenerator.ts` readable and allowing the helpers to be unit-tested independently.

## Data Flow

```
Remote config JSON (server)
└── fetchRemoteConfiguration() [Node.js, build time]
└── RumRemoteConfiguration
β”œβ”€β”€ scalar fields (sessionSampleRate, service, ...)
β”œβ”€β”€ user: ContextItem[] [DynamicOption values β€” unresolved]
└── context: ContextItem[] [DynamicOption values β€” unresolved]

generateCombinedBundle() [Node.js, build time]
└── Embeds raw config as __DATADOG_REMOTE_CONFIG__
└── Emits resolution helpers + loop
└── Emits DD_RUM.init({ ...scalars, user: __dd_user, globalContext: __dd_ctx })

Browser loads generated bundle [runtime]
1. __dd_resolveContextValue resolves each DynamicOption
(reads cookies, walks window.*, queries DOM)
2. DD_RUM.init({ user: { id: "resolved", ... }, globalContext: { plan: "pro" } })
3. rum-core's doStartRum applies user/globalContext via context managers
4. First view event carries correct user context and global context
```

## Non-Goals

- No changes to `setUser()` or `setGlobalContextProperty()` public API
- No new dependencies between `rum-core` and `@datadog/browser-remote-config`
- The `localStorage` strategy gap (exists in types, missing handler in `resolveDynamicOption`) is out of scope for this change

## Testing

### Unit tests

- `packages/rum-core/src/boot/rumPublicApi.spec.ts` β€” verify that `user` and `globalContext` in `initConfiguration` are applied to context managers after start; verify they are ignored gracefully if absent or empty
- `packages/endpoint/src/contextResolutionHelpers.spec.ts` β€” verify the emitted JS string resolves cookie/js/dom/string strategies correctly (run in jsdom)
- `packages/endpoint/src/bundleGenerator.spec.ts` β€” verify the generated bundle includes resolution code when `user`/`context` are present; verify correct `DD_RUM.init()` shape in generated output

### E2E tests

- `test/e2e/scenario/rum/embeddedConfig.scenario.ts` β€” add cases for static user context and global context applied from embedded config
- `test/e2e/scenario/rum/embeddedConfigDynamic.scenario.ts` β€” add cases for cookie and JS-path dynamic user context resolved correctly at page load
Loading