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. Passingoptionsagain re-runsinitin place. - If a framework hands you a client, call
setRumClient(existingClient). - Reset the locator in tests to avoid cross-suite leakage.