Skip to content

Commit e22fd85

Browse files
authored
[RUM Profiler] send view names as event attributes (#3851)
* [RUM Profiler] send view names as event attributes * [RUM Profiler] move buildProfileEventAttributes to its own file with tests * [RUM Profiler] update buildProfileEventAttributes unit test
1 parent 6a4cb04 commit e22fd85

File tree

3 files changed

+387
-43
lines changed

3 files changed

+387
-43
lines changed
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import { clocksOrigin } from '@datadog/browser-core'
2+
import type { RumProfilerTrace, RumViewEntry, RUMProfilerLongTaskEntry } from '../types'
3+
import { buildProfileEventAttributes, type ProfileEventAttributes } from './buildProfileEventAttributes'
4+
5+
describe('buildProfileEventAttributes', () => {
6+
const applicationId = 'test-app-id'
7+
const sessionId = 'test-session-id'
8+
9+
function createMockViewEntry(overrides: Partial<RumViewEntry> = {}): RumViewEntry {
10+
return {
11+
startClocks: clocksOrigin(),
12+
viewId: 'view-123',
13+
viewName: 'Home Page',
14+
...overrides,
15+
}
16+
}
17+
18+
function createMockLongTaskEntry(overrides: Partial<RUMProfilerLongTaskEntry> = {}): RUMProfilerLongTaskEntry {
19+
return {
20+
id: 'longtask-456',
21+
duration: 100 as any,
22+
entryType: 'longtask',
23+
startClocks: clocksOrigin(),
24+
...overrides,
25+
}
26+
}
27+
28+
function createMockProfilerTrace(overrides: Partial<RumProfilerTrace> = {}): RumProfilerTrace {
29+
return {
30+
startClocks: clocksOrigin(),
31+
endClocks: clocksOrigin(),
32+
clocksOrigin: clocksOrigin(),
33+
sampleInterval: 10,
34+
views: [],
35+
longTasks: [],
36+
resources: [],
37+
frames: [],
38+
stacks: [],
39+
samples: [],
40+
...overrides,
41+
}
42+
}
43+
44+
describe('when creating basic profile event attributes', () => {
45+
it('should include application id', () => {
46+
const profilerTrace = createMockProfilerTrace()
47+
48+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
49+
50+
expect(result.application).toEqual({ id: applicationId })
51+
})
52+
53+
it('should include session id when provided', () => {
54+
const profilerTrace = createMockProfilerTrace()
55+
56+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
57+
58+
expect(result.session).toEqual({ id: sessionId })
59+
})
60+
61+
it('should omit session when sessionId is undefined', () => {
62+
const profilerTrace = createMockProfilerTrace()
63+
64+
const result = buildProfileEventAttributes(profilerTrace, applicationId, undefined)
65+
66+
expect(result.session).toBeUndefined()
67+
})
68+
69+
it('should omit session when sessionId is empty string', () => {
70+
const profilerTrace = createMockProfilerTrace()
71+
72+
const result = buildProfileEventAttributes(profilerTrace, applicationId, '')
73+
74+
expect(result.session).toBeUndefined()
75+
})
76+
})
77+
78+
describe('when handling views', () => {
79+
it('should extract view ids and names from single view', () => {
80+
const view = createMockViewEntry({
81+
viewId: 'view-123',
82+
viewName: 'Home Page',
83+
})
84+
const profilerTrace = createMockProfilerTrace({ views: [view] })
85+
86+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
87+
88+
expect(result.view).toEqual({
89+
id: ['view-123'],
90+
name: ['Home Page'],
91+
})
92+
})
93+
94+
it('should extract view ids and names from multiple views', () => {
95+
const views = [
96+
createMockViewEntry({ viewId: 'view-123', viewName: 'Home Page' }),
97+
createMockViewEntry({ viewId: 'view-456', viewName: 'About Page' }),
98+
createMockViewEntry({ viewId: 'view-789', viewName: 'Contact Page' }),
99+
]
100+
const profilerTrace = createMockProfilerTrace({ views })
101+
102+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
103+
104+
expect(result.view).toEqual({
105+
id: ['view-123', 'view-456', 'view-789'],
106+
name: ['Home Page', 'About Page', 'Contact Page'],
107+
})
108+
})
109+
110+
it('should handle views with undefined names', () => {
111+
const views = [
112+
createMockViewEntry({ viewId: 'view-123', viewName: 'Home Page' }),
113+
createMockViewEntry({ viewId: 'view-456', viewName: undefined }),
114+
createMockViewEntry({ viewId: 'view-789', viewName: 'Contact Page' }),
115+
]
116+
const profilerTrace = createMockProfilerTrace({ views })
117+
118+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
119+
120+
expect(result.view).toEqual({
121+
id: ['view-123', 'view-456', 'view-789'],
122+
name: ['Home Page', 'Contact Page'],
123+
})
124+
})
125+
126+
it('should remove duplicate view names', () => {
127+
const views = [
128+
createMockViewEntry({ viewId: 'view-123', viewName: 'Home Page' }),
129+
createMockViewEntry({ viewId: 'view-456', viewName: 'Home Page' }),
130+
createMockViewEntry({ viewId: 'view-789', viewName: 'About Page' }),
131+
createMockViewEntry({ viewId: 'view-abc', viewName: 'Home Page' }),
132+
]
133+
const profilerTrace = createMockProfilerTrace({ views })
134+
135+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
136+
137+
expect(result.view).toEqual({
138+
id: ['view-123', 'view-456', 'view-789', 'view-abc'],
139+
name: ['Home Page', 'About Page'],
140+
})
141+
})
142+
143+
it('should handle all views without names', () => {
144+
const views = [
145+
createMockViewEntry({ viewId: 'view-123', viewName: undefined }),
146+
createMockViewEntry({ viewId: 'view-456', viewName: undefined }),
147+
]
148+
const profilerTrace = createMockProfilerTrace({ views })
149+
150+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
151+
152+
expect(result.view).toEqual({
153+
id: ['view-123', 'view-456'],
154+
name: [],
155+
})
156+
})
157+
158+
it('should omit view attribute when no views are present', () => {
159+
const profilerTrace = createMockProfilerTrace({ views: [] })
160+
161+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
162+
163+
expect(result.view).toBeUndefined()
164+
})
165+
})
166+
167+
describe('when handling long tasks', () => {
168+
it('should extract long task ids from single long task', () => {
169+
const longTask = createMockLongTaskEntry({ id: 'longtask-123' })
170+
const profilerTrace = createMockProfilerTrace({ longTasks: [longTask] })
171+
172+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
173+
174+
expect(result.long_task).toEqual({
175+
id: ['longtask-123'],
176+
})
177+
})
178+
179+
it('should extract long task ids from multiple long tasks', () => {
180+
const longTasks = [
181+
createMockLongTaskEntry({ id: 'longtask-123' }),
182+
createMockLongTaskEntry({ id: 'longtask-456' }),
183+
createMockLongTaskEntry({ id: 'longtask-789' }),
184+
]
185+
const profilerTrace = createMockProfilerTrace({ longTasks })
186+
187+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
188+
189+
expect(result.long_task).toEqual({
190+
id: ['longtask-123', 'longtask-456', 'longtask-789'],
191+
})
192+
})
193+
194+
it('should filter out long tasks with undefined ids', () => {
195+
const longTasks = [
196+
createMockLongTaskEntry({ id: 'longtask-123' }),
197+
createMockLongTaskEntry({ id: undefined }),
198+
createMockLongTaskEntry({ id: 'longtask-789' }),
199+
]
200+
const profilerTrace = createMockProfilerTrace({ longTasks })
201+
202+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
203+
204+
expect(result.long_task).toEqual({
205+
id: ['longtask-123', 'longtask-789'],
206+
})
207+
})
208+
209+
it('should omit long_task attribute when no long tasks have ids', () => {
210+
const longTasks = [createMockLongTaskEntry({ id: undefined }), createMockLongTaskEntry({ id: undefined })]
211+
const profilerTrace = createMockProfilerTrace({ longTasks })
212+
213+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
214+
215+
expect(result.long_task).toBeUndefined()
216+
})
217+
218+
it('should omit long_task attribute when no long tasks are present', () => {
219+
const profilerTrace = createMockProfilerTrace({ longTasks: [] })
220+
221+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
222+
223+
expect(result.long_task).toBeUndefined()
224+
})
225+
})
226+
227+
describe('when handling complex scenarios', () => {
228+
it('should handle profiler trace with both views and long tasks', () => {
229+
const views = [
230+
createMockViewEntry({ viewId: 'view-123', viewName: 'Home Page' }),
231+
createMockViewEntry({ viewId: 'view-456', viewName: 'About Page' }),
232+
]
233+
const longTasks = [
234+
createMockLongTaskEntry({ id: 'longtask-123' }),
235+
createMockLongTaskEntry({ id: 'longtask-456' }),
236+
]
237+
const profilerTrace = createMockProfilerTrace({ views, longTasks })
238+
239+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
240+
241+
const expected: ProfileEventAttributes = {
242+
application: { id: applicationId },
243+
session: { id: sessionId },
244+
view: {
245+
id: ['view-123', 'view-456'],
246+
name: ['Home Page', 'About Page'],
247+
},
248+
long_task: {
249+
id: ['longtask-123', 'longtask-456'],
250+
},
251+
}
252+
253+
expect(result).toEqual(expected)
254+
})
255+
256+
it('should handle empty profiler trace', () => {
257+
const profilerTrace = createMockProfilerTrace()
258+
259+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
260+
261+
const expected: ProfileEventAttributes = {
262+
application: { id: applicationId },
263+
session: { id: sessionId },
264+
}
265+
266+
expect(result).toEqual(expected)
267+
})
268+
269+
it('should handle profiler trace with empty string view names consistently', () => {
270+
const views = [
271+
createMockViewEntry({ viewId: 'view-123', viewName: '' }), // will be ignored
272+
createMockViewEntry({ viewId: 'view-456', viewName: 'Valid Page' }),
273+
]
274+
const profilerTrace = createMockProfilerTrace({ views })
275+
276+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
277+
278+
expect(result.view).toEqual({
279+
id: ['view-123', 'view-456'],
280+
name: ['Valid Page'],
281+
})
282+
})
283+
})
284+
285+
describe('edge cases', () => {
286+
it('should handle profiler trace with duplicate view names and mixed undefined names', () => {
287+
const views = [
288+
createMockViewEntry({ viewId: 'view-1', viewName: 'Page A' }),
289+
createMockViewEntry({ viewId: 'view-2', viewName: undefined }),
290+
createMockViewEntry({ viewId: 'view-3', viewName: 'Page A' }),
291+
createMockViewEntry({ viewId: 'view-4', viewName: 'Page B' }),
292+
createMockViewEntry({ viewId: 'view-5', viewName: undefined }),
293+
createMockViewEntry({ viewId: 'view-6', viewName: 'Page A' }),
294+
]
295+
const profilerTrace = createMockProfilerTrace({ views })
296+
297+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
298+
299+
expect(result.view).toEqual({
300+
id: ['view-1', 'view-2', 'view-3', 'view-4', 'view-5', 'view-6'],
301+
name: ['Page A', 'Page B'], // Duplicates removed
302+
})
303+
})
304+
305+
it('should preserve order of view ids but deduplicate names', () => {
306+
const views = [
307+
createMockViewEntry({ viewId: 'view-last', viewName: 'Page Z' }),
308+
createMockViewEntry({ viewId: 'view-first', viewName: 'Page A' }),
309+
createMockViewEntry({ viewId: 'view-middle', viewName: 'Page Z' }), // Duplicate name
310+
]
311+
const profilerTrace = createMockProfilerTrace({ views })
312+
313+
const result = buildProfileEventAttributes(profilerTrace, applicationId, sessionId)
314+
315+
expect(result.view?.id).toEqual(['view-last', 'view-first', 'view-middle'])
316+
expect(result.view?.name).toEqual(['Page Z', 'Page A']) // Order based on first occurrence
317+
})
318+
})
319+
})
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { RumProfilerTrace, RumViewEntry } from '../types'
2+
3+
export interface ProfileEventAttributes {
4+
application: { id: string }
5+
session?: { id: string }
6+
view?: { id: string[]; name: string[] }
7+
long_task?: { id: string[] }
8+
}
9+
10+
/**
11+
* Builds attributes for the Profile Event.
12+
*
13+
* @param profilerTrace - Profiler trace
14+
* @param applicationId - application id.
15+
* @param sessionId - session id.
16+
* @returns Additional attributes.
17+
*/
18+
export function buildProfileEventAttributes(
19+
profilerTrace: RumProfilerTrace,
20+
applicationId: string,
21+
sessionId: string | undefined
22+
): ProfileEventAttributes {
23+
const attributes: ProfileEventAttributes = {
24+
application: {
25+
id: applicationId,
26+
},
27+
}
28+
if (sessionId) {
29+
attributes.session = {
30+
id: sessionId,
31+
}
32+
}
33+
34+
// Extract view ids and names from the profiler trace and add them as attributes of the profile event.
35+
// This will be used to filter the profiles by @view.id and/or @view.name.
36+
const { ids, names } = extractViewIdsAndNames(profilerTrace.views)
37+
38+
if (ids.length) {
39+
attributes.view = {
40+
id: ids,
41+
name: names,
42+
}
43+
}
44+
const longTaskIds: string[] = profilerTrace.longTasks.map((longTask) => longTask.id).filter((id) => id !== undefined)
45+
46+
if (longTaskIds.length) {
47+
attributes.long_task = { id: longTaskIds }
48+
}
49+
return attributes
50+
}
51+
52+
function extractViewIdsAndNames(views: RumViewEntry[]): { ids: string[]; names: string[] } {
53+
const result: { ids: string[]; names: string[] } = { ids: [], names: [] }
54+
for (const view of views) {
55+
result.ids.push(view.viewId)
56+
57+
if (view.viewName) {
58+
result.names.push(view.viewName)
59+
}
60+
}
61+
62+
// Remove duplicates
63+
result.names = Array.from(new Set(result.names))
64+
65+
return result
66+
}

0 commit comments

Comments
 (0)