Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ 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=
VITE_DATADOG_RUM_SESSION_REPLAY_SAMPLE_RATE=

AMPLITUDE_API_KEY=
AMPLITUDE_SERVER_URL=
AMPLITUDE_SERVER_ZONE=
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
60 changes: 55 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -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<ErrorBoundaryProps> {
componentDidCatch(error: Error): void {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
log('ErrorBoundary', error);
reportRumReactError(error, errorInfo);
}

render() {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@/constants/analytics';

import { dd } from './datadog';
import { reportRumUserProperty } from './datadogRum';

const DEBUG_ANALYTICS = false;

Expand All @@ -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) => {
Expand Down
100 changes: 100 additions & 0 deletions src/lib/analytics/datadogRum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { ErrorInfo } from 'react';

import { datadogRum } from '@datadog/browser-rum';
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;
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';

// 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;

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;

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: Number.isFinite(SESSION_REPLAY_SAMPLE_RATE)
? SESSION_REPLAY_SAMPLE_RATE
: 0,
defaultPrivacyLevel: 'mask',
trackResources: true,
trackUserInteractions: true,
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);
}

// Feature flag evaluations, recorded on RUM views/errors as @feature_flags.<key>.
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<string, string> = {
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);
}
}
5 changes: 5 additions & 0 deletions src/lib/statsig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StatsigClient> | null = null;

Expand Down Expand Up @@ -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;
})();
Expand Down
3 changes: 3 additions & 0 deletions src/lib/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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);
};

Expand Down
3 changes: 3 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading