import { SKIPPED, computeStackTrace, objectEntries, addTelemetryError, HookNames } from '@datadog/browser-core' import type { Hooks, DefaultRumEventAttributes, AssembleHookParams } from '../hooks' interface SourceCodeContext { service: string version?: string } export interface BrowserWindow { DD_SOURCE_CODE_CONTEXT?: { [stack: string]: SourceCodeContext } } type StackFrameUrl = string export function startSourceCodeContext(hooks: Hooks) { const browserWindow = window as BrowserWindow const contextByFile = new Map() function buildContextByFile() { if (!browserWindow.DD_SOURCE_CODE_CONTEXT) { return } objectEntries(browserWindow.DD_SOURCE_CODE_CONTEXT).forEach(([stack, context]) => { const stackTrace = computeStackTrace({ stack }) const firstFrame = stackTrace.stack[0] if (!firstFrame.url) { addTelemetryError('Source code context: missing frame url', { stack }) return } // don't overwrite existing context if (!contextByFile.has(firstFrame.url)) { contextByFile.set(firstFrame.url, context) } }) browserWindow.DD_SOURCE_CODE_CONTEXT = {} } buildContextByFile() hooks.register(HookNames.Assemble, ({ domainContext, rawRumEvent }): DefaultRumEventAttributes | SKIPPED => { buildContextByFile() if (contextByFile.size === 0) { return SKIPPED } const url = getSourceUrl(domainContext, rawRumEvent) const context = url && contextByFile.get(url) if (!context) { return SKIPPED } return { type: rawRumEvent.type, service: context.service, version: context.version, } }) } function getSourceUrl( domainContext: AssembleHookParams['domainContext'], rawRumEvent: AssembleHookParams['rawRumEvent'] ) { if (rawRumEvent.type === 'long_task' && rawRumEvent.long_task.entry_type === 'long-animation-frame') { return rawRumEvent.long_task.scripts[0]?.source_url } let stack if ('handlingStack' in domainContext) { stack = domainContext.handlingStack } if (rawRumEvent.type === 'error' && rawRumEvent.error.stack) { stack = rawRumEvent.error.stack } const stackTrace = computeStackTrace({ stack }) return stackTrace.stack[0]?.url }