Skip to content

Commit 4293f77

Browse files
committed
feat: round values from RUM report
also tracer to main.ts
1 parent 6b14442 commit 4293f77

File tree

5 files changed

+195
-65
lines changed

5 files changed

+195
-65
lines changed

src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export * from './v3/getDynamicQuietWindowDuration'
5656
export * from './v3/getSpanFromPerformanceEntry'
5757
export * from './v3/hooks'
5858
export type * from './v3/hooksTypes'
59+
export * from './v3/tracer'
5960
// eslint-disable-next-line import/first, import/newline-after-import
6061
import * as match from './v3/matchSpan'
6162
export { match }

src/v3/convertToRum.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { convertTraceToRUM } from './convertToRum'
3+
import { createTraceRecording } from './recordingComputeUtils'
4+
import { ActiveTraceInput } from './spanTypes'
5+
import {
6+
AnyScope,
7+
createMockSpanAndAnnotation,
8+
createTimestamp,
9+
} from './testUtility/createMockFactory'
10+
import type { CompleteTraceDefinition } from './types'
11+
12+
describe('convertTraceToRUM', () => {
13+
it('should round all numeric values in the trace recording', () => {
14+
const definition: CompleteTraceDefinition<never, AnyScope, 'origin'> = {
15+
name: 'test-trace',
16+
scopes: [],
17+
requiredSpans: [() => true],
18+
computedSpanDefinitions: [],
19+
computedValueDefinitions: [],
20+
variantsByOriginatedFrom: {
21+
origin: { timeoutDuration: 45_000 },
22+
},
23+
}
24+
25+
const input: ActiveTraceInput<{}, 'origin'> = {
26+
id: 'test',
27+
startTime: createTimestamp(0),
28+
scope: {},
29+
originatedFrom: 'origin',
30+
}
31+
32+
const traceRecording = createTraceRecording(
33+
{
34+
definition,
35+
recordedItems: [
36+
createMockSpanAndAnnotation(100.501, {
37+
name: 'test-component',
38+
type: 'component-render',
39+
scope: {},
40+
duration: 50.499,
41+
isIdle: false,
42+
renderCount: 1,
43+
renderedOutput: 'loading',
44+
}),
45+
createMockSpanAndAnnotation(
46+
200.001,
47+
{
48+
name: 'test-component',
49+
type: 'component-render',
50+
scope: {},
51+
duration: 50.999,
52+
isIdle: true,
53+
renderCount: 2,
54+
renderedOutput: 'content',
55+
},
56+
{ occurrence: 2 },
57+
),
58+
],
59+
input: {
60+
id: 'test',
61+
startTime: createTimestamp(0),
62+
scope: { ticketId: '74' },
63+
originatedFrom: 'origin',
64+
},
65+
},
66+
{ transitionFromState: 'active' },
67+
)
68+
69+
const context = {
70+
definition,
71+
input,
72+
}
73+
74+
const result = convertTraceToRUM(traceRecording, context)
75+
76+
// Check rounded values in embeddedSpans
77+
const embeddedSpan = result.embeddedSpans['component-render|test-component']
78+
if (embeddedSpan) {
79+
expect(Number.isInteger(embeddedSpan.totalDuration)).toBe(true)
80+
expect(Number.isInteger(embeddedSpan.spans[0]!.startOffset)).toBe(true)
81+
expect(Number.isInteger(embeddedSpan.spans[0]!.duration)).toBe(true)
82+
expect(Number.isInteger(embeddedSpan.spans[1]!.startOffset)).toBe(true)
83+
expect(Number.isInteger(embeddedSpan.spans[1]!.duration)).toBe(true)
84+
85+
// Check specific rounded values
86+
expect(embeddedSpan.spans[0]!.startOffset).toBe(101) // 100.501 rounded
87+
expect(embeddedSpan.spans[0]!.duration).toBe(50) // 50.499 rounded
88+
expect(embeddedSpan.spans[1]!.startOffset).toBe(200) // 200.001 rounded
89+
expect(embeddedSpan.spans[1]!.duration).toBe(51) // 50.999 rounded
90+
}
91+
})
92+
})

src/v3/convertToRum.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,33 @@ export function getSpanSummaryAttributes<AllPossibleScopesT>(
122122
return spanAttributes
123123
}
124124

125+
type RoundFunction = (x: number) => number
126+
127+
function recursivelyRoundValues<T extends object>(
128+
obj: T,
129+
roundFunc: RoundFunction = (x) => Math.round(x),
130+
): T {
131+
const result: Record<string, unknown> = {}
132+
133+
for (const [key, value] of Object.entries(obj as object)) {
134+
if (typeof value === 'number') {
135+
result[key] = roundFunc(value)
136+
} else if (Array.isArray(value)) {
137+
result[key] = value.map((item: number | T) =>
138+
typeof item === 'number'
139+
? roundFunc(item)
140+
: recursivelyRoundValues(item, roundFunc),
141+
)
142+
} else if (value && typeof value === 'object') {
143+
result[key] = recursivelyRoundValues(value, roundFunc)
144+
} else {
145+
result[key] = value
146+
}
147+
}
148+
149+
return result as T
150+
}
151+
125152
export function convertTraceToRUM<
126153
TracerScopeKeysT extends KeysOfUnion<AllPossibleScopesT>,
127154
AllPossibleScopesT,
@@ -172,10 +199,14 @@ export function convertTraceToRUM<
172199
}
173200
}
174201

175-
return {
202+
const result: RumTraceRecording<
203+
SelectScopeByKey<TracerScopeKeysT, AllPossibleScopesT>
204+
> = {
176205
...otherTraceRecordingAttributes,
177206
embeddedSpans: Object.fromEntries(embeddedSpans),
178207
nonEmbeddedSpans: [...nonEmbeddedSpans],
179208
spanAttributes,
180209
}
210+
211+
return recursivelyRoundValues(result)
181212
}

src/v3/recordingComputeUtils.test.ts

Lines changed: 5 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,74 +5,15 @@ import {
55
getComputedSpans,
66
getComputedValues,
77
} from './recordingComputeUtils'
8-
import type { SpanAndAnnotation, SpanAnnotation } from './spanAnnotationTypes'
9-
import type { Span } from './spanTypes'
10-
import type { CompleteTraceDefinition, Timestamp } from './types'
8+
import {
9+
createMockSpanAndAnnotation,
10+
createTimestamp,
11+
} from './testUtility/createMockFactory'
12+
import type { CompleteTraceDefinition } from './types'
1113

1214
type AnyScope = Record<string, unknown>
1315

1416
describe('recordingComputeUtils', () => {
15-
const EPOCH_START = 1_000
16-
const createTimestamp = (now: number): Timestamp => ({
17-
epoch: EPOCH_START + now,
18-
now,
19-
})
20-
21-
const createAnnotation = (
22-
span: Span<AnyScope>,
23-
traceStartTime: Timestamp,
24-
partial: Partial<SpanAnnotation> = {},
25-
): SpanAnnotation => ({
26-
id: 'test-id',
27-
operationRelativeStartTime: span.startTime.now - traceStartTime.now,
28-
operationRelativeEndTime:
29-
span.startTime.now + span.duration - traceStartTime.now,
30-
occurrence: 1,
31-
recordedInState: 'active',
32-
labels: [],
33-
...partial,
34-
})
35-
36-
const createMockSpan = <TSpan extends Span<AnyScope>>(
37-
startTimeNow: number,
38-
partial: Partial<TSpan>,
39-
): TSpan =>
40-
(partial.type === 'component-render'
41-
? {
42-
name: 'test-component',
43-
type: 'component-render',
44-
scope: {},
45-
startTime: createTimestamp(startTimeNow),
46-
duration: 100,
47-
attributes: {},
48-
isIdle: true,
49-
renderCount: 1,
50-
renderedOutput: 'content',
51-
...partial,
52-
}
53-
: {
54-
name: 'test-span',
55-
type: 'mark',
56-
startTime: createTimestamp(startTimeNow),
57-
duration: 100,
58-
attributes: {},
59-
...partial,
60-
}) as TSpan
61-
62-
const createMockSpanAndAnnotation = <TSpan extends Span<AnyScope>>(
63-
startTimeNow: number,
64-
spanPartial: Partial<TSpan> = {},
65-
annotationPartial: Partial<SpanAnnotation> = {},
66-
): SpanAndAnnotation<AnyScope> => {
67-
const span = createMockSpan<TSpan>(startTimeNow, spanPartial)
68-
return {
69-
span,
70-
annotation: createAnnotation(span, createTimestamp(0), annotationPartial),
71-
}
72-
}
73-
74-
const onEnd = jest.fn()
75-
7617
describe('error status propagation', () => {
7718
const baseDefinition: CompleteTraceDefinition<never, AnyScope, 'origin'> = {
7819
name: 'test-trace',
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { SpanAndAnnotation, SpanAnnotation } from '../spanAnnotationTypes'
2+
import { Span } from '../spanTypes'
3+
import type { Timestamp } from '../types'
4+
5+
const EPOCH_START = 1_000
6+
7+
export const createTimestamp = (now: number): Timestamp => ({
8+
epoch: EPOCH_START + now,
9+
now,
10+
})
11+
12+
export type AnyScope = Record<string, unknown>
13+
14+
export const createAnnotation = (
15+
span: Span<AnyScope>,
16+
traceStartTime: Timestamp,
17+
partial: Partial<SpanAnnotation> = {},
18+
): SpanAnnotation => ({
19+
id: 'test-id',
20+
operationRelativeStartTime: span.startTime.now - traceStartTime.now,
21+
operationRelativeEndTime:
22+
span.startTime.now + span.duration - traceStartTime.now,
23+
occurrence: 1,
24+
recordedInState: 'active',
25+
labels: [],
26+
...partial,
27+
})
28+
29+
export const createMockSpan = <TSpan extends Span<AnyScope>>(
30+
startTimeNow: number,
31+
partial: Partial<TSpan>,
32+
): TSpan =>
33+
(partial.type === 'component-render'
34+
? {
35+
name: 'test-component',
36+
type: 'component-render',
37+
scope: {},
38+
startTime: createTimestamp(startTimeNow),
39+
duration: 100,
40+
attributes: {},
41+
isIdle: true,
42+
renderCount: 1,
43+
renderedOutput: 'content',
44+
...partial,
45+
}
46+
: {
47+
name: 'test-span',
48+
type: 'mark',
49+
startTime: createTimestamp(startTimeNow),
50+
duration: 100,
51+
attributes: {},
52+
...partial,
53+
}) as TSpan
54+
55+
export const createMockSpanAndAnnotation = <TSpan extends Span<AnyScope>>(
56+
startTimeNow: number,
57+
spanPartial: Partial<TSpan> = {},
58+
annotationPartial: Partial<SpanAnnotation> = {},
59+
): SpanAndAnnotation<AnyScope> => {
60+
const span = createMockSpan<TSpan>(startTimeNow, spanPartial)
61+
return {
62+
span,
63+
annotation: createAnnotation(span, createTimestamp(0), annotationPartial),
64+
}
65+
}

0 commit comments

Comments
 (0)