From 1585ff64b3711a63599d92270d52ffb567d4dfdd Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Wed, 22 Oct 2025 17:20:06 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8parse=20chrome=20html=20anonymous?= =?UTF-8?q?=20listener=20stack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stackTrace/capturedExceptions.specHelper.ts | 7 +++++++ .../src/tools/stackTrace/computeStackTrace.spec.ts | 14 ++++++++++++++ .../core/src/tools/stackTrace/computeStackTrace.ts | 14 +++++++++----- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/core/src/tools/stackTrace/capturedExceptions.specHelper.ts b/packages/core/src/tools/stackTrace/capturedExceptions.specHelper.ts index 5c212afbd1..0eee2b4f84 100644 --- a/packages/core/src/tools/stackTrace/capturedExceptions.specHelper.ts +++ b/packages/core/src/tools/stackTrace/capturedExceptions.specHelper.ts @@ -247,6 +247,13 @@ export const CHROME_111_SNIPPET = { at snippet:///snippet_file:1:13`, } +export const CHROME_141_HTML_ANONYMOUS_LISTENER = { + message: 'message string', + name: 'Error', + stack: `Error: message string +at HTMLButtonElement. @ http://path/to/file.js:1:4287`, +} + export const PHANTOMJS_1_19 = { stack: `Error: foo at file:///path/to/file.js:878 diff --git a/packages/core/src/tools/stackTrace/computeStackTrace.spec.ts b/packages/core/src/tools/stackTrace/computeStackTrace.spec.ts index 6184bbbe87..921090ca47 100644 --- a/packages/core/src/tools/stackTrace/computeStackTrace.spec.ts +++ b/packages/core/src/tools/stackTrace/computeStackTrace.spec.ts @@ -1,5 +1,6 @@ import { isSafari } from '../utils/browserDetection' import * as CapturedExceptions from './capturedExceptions.specHelper' +import { CHROME_141_HTML_ANONYMOUS_LISTENER } from './capturedExceptions.specHelper' import { computeStackTrace } from './computeStackTrace' describe('computeStackTrace', () => { @@ -1004,4 +1005,17 @@ Error: foo column: undefined, }) }) + + it('should parse stack from html button click listener', () => { + const stackFrames = computeStackTrace(CHROME_141_HTML_ANONYMOUS_LISTENER) + + expect(stackFrames.stack.length).toEqual(1) + expect(stackFrames.stack[0]).toEqual({ + args: [], + column: 4287, + func: 'HTMLButtonElement.', + line: 1, + url: 'http://path/to/file.js', + }) + }) }) diff --git a/packages/core/src/tools/stackTrace/computeStackTrace.ts b/packages/core/src/tools/stackTrace/computeStackTrace.ts index af3a21fb10..9caedaab4e 100644 --- a/packages/core/src/tools/stackTrace/computeStackTrace.ts +++ b/packages/core/src/tools/stackTrace/computeStackTrace.ts @@ -111,7 +111,11 @@ function parseChromeLine(line: string): StackFrame | undefined { } } -const CHROME_ANONYMOUS_FUNCTION_RE = new RegExp(`^\\s*at ?${fileUrl}${filePosition}?${filePosition}??\\s*$`, 'i') +const htmlAnonymousPart = '(?:(.*)?(?: @))' +const CHROME_ANONYMOUS_FUNCTION_RE = new RegExp( + `^\\s*at\\s*${htmlAnonymousPart}?\\s*${fileUrl}${filePosition}?${filePosition}??\\s*$`, + 'i' +) function parseChromeAnonymousLine(line: string): StackFrame | undefined { const parts = CHROME_ANONYMOUS_FUNCTION_RE.exec(line) @@ -122,10 +126,10 @@ function parseChromeAnonymousLine(line: string): StackFrame | undefined { return { args: [], - column: parts[3] ? +parts[3] : undefined, - func: UNKNOWN_FUNCTION, - line: parts[2] ? +parts[2] : undefined, - url: parts[1], + column: parts[4] ? +parts[4] : undefined, + func: parts[1] || UNKNOWN_FUNCTION, + line: parts[3] ? +parts[3] : undefined, + url: parts[2], } } From e111afe0fc1f8c36481506a2558a061c53428e0a Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Thu, 23 Oct 2025 11:25:30 +0200 Subject: [PATCH 2/2] POC source code context --- packages/rum-core/src/boot/startRum.ts | 2 + packages/rum-core/src/domain/assembly.ts | 2 + .../src/domain/contexts/sourceCodeContext.ts | 46 +++++++++++++++++++ packages/rum-core/src/domain/hooks.ts | 4 ++ 4 files changed, 54 insertions(+) create mode 100644 packages/rum-core/src/domain/contexts/sourceCodeContext.ts diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 0e9d6fa180..2e85d6c55f 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -57,6 +57,7 @@ import type { Hooks } from '../domain/hooks' import { createHooks } from '../domain/hooks' import { startEventCollection } from '../domain/event/eventCollection' import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry' +import { startSourceCodeContext } from '../domain/contexts/sourceCodeContext' import type { RecorderApi, ProfilerApi } from './rumPublicApi' export type StartRum = typeof startRum @@ -139,6 +140,7 @@ export function startRum( startSessionContext(hooks, session, recorderApi, viewHistory) startConnectivityContext(hooks) startTrackingConsentContext(hooks, trackingConsentState) + startSourceCodeContext(hooks) const globalContext = startGlobalContext(hooks, configuration, 'rum', true) const userContext = startUserContext(hooks, configuration, session, 'rum') const accountContext = startAccountContext(hooks, configuration, 'rum') diff --git a/packages/rum-core/src/domain/assembly.ts b/packages/rum-core/src/domain/assembly.ts index ab61f47069..8de08c2dc4 100644 --- a/packages/rum-core/src/domain/assembly.ts +++ b/packages/rum-core/src/domain/assembly.ts @@ -105,6 +105,8 @@ export function startRumAssembly( ({ startTime, duration, rawRumEvent, domainContext }) => { const defaultRumEventAttributes = hooks.triggerHook(HookNames.Assemble, { eventType: rawRumEvent.type, + rawRumEvent, + domainContext, startTime, duration, })! diff --git a/packages/rum-core/src/domain/contexts/sourceCodeContext.ts b/packages/rum-core/src/domain/contexts/sourceCodeContext.ts new file mode 100644 index 0000000000..802800579d --- /dev/null +++ b/packages/rum-core/src/domain/contexts/sourceCodeContext.ts @@ -0,0 +1,46 @@ +import type { Context } from '@datadog/browser-core' +import { SKIPPED, computeStackTrace, objectEntries, addTelemetryError, HookNames } from '@datadog/browser-core' +import type { Hooks, DefaultRumEventAttributes } from '../hooks' + +interface BrowserWindow { + DD_SOURCE_CODE_CONTEXT?: { [stack: string]: Context } +} +export function startSourceCodeContext(hooks: Hooks) { + const browserWindow = window as BrowserWindow + browserWindow.DD_SOURCE_CODE_CONTEXT = browserWindow.DD_SOURCE_CODE_CONTEXT || {} + const contextByFile = new Map() + + objectEntries(browserWindow.DD_SOURCE_CODE_CONTEXT).forEach(([stack, context]) => { + const stackTrace = computeStackTrace({ stack }) + const firstFrame = stackTrace.stack[0] + if (firstFrame.url) { + contextByFile.set(firstFrame.url, context) + } else { + addTelemetryError('Source code context: missing frame url', { stack }) + } + }) + + // TODO: allow late global variable update to be taken into account + + hooks.register(HookNames.Assemble, ({ domainContext, rawRumEvent }) => { + let stack + if ('handling_stack' in domainContext) { + stack = domainContext.handling_stack + } + if (rawRumEvent.type === 'error' && 'stack' in rawRumEvent.error) { + stack = rawRumEvent.error.stack + } + if (!stack) { + return SKIPPED + } + const stackTrace = computeStackTrace({ stack }) + const firstFrame = stackTrace.stack[0] + if (firstFrame.url) { + const context = contextByFile.get(firstFrame.url) + if (context) { + return context as DefaultRumEventAttributes + } + } + return SKIPPED + }) +} diff --git a/packages/rum-core/src/domain/hooks.ts b/packages/rum-core/src/domain/hooks.ts index 4f43d66b6a..2d70d49b9f 100644 --- a/packages/rum-core/src/domain/hooks.ts +++ b/packages/rum-core/src/domain/hooks.ts @@ -9,6 +9,8 @@ import type { } from '@datadog/browser-core' import { abstractHooks } from '@datadog/browser-core' import type { RumEvent } from '../rumEvent.types' +import type { RawRumEvent } from '../rawRumEvent.types' +import type { RumEventDomainContext } from '../domainContext.types' // Define a partial RUM event type. // Ensuring the `type` field is always present improves type checking, especially in conditional logic in hooks (e.g., `if (eventType === 'view')`). @@ -18,6 +20,8 @@ export type DefaultTelemetryEventAttributes = RecursivePartial export interface HookCallbackMap { [HookNamesAsConst.ASSEMBLE]: (param: { eventType: RumEvent['type'] + rawRumEvent: RawRumEvent + domainContext: RumEventDomainContext startTime: RelativeTime duration?: Duration | undefined }) => DefaultRumEventAttributes | SKIPPED | DISCARDED