UNPKG

@zendesk/react-measure-timing-hooks

Version:

react hooks for measuring time to interactive and time to render of components

416 lines (386 loc) 12.1 kB
import { describe, expect, it, vitest as jest } from 'vitest' import { getSpanSummaryAttributes } from './convertToRum' import { createTraceRecording, getComputedSpans, getComputedValues, } from './recordingComputeUtils' import { createMockSpanAndAnnotation, createTimestamp, } from './testUtility/createMockFactory' import type { CompleteTraceDefinition } from './types' interface AnyRelation { global: {} } const baseDefinitionFixture: CompleteTraceDefinition< 'global', AnyRelation, 'origin' > = { name: 'test-trace', relationSchemaName: 'global', relationSchema: { global: {} }, requiredSpans: [() => true], computedSpanDefinitions: {}, computedValueDefinitions: {}, variants: { origin: { timeout: 45_000 }, }, } describe('recordingComputeUtils', () => { describe('error status propagation', () => { it('should mark trace as error if any non-suppressed span has error status', () => { const recording = createTraceRecording( { definition: baseDefinitionFixture, recordedItems: new Set([ createMockSpanAndAnnotation(100, { status: 'error', name: 'error-span', }), createMockSpanAndAnnotation(200, { status: 'ok', name: 'ok-span' }), ]), input: { id: 'test', startTime: createTimestamp(0), relatedTo: {}, variant: 'origin', }, recordedItemsByLabel: {}, }, { transitionFromState: 'active' }, ) expect(recording.status).toBe('error') expect(recording.additionalDurations.startTillInteractive).toBeNull() expect(recording.additionalDurations.completeTillInteractive).toBeNull() expect(recording.additionalDurations.startTillRequirementsMet).toBeNull() }) it('should not mark trace as error if all error spans are suppressed', () => { const recording = createTraceRecording( { definition: { ...baseDefinitionFixture, suppressErrorStatusPropagationOnSpans: [ ({ span }) => span.name === 'suppressed-error-span', ], }, recordedItems: new Set([ createMockSpanAndAnnotation(100, { status: 'error', name: 'suppressed-error-span', }), createMockSpanAndAnnotation(200, { status: 'ok', name: 'ok-span' }), ]), input: { id: 'test', startTime: createTimestamp(0), relatedTo: {}, variant: 'origin', }, recordedItemsByLabel: {}, }, { transitionFromState: 'active' }, ) expect(recording.status).toBe('ok') expect(recording.additionalDurations.startTillInteractive).toBeNull() }) it('should mark trace as error if any error span is not suppressed', () => { const recording = createTraceRecording( { definition: { ...baseDefinitionFixture, suppressErrorStatusPropagationOnSpans: [ ({ span }) => span.name === 'suppressed-error-span', ], }, recordedItems: new Set([ createMockSpanAndAnnotation(100, { status: 'error', name: 'suppressed-error-span', }), createMockSpanAndAnnotation(200, { status: 'error', name: 'non-suppressed-error-span', }), createMockSpanAndAnnotation(300, { status: 'ok', name: 'ok-span' }), ]), input: { id: 'test', startTime: createTimestamp(0), relatedTo: {}, variant: 'origin', }, recordedItemsByLabel: {}, }, { transitionFromState: 'active' }, ) expect(recording.status).toBe('error') expect(recording.additionalDurations.startTillInteractive).toBeNull() }) it('should prioritize interrupted status over error status', () => { const recording = createTraceRecording( { definition: baseDefinitionFixture, recordedItems: new Set([ createMockSpanAndAnnotation(100, { status: 'error', name: 'error-span', }), ]), input: { id: 'test', startTime: createTimestamp(0), relatedTo: {}, variant: 'origin', }, recordedItemsByLabel: {}, }, { transitionFromState: 'active', interruptionReason: 'timeout', }, ) expect(recording.status).toBe('interrupted') expect(recording.additionalDurations.startTillInteractive).toBeNull() }) }) describe('getComputedSpans', () => { const baseDefinition: CompleteTraceDefinition< 'global', AnyRelation, 'origin' > = { ...baseDefinitionFixture, computedSpanDefinitions: { 'test-computed-span': { startSpan: ({ span }) => span.name === 'start-span', endSpan: ({ span }) => span.name === 'end-span', }, }, } it('should compute duration and startOffset correctly', () => { const result = getComputedSpans({ definition: baseDefinition, recordedItems: new Set([ createMockSpanAndAnnotation(100, { name: 'start-span', duration: 50, }), createMockSpanAndAnnotation(200, { name: 'end-span', duration: 50, }), ]), input: { id: 'test', startTime: createTimestamp(0), relatedTo: {}, variant: 'origin', }, recordedItemsByLabel: {}, }) expect(result['test-computed-span']).toEqual({ duration: 150, // (200 + 50) - 100 startOffset: 100, }) }) it('should handle operation-start and operation-end special matchers', () => { const definition: CompleteTraceDefinition< 'global', AnyRelation, 'origin' > = { ...baseDefinition, computedSpanDefinitions: { 'operation-span': { startSpan: 'operation-start', endSpan: 'operation-end', }, }, } const markedCompleteSpan = createMockSpanAndAnnotation(200, { name: 'end-span', }) markedCompleteSpan.annotation.markedComplete = true const result = getComputedSpans({ definition, recordedItems: new Set([ createMockSpanAndAnnotation(100, { name: 'start-span' }), markedCompleteSpan, ]), input: { id: 'test', startTime: createTimestamp(0), // eslint-disable-next-line @typescript-eslint/consistent-type-assertions relatedTo: {} as never, variant: 'origin', }, recordedItemsByLabel: {}, }) expect(result['operation-span']).toBeDefined() }) }) describe('getComputedValues', () => { const baseDefinition: CompleteTraceDefinition< 'global', AnyRelation, 'origin' > = { ...baseDefinitionFixture, computedValueDefinitions: { 'error-count': { matches: [({ span }) => span.status === 'error'], computeValueFromMatches: (matches) => matches.length, }, }, } it('should compute values based on matching spans', () => { const result = getComputedValues({ definition: baseDefinition, recordedItems: new Set([ createMockSpanAndAnnotation(100, { status: 'error' }), createMockSpanAndAnnotation(200, { status: 'error' }), createMockSpanAndAnnotation(300, { status: 'ok' }), ]), input: { id: 'test', startTime: createTimestamp(0), // eslint-disable-next-line @typescript-eslint/consistent-type-assertions relatedTo: {} as never, variant: 'origin', }, recordedItemsByLabel: {}, }) expect(result['error-count']).toBe(2) }) it('should handle multiple matches in computeValueFromMatches', () => { const definition: CompleteTraceDefinition< 'global', AnyRelation, 'origin' > = { ...baseDefinition, computedValueDefinitions: { 'status-counts': { matches: [ ({ span }) => span.status === 'error', ({ span }) => span.status === 'ok', ], computeValueFromMatches: (errors, oks) => errors.length + oks.length, }, }, } const result = getComputedValues({ definition, recordedItems: new Set([ createMockSpanAndAnnotation(100, { status: 'error' }), createMockSpanAndAnnotation(200, { status: 'ok' }), createMockSpanAndAnnotation(300, { status: 'error' }), ]), input: { id: 'test', startTime: createTimestamp(0), // eslint-disable-next-line @typescript-eslint/consistent-type-assertions relatedTo: {} as never, variant: 'origin', }, recordedItemsByLabel: {}, }) expect(result['status-counts']).toEqual(3) }) }) describe('getSpanSummaryAttributes', () => { it('should merge attributes from spans with the same name', () => { const result = getSpanSummaryAttributes([ createMockSpanAndAnnotation(100, { name: 'test-span', attributes: { first: true }, }), createMockSpanAndAnnotation(200, { name: 'test-span', attributes: { second: true }, }), ]) expect(result['test-span']).toEqual({ first: true, second: true, }) }) }) describe('computedRenderBeaconSpans', () => { it('should compute render beacon metrics correctly', () => { const recording = createTraceRecording( { definition: baseDefinitionFixture, recordedItems: new Set([ createMockSpanAndAnnotation(100, { name: 'test-component', type: 'component-render', relatedTo: {}, duration: 50, isIdle: true, renderCount: 1, renderedOutput: 'loading', }), createMockSpanAndAnnotation( 200, { name: 'test-component', type: 'component-render', relatedTo: {}, duration: 50, isIdle: true, renderCount: 2, renderedOutput: 'content', }, { occurrence: 2 }, ), ]), input: { id: 'test', startTime: createTimestamp(0), relatedTo: {}, variant: 'origin', }, recordedItemsByLabel: {}, }, { transitionFromState: 'active' }, ) expect(recording.computedRenderBeaconSpans['test-component']).toEqual({ startOffset: 100, firstRenderTillContent: 150, firstRenderTillLoading: 50, firstRenderTillData: 100, renderCount: 2, sumOfRenderDurations: 100, }) }) }) describe('variant property', () => { it('should include the variant in the recording', () => { const recording = createTraceRecording( { definition: baseDefinitionFixture, recordedItems: new Set([ createMockSpanAndAnnotation(100, { status: 'ok', name: 'test-span', }), ]), input: { id: 'test', startTime: createTimestamp(0), relatedTo: {}, variant: 'origin', }, recordedItemsByLabel: {}, }, { transitionFromState: 'active' }, ) // Verify the variant is included in the recording expect(recording.variant).toBe('origin') }) }) })