Skip to content

Commit 4736888

Browse files
committed
Handle LOAf and add e2e tests
1 parent c86c238 commit 4736888

File tree

4 files changed

+204
-30
lines changed

4 files changed

+204
-30
lines changed

packages/rum-core/src/domain/contexts/sourceCodeContext.spec.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { ExperimentalFeature, HookNames } from '@datadog/browser-core'
22
import type { RelativeTime } from '@datadog/browser-core'
3-
import type { Hooks } from '../hooks'
3+
import type { AssembleHookParams, Hooks } from '../hooks'
44
import { createHooks } from '../hooks'
55
import { mockExperimentalFeatures, registerCleanupTask } from '../../../../core/test'
6+
import type { RawRumLongAnimationFrameEvent } from '../../rawRumEvent.types'
67
import type { BrowserWindow } from './sourceCodeContext'
78
import { startSourceCodeContext } from './sourceCodeContext'
89

@@ -49,8 +50,8 @@ describe('sourceCodeContext', () => {
4950
error: {
5051
stack: MATCHING_TEST_STACK,
5152
},
52-
} as any,
53-
})
53+
},
54+
} as AssembleHookParams)
5455

5556
expect(result).toBeUndefined()
5657
})
@@ -74,8 +75,8 @@ describe('sourceCodeContext', () => {
7475
error: {
7576
stack: MATCHING_TEST_STACK,
7677
},
77-
} as any,
78-
})
78+
},
79+
} as AssembleHookParams)
7980

8081
expect(result).toEqual({
8182
type: 'error',
@@ -93,11 +94,11 @@ describe('sourceCodeContext', () => {
9394
startTime: 0 as RelativeTime,
9495
rawRumEvent: {
9596
type: 'action',
96-
} as any,
97+
},
9798
domainContext: {
98-
handling_stack: MATCHING_TEST_STACK,
99+
handlingStack: MATCHING_TEST_STACK,
99100
},
100-
})
101+
} as AssembleHookParams)
101102

102103
expect(result).toEqual({
103104
type: 'action',
@@ -106,6 +107,34 @@ describe('sourceCodeContext', () => {
106107
})
107108
})
108109

110+
it('should add source code context matching the LoAF first script source URL', () => {
111+
setupBrowserWindowWithContext()
112+
startSourceCodeContext(hooks)
113+
114+
const result = hooks.triggerHook(HookNames.Assemble, {
115+
eventType: 'long_task',
116+
startTime: 0 as RelativeTime,
117+
domainContext: {},
118+
rawRumEvent: {
119+
type: 'long_task',
120+
long_task: {
121+
entry_type: 'long-animation-frame',
122+
scripts: [
123+
{
124+
source_url: 'http://localhost:8080/file.js',
125+
},
126+
],
127+
},
128+
} as RawRumLongAnimationFrameEvent,
129+
} as AssembleHookParams)
130+
131+
expect(result).toEqual({
132+
type: 'long_task',
133+
service: 'my-service',
134+
version: '1.0.0',
135+
})
136+
})
137+
109138
it('should not add source code context matching no stack', () => {
110139
setupBrowserWindowWithContext()
111140
startSourceCodeContext(hooks)
@@ -120,8 +149,8 @@ describe('sourceCodeContext', () => {
120149
stack: `Error: Another error
121150
at anotherFunction (http://localhost:8080/another-file.js:41:27)`,
122151
},
123-
} as any,
124-
})
152+
},
153+
} as AssembleHookParams)
125154

126155
expect(result).toBeUndefined()
127156
})
@@ -141,8 +170,8 @@ describe('sourceCodeContext', () => {
141170
error: {
142171
stack: TEST_STACK,
143172
},
144-
} as any,
145-
})
173+
},
174+
} as AssembleHookParams)
146175

147176
expect(result).toEqual({
148177
type: 'error',
@@ -170,8 +199,8 @@ describe('sourceCodeContext', () => {
170199
error: {
171200
stack: TEST_STACK,
172201
},
173-
} as any,
174-
})
202+
},
203+
} as AssembleHookParams)
175204

176205
expect(result).toEqual({
177206
type: 'error',

packages/rum-core/src/domain/contexts/sourceCodeContext.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
isExperimentalFeatureEnabled,
88
ExperimentalFeature,
99
} from '@datadog/browser-core'
10-
import type { Hooks, DefaultRumEventAttributes } from '../hooks'
10+
import type { Hooks, DefaultRumEventAttributes, AssembleHookParams } from '../hooks'
1111

1212
interface SourceCodeContext {
1313
service: string
@@ -53,20 +53,11 @@ export function startSourceCodeContext(hooks: Hooks) {
5353

5454
hooks.register(HookNames.Assemble, ({ domainContext, rawRumEvent }): DefaultRumEventAttributes | SKIPPED => {
5555
buildContextByFile()
56-
let stack
57-
if ('handling_stack' in domainContext) {
58-
stack = domainContext.handling_stack
59-
}
60-
if (rawRumEvent.type === 'error' && 'stack' in rawRumEvent.error) {
61-
stack = rawRumEvent.error.stack
62-
}
63-
if (!stack) {
64-
return SKIPPED
65-
}
66-
const stackTrace = computeStackTrace({ stack })
67-
const firstFrame = stackTrace.stack[0]
68-
if (firstFrame.url) {
69-
const context = contextByFile.get(firstFrame.url)
56+
57+
const url = getSourceUrl(domainContext, rawRumEvent)
58+
59+
if (url) {
60+
const context = contextByFile.get(url)
7061
if (context) {
7162
return {
7263
type: rawRumEvent.type,
@@ -78,3 +69,24 @@ export function startSourceCodeContext(hooks: Hooks) {
7869
return SKIPPED
7970
})
8071
}
72+
73+
function getSourceUrl(
74+
domainContext: AssembleHookParams['domainContext'],
75+
rawRumEvent: AssembleHookParams['rawRumEvent']
76+
) {
77+
if (rawRumEvent.type === 'long_task' && rawRumEvent.long_task.entry_type === 'long-animation-frame') {
78+
return rawRumEvent.long_task.scripts[0]?.source_url
79+
}
80+
81+
let stack
82+
if ('handlingStack' in domainContext) {
83+
stack = domainContext.handlingStack
84+
}
85+
86+
if (rawRumEvent.type === 'error' && rawRumEvent.error.stack) {
87+
stack = rawRumEvent.error.stack
88+
}
89+
const stackTrace = computeStackTrace({ stack })
90+
91+
return stackTrace.stack[0]?.url
92+
}

test/e2e/lib/types/global.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ declare global {
55
interface Window {
66
DD_LOGS?: LogsGlobal
77
DD_RUM?: RumGlobal
8+
DD_SOURCE_CODE_CONTEXT?: { [stack: string]: { service: string; version?: string } }
89
}
910
}

test/e2e/scenario/microfrontend.scenario.ts

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type { RumEvent, RumEventDomainContext, RumInitConfiguration } from '@datadog/browser-rum-core'
22
import type { LogsEvent, LogsInitConfiguration, LogsEventDomainContext } from '@datadog/browser-logs'
3+
import type { Page } from '@playwright/test'
34
import { test, expect } from '@playwright/test'
4-
import { createTest } from '../lib/framework'
5+
import { ExperimentalFeature } from '@datadog/browser-core'
6+
import { createTest, html } from '../lib/framework'
57

68
const HANDLING_STACK_REGEX = /^HandlingStack: .*\n\s+at testHandlingStack @/
79

810
const RUM_CONFIG: Partial<RumInitConfiguration> = {
911
service: 'main-service',
1012
version: '1.0.0',
13+
enableExperimentalFeatures: [ExperimentalFeature.SOURCE_CODE_CONTEXT],
1114
beforeSend: (event: RumEvent, domainContext: RumEventDomainContext) => {
1215
if ('handlingStack' in domainContext) {
1316
event.context!.handlingStack = domainContext.handlingStack
@@ -28,6 +31,31 @@ const LOGS_CONFIG: Partial<LogsInitConfiguration> = {
2831
},
2932
}
3033

34+
// because the evaluation the script in a different context than the page, resulting in unexpected stack traces
35+
function createBody(eventGenerator: string) {
36+
return html`
37+
<button>click me</button>
38+
<script>
39+
const button = document.querySelector('button')
40+
button.addEventListener('click', function handler() {
41+
${eventGenerator}
42+
})
43+
</script>
44+
`
45+
}
46+
47+
function setSourceCodeContext(page: Page, baseUrl: string) {
48+
return page.evaluate((baseUrl) => {
49+
window.DD_SOURCE_CODE_CONTEXT = {
50+
[`Error: Test error
51+
at testFunction (${baseUrl}:41:27)`]: {
52+
service: 'mf-service',
53+
version: '0.1.0',
54+
},
55+
}
56+
}, baseUrl)
57+
}
58+
3159
test.describe('microfrontend', () => {
3260
createTest('expose handling stack for fetch requests')
3361
.withRum(RUM_CONFIG)
@@ -244,4 +272,108 @@ test.describe('microfrontend', () => {
244272
expect(viewEvent.service).toBe('mf-service')
245273
expect(viewEvent.version).toBe('0.1.0')
246274
})
275+
276+
test.describe('source code context', () => {
277+
createTest('errors from DD_RUM.addError should have service and version from source code context')
278+
.withRum(RUM_CONFIG)
279+
.withBody(createBody('window.DD_RUM.addError(new Error("foo"))'))
280+
.run(async ({ intakeRegistry, flushEvents, page, baseUrl }) => {
281+
await setSourceCodeContext(page, baseUrl)
282+
await page.locator('button').click()
283+
await flushEvents()
284+
285+
const errorEvent = intakeRegistry.rumErrorEvents[0]
286+
expect(errorEvent).toMatchObject({ service: 'mf-service', version: '0.1.0' })
287+
})
288+
289+
createTest('errors from console.error should have service and version from source code context')
290+
.withRum(RUM_CONFIG)
291+
.withBody(createBody('console.error("foo")'))
292+
.run(async ({ intakeRegistry, flushEvents, page, baseUrl, withBrowserLogs }) => {
293+
await setSourceCodeContext(page, baseUrl)
294+
await page.locator('button').click()
295+
await flushEvents()
296+
297+
const errorEvent = intakeRegistry.rumErrorEvents[0]
298+
expect(errorEvent).toMatchObject({ service: 'mf-service', version: '0.1.0' })
299+
300+
withBrowserLogs((browserLogs) => {
301+
expect(browserLogs).toHaveLength(1)
302+
})
303+
})
304+
305+
createTest('runtime errors should have service and version from source code context')
306+
.withRum(RUM_CONFIG)
307+
.withBody(createBody('throw new Error("oh snap")'))
308+
.run(async ({ intakeRegistry, flushEvents, page, baseUrl, withBrowserLogs }) => {
309+
await setSourceCodeContext(page, baseUrl)
310+
await page.locator('button').click()
311+
await flushEvents()
312+
313+
const errorEvent = intakeRegistry.rumErrorEvents[0]
314+
expect(errorEvent).toMatchObject({ service: 'mf-service', version: '0.1.0' })
315+
316+
withBrowserLogs((browserLogs) => {
317+
expect(browserLogs).toHaveLength(1)
318+
})
319+
})
320+
321+
createTest('fetch requests should have service and version from source code context')
322+
.withRum(RUM_CONFIG)
323+
.withBody(createBody('fetch("/ok").then(() => {}, () => {})'))
324+
.run(async ({ intakeRegistry, flushEvents, page, baseUrl }) => {
325+
await setSourceCodeContext(page, baseUrl)
326+
await page.locator('button').click()
327+
await flushEvents()
328+
329+
const resourceEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.type === 'fetch')!
330+
expect(resourceEvent).toMatchObject({ service: 'mf-service', version: '0.1.0' })
331+
})
332+
333+
createTest('xhr requests should have service and version from source code context')
334+
.withRum(RUM_CONFIG)
335+
.withBody(createBody("const xhr = new XMLHttpRequest(); xhr.open('GET', '/ok'); xhr.send();"))
336+
.run(async ({ intakeRegistry, flushEvents, page, baseUrl }) => {
337+
await setSourceCodeContext(page, baseUrl)
338+
await page.locator('button').click()
339+
await flushEvents()
340+
341+
const resourceEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.type === 'xhr')!
342+
expect(resourceEvent).toMatchObject({ service: 'mf-service', version: '0.1.0' })
343+
})
344+
345+
createTest('custom actions should have service and version from source code context')
346+
.withRum(RUM_CONFIG)
347+
.withBody(createBody("window.DD_RUM.addAction('foo')"))
348+
.run(async ({ intakeRegistry, flushEvents, page, baseUrl }) => {
349+
await setSourceCodeContext(page, baseUrl)
350+
await page.locator('button').click()
351+
await flushEvents()
352+
353+
const actionEvent = intakeRegistry.rumActionEvents[0]
354+
expect(actionEvent).toMatchObject({ service: 'mf-service', version: '0.1.0' })
355+
})
356+
357+
createTest('LOAf should have service and version from source code context')
358+
.withRum(RUM_CONFIG)
359+
.withBody(
360+
createBody(`
361+
const end = performance.now() + 55
362+
while (performance.now() < end) {} // block the handler for ~55ms to trigger a long task
363+
`)
364+
)
365+
.run(async ({ intakeRegistry, flushEvents, page, baseUrl, browserName }) => {
366+
test.skip(browserName !== 'chromium', 'Non-Chromium browsers do not support long tasks')
367+
368+
await setSourceCodeContext(page, baseUrl)
369+
await page.locator('button').click()
370+
await flushEvents()
371+
372+
const longTaskEvent = intakeRegistry.rumLongTaskEvents.find((event) =>
373+
event.long_task.scripts?.[0]?.invoker?.includes('BUTTON.onclick')
374+
)
375+
376+
expect(longTaskEvent).toMatchObject({ service: 'mf-service', version: '0.1.0' })
377+
})
378+
})
247379
})

0 commit comments

Comments
 (0)