From 4e0b7ad86a02e2afce70f526c43eee58c80ef525 Mon Sep 17 00:00:00 2001 From: Rudi Mocnik <17800978+rudimocnik@users.noreply.github.com> Date: Fri, 29 May 2026 13:44:04 +0200 Subject: [PATCH 1/6] chore: add Datadog RUM SDK dependencies Co-Authored-By: Claude Opus 4.8 --- package.json | 2 ++ pnpm-lock.yaml | 60 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3d0cca44f..b524c63ff 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "@cosmjs/stargate": "^0.32.1", "@cosmjs/tendermint-rpc": "^0.32.1", "@datadog/browser-logs": "^5.23.3", + "@datadog/browser-rum": "^5.23.3", + "@datadog/browser-rum-react": "^5.23.3", "@dydxprotocol/v4-client-js": "3.6.0", "@dydxprotocol/v4-localization": "1.1.404", "@dydxprotocol/v4-proto": "^7.0.0-dev.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29fabece6..0a6bc1e59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,13 @@ dependencies: version: 0.32.4 '@datadog/browser-logs': specifier: ^5.23.3 - version: 5.35.1 + version: 5.35.1(@datadog/browser-rum@5.35.1) + '@datadog/browser-rum': + specifier: ^5.23.3 + version: 5.35.1(@datadog/browser-logs@5.35.1) + '@datadog/browser-rum-react': + specifier: ^5.23.3 + version: 5.35.1(@datadog/browser-rum@5.35.1)(react-router-dom@6.30.1)(react@18.3.1) '@dydxprotocol/v4-client-js': specifier: 3.6.0 version: 3.6.0 @@ -1660,17 +1666,61 @@ packages: resolution: {integrity: sha512-zjmw3WkF5syMq5+2jneSgSILxO3DTS+hKw270tzk/yQUfJIGInyGrkHUYYGLmZaVuVp+6F7iO3tUAwIqQYGBFw==} dev: false - /@datadog/browser-logs@5.35.1: + /@datadog/browser-logs@5.35.1(@datadog/browser-rum@5.35.1): resolution: {integrity: sha512-UghvsmCy1dTBVTicdDSXxk3l+xMdKKWLbyrwdoQJaqId3qAJ3vMez1UdOL2XulQTw1cy28DTzYy2EHK+ngfahA==} peerDependencies: '@datadog/browser-rum': 5.35.1 peerDependenciesMeta: '@datadog/browser-rum': optional: true + dependencies: + '@datadog/browser-core': 5.35.1 + '@datadog/browser-rum': 5.35.1(@datadog/browser-logs@5.35.1) + dev: false + + /@datadog/browser-rum-core@5.35.1: + resolution: {integrity: sha512-+WMHeL83BvFTE618SBx7VKT3gwJ/c1DuNehvv+ZbbMqRgrl2hBng5cLfhcEW/U6bLOyr08ZfaZ1m2r/xs2n4aA==} dependencies: '@datadog/browser-core': 5.35.1 dev: false + /@datadog/browser-rum-react@5.35.1(@datadog/browser-rum@5.35.1)(react-router-dom@6.30.1)(react@18.3.1): + resolution: {integrity: sha512-C6fqeNXBhNtm8kc8+K5wlGvcYY0EuY8NbUwWGwMnX74keZKit3FaiCFGWN6JHixYbGlDp+yTKiiIsFTKrDTYbw==} + peerDependencies: + '@datadog/browser-rum': '*' + '@datadog/browser-rum-slim': '*' + react: '18' + react-router-dom: '6' + peerDependenciesMeta: + '@datadog/browser-rum': + optional: true + '@datadog/browser-rum-slim': + optional: true + react: + optional: true + react-router-dom: + optional: true + dependencies: + '@datadog/browser-core': 5.35.1 + '@datadog/browser-rum': 5.35.1(@datadog/browser-logs@5.35.1) + '@datadog/browser-rum-core': 5.35.1 + react: 18.3.1 + react-router-dom: 6.30.1(react-dom@18.3.1)(react@18.3.1) + dev: false + + /@datadog/browser-rum@5.35.1(@datadog/browser-logs@5.35.1): + resolution: {integrity: sha512-/I3mQpnAuGZ1DU3wevgirKfT1DXuWY1eg4gOHjmONN91KThVoiuHg9HiAowfFFtQArsbcP5HqdA4lLJvH6w17A==} + peerDependencies: + '@datadog/browser-logs': 5.35.1 + peerDependenciesMeta: + '@datadog/browser-logs': + optional: true + dependencies: + '@datadog/browser-core': 5.35.1 + '@datadog/browser-logs': 5.35.1(@datadog/browser-rum@5.35.1) + '@datadog/browser-rum-core': 5.35.1 + dev: false + /@dydxprotocol/v4-client-js@3.6.0: resolution: {integrity: sha512-rg9JTE07X7LyfrFadH+jnNXFFSgMfv5TrfOOqowfzyT5l4MGwDbNArRR1LfI4abcbSMCQRhKsCXEqpM3p3oZ+w==} dependencies: @@ -26676,7 +26726,7 @@ packages: resolution: {integrity: sha512-ZCfOjYKAjaX2TGI8uif5ah+J3BYFuo+47JOIV1RIz2l7kD9VfnxvRH5UiQDRyMALQC7KFd2hUqIEtHklapNyKA==} engines: {node: '>=8.0.0'} dependencies: - bn.js: 5.2.2 + bn.js: 5.2.3 web3-utils: 1.10.3 dev: false @@ -26724,7 +26774,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: '@ethereumjs/util': 8.1.0 - bn.js: 5.2.2 + bn.js: 5.2.3 ethereum-bloom-filters: 1.2.0 ethereum-cryptography: 2.2.1 ethjs-unit: 0.1.6 @@ -27567,4 +27617,4 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - dev: true \ No newline at end of file + dev: true From ee72e8ac295caee7f11049d02f93ebc009cb5616 Mon Sep 17 00:00:00 2001 From: Rudi Mocnik <17800978+rudimocnik@users.noreply.github.com> Date: Fri, 29 May 2026 13:45:35 +0200 Subject: [PATCH 2/6] feat: initialize Datadog RUM (collection only) Co-Authored-By: Claude Opus 4.8 --- .env.example | 6 ++++++ src/lib/analytics/datadogRum.ts | 33 +++++++++++++++++++++++++++++++++ src/main.tsx | 3 +++ 3 files changed, 42 insertions(+) create mode 100644 src/lib/analytics/datadogRum.ts diff --git a/.env.example b/.env.example index 98e4d9707..e20a2a96d 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,12 @@ VITE_FUNKIT_API_KEY= # URL for the qrcode that is generated within the modal share pnl analytics VITE_SHARE_PNL_ANALYTICS_URL= +# Datadog RUM (leave blank to disable; requires application id, token, and env) +VITE_DATADOG_RUM_APPLICATION_ID= +VITE_DATADOG_RUM_CLIENT_TOKEN= +VITE_DATADOG_RUM_ENV= +VITE_DATADOG_RUM_SESSION_SAMPLE_RATE= + AMPLITUDE_API_KEY= AMPLITUDE_SERVER_URL= AMPLITUDE_SERVER_ZONE= diff --git a/src/lib/analytics/datadogRum.ts b/src/lib/analytics/datadogRum.ts new file mode 100644 index 000000000..4d9ecbcc3 --- /dev/null +++ b/src/lib/analytics/datadogRum.ts @@ -0,0 +1,33 @@ +import { datadogRum } from '@datadog/browser-rum'; +import { reactPlugin } from '@datadog/browser-rum-react'; + +const APPLICATION_ID = import.meta.env.VITE_DATADOG_RUM_APPLICATION_ID; +const CLIENT_TOKEN = import.meta.env.VITE_DATADOG_RUM_CLIENT_TOKEN; +const ENV = import.meta.env.VITE_DATADOG_RUM_ENV; +const SESSION_SAMPLE_RATE = Number(import.meta.env.VITE_DATADOG_RUM_SESSION_SAMPLE_RATE ?? 100); + +const SERVICE_NAME = 'v4-web'; +const SITE_NAME = 'ap1.datadoghq.com'; + +// VITE_LAST_TAG looks like "tags/release/v2.7.4" — keep only "v2.7.4". +const rawTag = import.meta.env.VITE_LAST_TAG; +const VERSION = rawTag ? rawTag.split('/').pop() : undefined; + +export function initializeDatadogRum() { + if (!APPLICATION_ID || !CLIENT_TOKEN || !ENV) return; + + datadogRum.init({ + applicationId: APPLICATION_ID, + clientToken: CLIENT_TOKEN, + site: SITE_NAME, + service: SERVICE_NAME, + env: ENV, + version: VERSION, + sessionSampleRate: Number.isFinite(SESSION_SAMPLE_RATE) ? SESSION_SAMPLE_RATE : 100, + sessionReplaySampleRate: 0, + trackResources: true, + trackUserInteractions: true, + trackLongTasks: true, + plugins: [reactPlugin({ router: false })], + }); +} diff --git a/src/main.tsx b/src/main.tsx index 46744cb3f..65137ed71 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,11 +9,14 @@ import { BrowserRouter, HashRouter } from 'react-router-dom'; import { storeLifecycles } from './bonsai/storeLifecycles'; import { ErrorBoundary } from './components/ErrorBoundary'; import './index.css'; +import { initializeDatadogRum } from './lib/analytics/datadogRum'; import { runFn } from './lib/do'; import { store } from './state/_store'; const Router = import.meta.env.VITE_ROUTER_TYPE === 'hash' ? HashRouter : BrowserRouter; +initializeDatadogRum(); + runFn(async () => { // we ignore the cleanups for now since we want these running forever storeLifecycles.forEach((fn) => fn(store)); From d42cd913532b0705bee5f751980fe12ba7e8abb9 Mon Sep 17 00:00:00 2001 From: Rudi Mocnik <17800978+rudimocnik@users.noreply.github.com> Date: Fri, 29 May 2026 15:03:09 +0200 Subject: [PATCH 3/6] feat: forward errors to Datadog RUM with React error tracking Co-Authored-By: Claude Opus 4.8 --- src/components/ErrorBoundary.tsx | 4 +++- src/lib/analytics/datadogRum.ts | 29 ++++++++++++++++++++++++++++- src/lib/telemetry.ts | 3 +++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 7085afcc7..348740399 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,12 +1,14 @@ import React from 'react'; +import { reportRumReactError } from '@/lib/analytics/datadogRum'; import { log } from '@/lib/telemetry'; type ErrorBoundaryProps = { children: React.ReactNode }; export class ErrorBoundary extends React.Component { - componentDidCatch(error: Error): void { + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { log('ErrorBoundary', error); + reportRumReactError(error, errorInfo); } render() { diff --git a/src/lib/analytics/datadogRum.ts b/src/lib/analytics/datadogRum.ts index 4d9ecbcc3..e539758b9 100644 --- a/src/lib/analytics/datadogRum.ts +++ b/src/lib/analytics/datadogRum.ts @@ -1,5 +1,7 @@ +import type { ErrorInfo } from 'react'; + import { datadogRum } from '@datadog/browser-rum'; -import { reactPlugin } from '@datadog/browser-rum-react'; +import { addReactError, reactPlugin } from '@datadog/browser-rum-react'; const APPLICATION_ID = import.meta.env.VITE_DATADOG_RUM_APPLICATION_ID; const CLIENT_TOKEN = import.meta.env.VITE_DATADOG_RUM_CLIENT_TOKEN; @@ -13,6 +15,17 @@ const SITE_NAME = 'ap1.datadoghq.com'; const rawTag = import.meta.env.VITE_LAST_TAG; const VERSION = rawTag ? rawTag.split('/').pop() : undefined; +let isInitialized = false; + +// Locations that reach RUM through a more specific path, so log() must not also +// forward them as generic errors: unhandled rejections are auto-captured by RUM, +// and ErrorBoundary catches are reported via reportRumReactError (addReactError). +const RUM_SKIP_FORWARD_LOCATIONS = new Set([ + 'window/onunhandledrejection', + 'window/onrejectionhandled', + 'ErrorBoundary', +]); + export function initializeDatadogRum() { if (!APPLICATION_ID || !CLIENT_TOKEN || !ENV) return; @@ -30,4 +43,18 @@ export function initializeDatadogRum() { trackLongTasks: true, plugins: [reactPlugin({ router: false })], }); + + isInitialized = true; +} + +export function reportRumError(location: string, error?: Error, metadata?: object) { + if (!isInitialized || RUM_SKIP_FORWARD_LOCATIONS.has(location)) return; + datadogRum.addError(error ?? new Error(location), { location, ...metadata }); +} + +// React render errors caught by ErrorBoundary: addReactError reports them as a +// ReactRenderingError with the component stack and `framework: 'react'`. +export function reportRumReactError(error: Error, errorInfo: ErrorInfo) { + if (!isInitialized) return; + addReactError(error, errorInfo); } diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts index e0106afb8..72aa1b06b 100644 --- a/src/lib/telemetry.ts +++ b/src/lib/telemetry.ts @@ -3,6 +3,7 @@ import { isDev } from '@/constants/networks'; import { track } from './analytics/analytics'; import { dd } from './analytics/datadog'; +import { reportRumError } from './analytics/datadogRum'; let lastLogTime = Date.now(); @@ -35,6 +36,8 @@ export const log = (location: string, error?: Error, metadata?: object) => { dd.error(`[Error] ${location}`, metadata, error); + reportRumError(location, error, metadata); + globalThis.dispatchEvent(customEvent); }; From 39e52990b13829ee6217edface7507831f5452da Mon Sep 17 00:00:00 2001 From: Rudi Mocnik <17800978+rudimocnik@users.noreply.github.com> Date: Fri, 29 May 2026 15:33:32 +0200 Subject: [PATCH 4/6] feat: enable Datadog RUM Session Replay (masked, sample rate via env) Co-Authored-By: Claude Opus 4.8 --- .env.example | 1 + src/lib/analytics/datadogRum.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index e20a2a96d..c48c6e273 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,7 @@ VITE_DATADOG_RUM_APPLICATION_ID= VITE_DATADOG_RUM_CLIENT_TOKEN= VITE_DATADOG_RUM_ENV= VITE_DATADOG_RUM_SESSION_SAMPLE_RATE= +VITE_DATADOG_RUM_SESSION_REPLAY_SAMPLE_RATE= AMPLITUDE_API_KEY= AMPLITUDE_SERVER_URL= diff --git a/src/lib/analytics/datadogRum.ts b/src/lib/analytics/datadogRum.ts index e539758b9..d1f45d2fe 100644 --- a/src/lib/analytics/datadogRum.ts +++ b/src/lib/analytics/datadogRum.ts @@ -7,6 +7,9 @@ const APPLICATION_ID = import.meta.env.VITE_DATADOG_RUM_APPLICATION_ID; const CLIENT_TOKEN = import.meta.env.VITE_DATADOG_RUM_CLIENT_TOKEN; const ENV = import.meta.env.VITE_DATADOG_RUM_ENV; const SESSION_SAMPLE_RATE = Number(import.meta.env.VITE_DATADOG_RUM_SESSION_SAMPLE_RATE ?? 100); +const SESSION_REPLAY_SAMPLE_RATE = Number( + import.meta.env.VITE_DATADOG_RUM_SESSION_REPLAY_SAMPLE_RATE ?? 0 +); const SERVICE_NAME = 'v4-web'; const SITE_NAME = 'ap1.datadoghq.com'; @@ -37,7 +40,10 @@ export function initializeDatadogRum() { env: ENV, version: VERSION, sessionSampleRate: Number.isFinite(SESSION_SAMPLE_RATE) ? SESSION_SAMPLE_RATE : 100, - sessionReplaySampleRate: 0, + sessionReplaySampleRate: Number.isFinite(SESSION_REPLAY_SAMPLE_RATE) + ? SESSION_REPLAY_SAMPLE_RATE + : 0, + defaultPrivacyLevel: 'mask', trackResources: true, trackUserInteractions: true, trackLongTasks: true, From fe1740e03bc0da51695110226348c186205033e3 Mon Sep 17 00:00:00 2001 From: Rudi Mocnik <17800978+rudimocnik@users.noreply.github.com> Date: Fri, 29 May 2026 16:55:05 +0200 Subject: [PATCH 5/6] feat: forward Statsig feature flags to Datadog RUM Co-Authored-By: Claude Opus 4.8 --- src/lib/analytics/datadogRum.ts | 6 ++++++ src/lib/statsig.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/lib/analytics/datadogRum.ts b/src/lib/analytics/datadogRum.ts index d1f45d2fe..206a8d23a 100644 --- a/src/lib/analytics/datadogRum.ts +++ b/src/lib/analytics/datadogRum.ts @@ -64,3 +64,9 @@ export function reportRumReactError(error: Error, errorInfo: ErrorInfo) { if (!isInitialized) return; addReactError(error, errorInfo); } + +// Feature flag evaluations, recorded on RUM views/errors as @feature_flags.. +export function reportRumFeatureFlagEvaluation(key: string, value: unknown) { + if (!isInitialized) return; + datadogRum.addFeatureFlagEvaluation(key, value); +} diff --git a/src/lib/statsig.ts b/src/lib/statsig.ts index eebf2e33c..19d2ba419 100644 --- a/src/lib/statsig.ts +++ b/src/lib/statsig.ts @@ -2,6 +2,8 @@ import { StatsigClient } from '@statsig/js-client'; import { STATSIG_ENVIRONMENT_TIER } from '@/constants/networks'; +import { reportRumFeatureFlagEvaluation } from '@/lib/analytics/datadogRum'; + let statsigClient: StatsigClient; let initPromise: Promise | null = null; @@ -29,6 +31,9 @@ export const initStatsigAsync = async () => { environment: { tier: STATSIG_ENVIRONMENT_TIER }, } ); + statsigClient.on('gate_evaluation', ({ gate }) => { + reportRumFeatureFlagEvaluation(gate.name, gate.value); + }); await statsigClient.initializeAsync(); return statsigClient; })(); From ea24ab891bedd739f6214c092b355659253bfef5 Mon Sep 17 00:00:00 2001 From: Rudi Mocnik <17800978+rudimocnik@users.noreply.github.com> Date: Fri, 29 May 2026 17:22:20 +0200 Subject: [PATCH 6/6] feat: attach user identity and context to Datadog RUM Co-Authored-By: Claude Opus 4.8 --- src/lib/analytics/analytics.ts | 3 +++ src/lib/analytics/datadogRum.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/lib/analytics/analytics.ts b/src/lib/analytics/analytics.ts index 143ce3023..725e40f1e 100644 --- a/src/lib/analytics/analytics.ts +++ b/src/lib/analytics/analytics.ts @@ -7,6 +7,7 @@ import { } from '@/constants/analytics'; import { dd } from './datadog'; +import { reportRumUserProperty } from './datadogRum'; const DEBUG_ANALYTICS = false; @@ -26,6 +27,8 @@ export const identify = (property: AnalyticsUserProperty) => { dd.info(`set context item: ${ddPropertyName} to value ${property.payload}`, dd.getContext()); globalThis.dispatchEvent(customEvent); + + reportRumUserProperty(propertyTypeToLog, property.payload); }; export const track = (event: AnalyticsEvent) => { diff --git a/src/lib/analytics/datadogRum.ts b/src/lib/analytics/datadogRum.ts index 206a8d23a..0d8c6f433 100644 --- a/src/lib/analytics/datadogRum.ts +++ b/src/lib/analytics/datadogRum.ts @@ -70,3 +70,31 @@ export function reportRumFeatureFlagEvaluation(key: string, value: unknown) { if (!isInitialized) return; datadogRum.addFeatureFlagEvaluation(key, value); } + +// identify() property names that define identity go on the RUM user (@usr.*); +// everything else becomes searchable global context (@context.*). Statsig flag +// objects are skipped here — they're already captured via addFeatureFlagEvaluation. +const RUM_USER_PROPERTY_KEYS: Record = { + dydxAddress: 'id', + walletAddress: 'walletAddress', + walletType: 'walletType', + userId: 'userId', +}; + +const RUM_SKIP_USER_PROPERTIES = new Set(['statsigFlags', 'customFlags']); + +// Mirror identify() user properties into RUM. A null/undefined value removes the +// property, so anonymous (never-connected) sessions carry no usr.id at all. +export function reportRumUserProperty(name: string, value: unknown) { + if (!isInitialized || RUM_SKIP_USER_PROPERTIES.has(name)) return; + + const userKey = RUM_USER_PROPERTY_KEYS[name]; + if (userKey) { + if (value == null) datadogRum.removeUserProperty(userKey); + else datadogRum.setUserProperty(userKey, value); + } else if (value == null) { + datadogRum.removeGlobalContextProperty(name); + } else { + datadogRum.setGlobalContextProperty(name, value); + } +}