From 4293f772c1e6713a0345817bfd58e24e3888bc9a Mon Sep 17 00:00:00 2001 From: Cynthia Ma Date: Thu, 6 Feb 2025 11:09:09 -0800 Subject: [PATCH] feat: round values from RUM report also tracer to main.ts --- src/main.ts | 1 + src/v3/convertToRum.test.ts | 92 +++++++++++++++++++++++++ src/v3/convertToRum.ts | 33 ++++++++- src/v3/recordingComputeUtils.test.ts | 69 ++----------------- src/v3/testUtility/createMockFactory.ts | 65 +++++++++++++++++ 5 files changed, 195 insertions(+), 65 deletions(-) create mode 100644 src/v3/convertToRum.test.ts create mode 100644 src/v3/testUtility/createMockFactory.ts diff --git a/src/main.ts b/src/main.ts index 439b5812..f7de90e5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -56,6 +56,7 @@ export * from './v3/getDynamicQuietWindowDuration' export * from './v3/getSpanFromPerformanceEntry' export * from './v3/hooks' export type * from './v3/hooksTypes' +export * from './v3/tracer' // eslint-disable-next-line import/first, import/newline-after-import import * as match from './v3/matchSpan' export { match } diff --git a/src/v3/convertToRum.test.ts b/src/v3/convertToRum.test.ts new file mode 100644 index 00000000..88883405 --- /dev/null +++ b/src/v3/convertToRum.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest' +import { convertTraceToRUM } from './convertToRum' +import { createTraceRecording } from './recordingComputeUtils' +import { ActiveTraceInput } from './spanTypes' +import { + AnyScope, + createMockSpanAndAnnotation, + createTimestamp, +} from './testUtility/createMockFactory' +import type { CompleteTraceDefinition } from './types' + +describe('convertTraceToRUM', () => { + it('should round all numeric values in the trace recording', () => { + const definition: CompleteTraceDefinition = { + name: 'test-trace', + scopes: [], + requiredSpans: [() => true], + computedSpanDefinitions: [], + computedValueDefinitions: [], + variantsByOriginatedFrom: { + origin: { timeoutDuration: 45_000 }, + }, + } + + const input: ActiveTraceInput<{}, 'origin'> = { + id: 'test', + startTime: createTimestamp(0), + scope: {}, + originatedFrom: 'origin', + } + + const traceRecording = createTraceRecording( + { + definition, + recordedItems: [ + createMockSpanAndAnnotation(100.501, { + name: 'test-component', + type: 'component-render', + scope: {}, + duration: 50.499, + isIdle: false, + renderCount: 1, + renderedOutput: 'loading', + }), + createMockSpanAndAnnotation( + 200.001, + { + name: 'test-component', + type: 'component-render', + scope: {}, + duration: 50.999, + isIdle: true, + renderCount: 2, + renderedOutput: 'content', + }, + { occurrence: 2 }, + ), + ], + input: { + id: 'test', + startTime: createTimestamp(0), + scope: { ticketId: '74' }, + originatedFrom: 'origin', + }, + }, + { transitionFromState: 'active' }, + ) + + const context = { + definition, + input, + } + + const result = convertTraceToRUM(traceRecording, context) + + // Check rounded values in embeddedSpans + const embeddedSpan = result.embeddedSpans['component-render|test-component'] + if (embeddedSpan) { + expect(Number.isInteger(embeddedSpan.totalDuration)).toBe(true) + expect(Number.isInteger(embeddedSpan.spans[0]!.startOffset)).toBe(true) + expect(Number.isInteger(embeddedSpan.spans[0]!.duration)).toBe(true) + expect(Number.isInteger(embeddedSpan.spans[1]!.startOffset)).toBe(true) + expect(Number.isInteger(embeddedSpan.spans[1]!.duration)).toBe(true) + + // Check specific rounded values + expect(embeddedSpan.spans[0]!.startOffset).toBe(101) // 100.501 rounded + expect(embeddedSpan.spans[0]!.duration).toBe(50) // 50.499 rounded + expect(embeddedSpan.spans[1]!.startOffset).toBe(200) // 200.001 rounded + expect(embeddedSpan.spans[1]!.duration).toBe(51) // 50.999 rounded + } + }) +}) diff --git a/src/v3/convertToRum.ts b/src/v3/convertToRum.ts index c959d219..3503a8f5 100644 --- a/src/v3/convertToRum.ts +++ b/src/v3/convertToRum.ts @@ -122,6 +122,33 @@ export function getSpanSummaryAttributes( return spanAttributes } +type RoundFunction = (x: number) => number + +function recursivelyRoundValues( + obj: T, + roundFunc: RoundFunction = (x) => Math.round(x), +): T { + const result: Record = {} + + for (const [key, value] of Object.entries(obj as object)) { + if (typeof value === 'number') { + result[key] = roundFunc(value) + } else if (Array.isArray(value)) { + result[key] = value.map((item: number | T) => + typeof item === 'number' + ? roundFunc(item) + : recursivelyRoundValues(item, roundFunc), + ) + } else if (value && typeof value === 'object') { + result[key] = recursivelyRoundValues(value, roundFunc) + } else { + result[key] = value + } + } + + return result as T +} + export function convertTraceToRUM< TracerScopeKeysT extends KeysOfUnion, AllPossibleScopesT, @@ -172,10 +199,14 @@ export function convertTraceToRUM< } } - return { + const result: RumTraceRecording< + SelectScopeByKey + > = { ...otherTraceRecordingAttributes, embeddedSpans: Object.fromEntries(embeddedSpans), nonEmbeddedSpans: [...nonEmbeddedSpans], spanAttributes, } + + return recursivelyRoundValues(result) } diff --git a/src/v3/recordingComputeUtils.test.ts b/src/v3/recordingComputeUtils.test.ts index a1463d31..691c7a19 100644 --- a/src/v3/recordingComputeUtils.test.ts +++ b/src/v3/recordingComputeUtils.test.ts @@ -5,74 +5,15 @@ import { getComputedSpans, getComputedValues, } from './recordingComputeUtils' -import type { SpanAndAnnotation, SpanAnnotation } from './spanAnnotationTypes' -import type { Span } from './spanTypes' -import type { CompleteTraceDefinition, Timestamp } from './types' +import { + createMockSpanAndAnnotation, + createTimestamp, +} from './testUtility/createMockFactory' +import type { CompleteTraceDefinition } from './types' type AnyScope = Record describe('recordingComputeUtils', () => { - const EPOCH_START = 1_000 - const createTimestamp = (now: number): Timestamp => ({ - epoch: EPOCH_START + now, - now, - }) - - const createAnnotation = ( - span: Span, - traceStartTime: Timestamp, - partial: Partial = {}, - ): SpanAnnotation => ({ - id: 'test-id', - operationRelativeStartTime: span.startTime.now - traceStartTime.now, - operationRelativeEndTime: - span.startTime.now + span.duration - traceStartTime.now, - occurrence: 1, - recordedInState: 'active', - labels: [], - ...partial, - }) - - const createMockSpan = >( - startTimeNow: number, - partial: Partial, - ): TSpan => - (partial.type === 'component-render' - ? { - name: 'test-component', - type: 'component-render', - scope: {}, - startTime: createTimestamp(startTimeNow), - duration: 100, - attributes: {}, - isIdle: true, - renderCount: 1, - renderedOutput: 'content', - ...partial, - } - : { - name: 'test-span', - type: 'mark', - startTime: createTimestamp(startTimeNow), - duration: 100, - attributes: {}, - ...partial, - }) as TSpan - - const createMockSpanAndAnnotation = >( - startTimeNow: number, - spanPartial: Partial = {}, - annotationPartial: Partial = {}, - ): SpanAndAnnotation => { - const span = createMockSpan(startTimeNow, spanPartial) - return { - span, - annotation: createAnnotation(span, createTimestamp(0), annotationPartial), - } - } - - const onEnd = jest.fn() - describe('error status propagation', () => { const baseDefinition: CompleteTraceDefinition = { name: 'test-trace', diff --git a/src/v3/testUtility/createMockFactory.ts b/src/v3/testUtility/createMockFactory.ts new file mode 100644 index 00000000..9cb813db --- /dev/null +++ b/src/v3/testUtility/createMockFactory.ts @@ -0,0 +1,65 @@ +import { SpanAndAnnotation, SpanAnnotation } from '../spanAnnotationTypes' +import { Span } from '../spanTypes' +import type { Timestamp } from '../types' + +const EPOCH_START = 1_000 + +export const createTimestamp = (now: number): Timestamp => ({ + epoch: EPOCH_START + now, + now, +}) + +export type AnyScope = Record + +export const createAnnotation = ( + span: Span, + traceStartTime: Timestamp, + partial: Partial = {}, +): SpanAnnotation => ({ + id: 'test-id', + operationRelativeStartTime: span.startTime.now - traceStartTime.now, + operationRelativeEndTime: + span.startTime.now + span.duration - traceStartTime.now, + occurrence: 1, + recordedInState: 'active', + labels: [], + ...partial, +}) + +export const createMockSpan = >( + startTimeNow: number, + partial: Partial, +): TSpan => + (partial.type === 'component-render' + ? { + name: 'test-component', + type: 'component-render', + scope: {}, + startTime: createTimestamp(startTimeNow), + duration: 100, + attributes: {}, + isIdle: true, + renderCount: 1, + renderedOutput: 'content', + ...partial, + } + : { + name: 'test-span', + type: 'mark', + startTime: createTimestamp(startTimeNow), + duration: 100, + attributes: {}, + ...partial, + }) as TSpan + +export const createMockSpanAndAnnotation = >( + startTimeNow: number, + spanPartial: Partial = {}, + annotationPartial: Partial = {}, +): SpanAndAnnotation => { + const span = createMockSpan(startTimeNow, spanPartial) + return { + span, + annotation: createAnnotation(span, createTimestamp(0), annotationPartial), + } +}