Skip to main content

RUM integration

RUM lives in the @cues/sawdust-datadog provider package, not core. Install it alongside core and import from the /rum subpath:

pnpm add @cues/sawdust-datadog @datadog/browser-rum

Historically the browser build relied on Datadog's global singleton. Sawdust exposes getRumClient so any shared module can reach the configured RUM client through the service locator — matching the logger pattern.

Traditional setup (for contrast)

import { datadogRum } from '@datadog/browser-rum'

datadogRum.init({
clientToken: 'abc',
applicationId: '123',
site: 'datadoghq.com',
service: 'my-web-app',
env: 'prod',
})
datadogRum.startSessionReplayRecording()

Every module had to import datadogRum directly or rely on globals, which made testing hard.

The Sawdust flow

Bootstrap once with getRumClient (or setRumClient if you already have an instance) and let shared code resolve the same client.

// app/rum/bootstrap.ts
import { getRumClient } from '@cues/sawdust-datadog/rum'

export function ensureRum() {
const rum = getRumClient({
enabled: true,
enableInDev: process.env.NEXT_PUBLIC_ENABLE_RUM === 'true',
init: {
clientToken: process.env.NEXT_PUBLIC_DD_CLIENT_TOKEN!,
applicationId: process.env.NEXT_PUBLIC_DD_APP_ID!,
service: 'my-web-app',
env: process.env.NEXT_PUBLIC_ENVIRONMENT,
version: process.env.NEXT_PUBLIC_APP_VERSION,
},
})

rum.addAction('rum-initialized')
return rum
}

Call ensureRum() once during browser hydration. The locator stores the client, so subsequent getRumClient() calls return the same instance.

Shared-library usage

// packages/shared-metrics/src/recordFeatureUsage.ts
import { getRumClient } from '@cues/sawdust-datadog/rum'

export function recordFeatureUsage(feature: string) {
const rum = getRumClient()
rum.addAction('feature-used', { feature })
rum.setGlobalAttribute('lastFeature', feature)
}

No direct datadogRum imports; works in any consumer app without extra wiring.

Identifying users

RUM types now ship from the provider package (@cues/sawdust-datadog/rum, or @cues/sawdust-datadog/types), not from core.

import type { RumClient, RumUser } from '@cues/sawdust-datadog/rum'

const anonymousUser: RumUser = { id, anonymousId, type: 'anonymous' }
rum.setUser(anonymousUser)
rum.setGlobalAttribute('authenticated', false)

Forwarding errors to RUM (opt-in)

Error forwarding used to happen automatically inside the browser logger. It is now an opt-in plugin so core stays provider-agnostic. Add datadogRumErrorPlugin() to plugins and any error-bearing log is mirrored to RUM via rum.addError — while still reaching every transport.

import { configureLogger } from '@cues/sawdust/logger'
import { datadogRumErrorPlugin } from '@cues/sawdust-datadog/rum'

configureLogger(
{
prefix: '[UI]',
transports: { console: { enabled: true } },
plugins: [datadogRumErrorPlugin()],
},
{ id: 'web:final', stage: 'final' },
)

The plugin resolves the client through the same locator, so wire up getRumClient(...) / setRumClient(...) (above) first — forwarding is a no-op until a client is installed and isEnabled() returns true.

Testing with mocks

import { setRumClient, resetRumClientLocator, getRumClient } from '@cues/sawdust-datadog/rum'

beforeEach(() => {
resetRumClientLocator()
setRumClient({
init: jest.fn(),
reset: jest.fn(),
isEnabled: jest.fn(() => true),
addAction: jest.fn(),
addTiming: jest.fn(),
addError: jest.fn(),
startView: jest.fn(),
stopSession: jest.fn(),
setViewContext: jest.fn(),
setViewName: jest.fn(),
setUser: jest.fn(),
clearUser: jest.fn(),
setGlobalContext: jest.fn(),
getGlobalContext: jest.fn(() => ({})),
setGlobalAttribute: jest.fn(),
removeGlobalAttribute: jest.fn(),
})
})

test('records feature usage', () => {
recordFeatureUsage('search')
expect(getRumClient().addAction).toHaveBeenCalledWith('feature-used', { feature: 'search' })
})

Tips

  • Prefer getRumClient(options?) to lazy-initialize when credentials are available. Passing options again re-runs init in place.
  • If a framework hands you a client, call setRumClient(existingClient).
  • Reset the locator in tests to avoid cross-suite leakage.