import { isSanitizedJourneyEvent, sanitizeJourneyEvent } from '../core/sanitize'; import { getJourneyRumFallback, setJourneyRumFallback } from '../global'; import type { JourneyEvent, JourneyStepEvent } from '../core/types'; import { journeyLogsLink, requestLogsUrl } from './kibana'; export { normalizeTagValue } from '../core/sanitize'; const JOURNEY_ACTION = 'user_journey'; const DD_RUM_KEY = 'DD_RUM'; /** * Not imported from @datadog/browser-rum on purpose: importing it sets globalThis.DD_RUM at * module load, which lets an MFE bundle replace the host's initialized RUM instance. */ interface RumLike { addAction(name: string, context?: object): void; getUser?(): unknown; } function getDdRum(): RumLike | undefined { return (globalThis as Record)[DD_RUM_KEY] as RumLike | undefined; } /** * Publish a RUM instance for MFE bundles to share — **only needed if * `globalThis.DD_RUM` is not set**. * * Prefer exposing the initialized instance as `globalThis.DD_RUM` (ST host: * `datadogGuard`). `sendToDatadog` resolves `DD_RUM` first, then `__stJourney.rum` * (this fallback); with neither present the event is dropped. * * Calling `setJourneyRum` while `globalThis.DD_RUM` is already set logs a warning — * the call is redundant because `sendToDatadog` prefers `DD_RUM`. */ export function setJourneyRum(rum: unknown): void { if (getDdRum()) { // eslint-disable-next-line no-console -- soft-deprecate redundant bridge use console.warn( '[journey] setJourneyRum() is unnecessary when globalThis.DD_RUM is set — sendToDatadog already prefers DD_RUM' ); } setJourneyRumFallback(rum); } function resolveRum(): RumLike | undefined { return getDdRum() ?? (getJourneyRumFallback() as RumLike | undefined); } function toDatadogStep(step: JourneyStepEvent) { /* eslint-disable @typescript-eslint/naming-convention -- Datadog RUM wire format (snake_case) */ return { name: step.name, start_ms: step.startMs, duration_ms: step.durationMs, outcome: step.outcome, ...(step.reason ? { reason: step.reason } : {}), ...(step.httpStatus !== undefined ? { http_status: step.httpStatus } : {}), ...(step.requests?.length ? { requests: step.requests.map(request => ({ request_id: request.rayId, started_at: new Date(request.startedAtEpoch).toISOString(), duration_ms: request.durationMs, http_status: request.status, kibana_url: requestLogsUrl(request), })), } : {}), ...(step.requestsTruncated ? { requests_truncated: true } : {}), ...(step.attributes && Object.keys(step.attributes).length ? { attributes: step.attributes } : {}), }; /* eslint-enable @typescript-eslint/naming-convention */ } /** Ship one terminal journey as a RUM custom action (`user_journey`). */ export function sendToDatadog(payload: JourneyEvent): void { const rum = resolveRum(); if (!rum || typeof rum.addAction !== 'function') { return; } const j = isSanitizedJourneyEvent(payload) ? payload.journey : sanitizeJourneyEvent(payload).journey; const logs = journeyLogsLink(j, () => rum.getUser?.(), globalThis.location?.href); /* eslint-disable @typescript-eslint/naming-convention -- Datadog RUM wire format (snake_case) */ rum.addAction(JOURNEY_ACTION, { journey: { name: j.name, team: j.team, group: j.group, service: j.service, outcome: j.outcome, ...(j.failedRequest ? { failed_request_id: j.failedRequest.rayId, } : {}), ...(logs ? { kibana_url: logs.url, kibana_scope: logs.scope } : {}), ...(j.reason ? { reason: j.reason } : {}), duration_ms: j.durationMs, steps: j.steps.map(toDatadogStep), ...(j.expected ? { expected: j.expected } : {}), ...(j.tags ? { tags: j.tags } : {}), }, }); /* eslint-enable @typescript-eslint/naming-convention */ }