UNPKG

@zendesk/retrace

Version:

define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API

984 lines (902 loc) 34.5 kB
import './testUtility/asciiTimelineSerializer' import { afterEach, beforeEach, describe, expect, it, type Mock, vitest as jest, } from 'vitest' import { DEFAULT_INTERACTIVE_TIMEOUT_DURATION } from './constants' import { createQuietWindowDurationCalculator } from './getDynamicQuietWindowDuration' import * as matchSpan from './matchSpan' import { type TicketIdRelationSchemasFixture } from './testUtility/fixtures/relationSchemas' import { Check, getSpansFromTimeline, LongTask, Render, } from './testUtility/makeTimeline' import { processSpans } from './testUtility/processSpans' import { TraceManager } from './TraceManager' import type { AnyPossibleReportFn } from './types' const GOOGLES_QUIET_WINDOW_DURATION = 2_000 const GOOGLES_CLUSTER_PADDING = 1_000 const GOOGLES_HEAVY_CLUSTER_THRESHOLD = 250 const cpuIdleProcessorOptions = { getQuietWindowDuration: () => GOOGLES_QUIET_WINDOW_DURATION, clusterPadding: GOOGLES_CLUSTER_PADDING, heavyClusterThreshold: GOOGLES_HEAVY_CLUSTER_THRESHOLD, } describe('TraceManager with Capture Interactivity', () => { let reportFn: Mock let generateId: Mock let reportErrorFn: Mock const DEFAULT_COLDBOOT_TIMEOUT_DURATION = 45_000 jest.useFakeTimers({ now: 0, }) let id = 0 beforeEach(() => { reportFn = jest.fn<AnyPossibleReportFn<TicketIdRelationSchemasFixture>>() id = 0 generateId = jest.fn(() => `id-${id++}`) reportErrorFn = jest.fn() }) afterEach(() => { jest.clearAllMocks() jest.clearAllTimers() }) it('correctly captures trace while waiting the long tasks to settle', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1' }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----${LongTask(50)}------<===5s===>---------${Check} Time: ${0} ${2_000} ${2_001} ${2_001 + DEFAULT_INTERACTIVE_TIMEOUT_DURATION} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end timeline | |-<⋯ +2000 ⋯>-| time (ms) | 0 2000 `) expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(2_000) expect(report.additionalDurations.startTillInteractive).toBe(2_000) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) it('completes the trace with waiting-for-interactive-timeout', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const interactiveTimeout = 5_000 const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: { ...cpuIdleProcessorOptions, timeout: interactiveTimeout, }, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----${LongTask(interactiveTimeout)} Time: ${0} ${2_000} ${2_050} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end timeline | |-<⋯ +2000 ⋯>-| time (ms) | 0 2000 `) expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(2_000) expect(report.interruption).toMatchObject({ reason: 'waiting-for-interactive-timeout', }) expect(report.additionalDurations.startTillInteractive).toBeNull() expect(report.status).toBe('ok') }) it('completes the trace when interrupted during waiting for capture interactive to finish', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.interrupt-during-long-task-operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [matchSpan.withName('end')], interruptOnSpans: [matchSpan.withName('interrupt')], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}---------${Render('end', 0)}---------${Render('interrupt', 0)}--${LongTask(100)}--${Check} Time: ${0} ${200} ${201} ${300} ${5_000} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end timeline | |-<⋯ +200 ⋯>-| time (ms) | 0 200 `) expect(report.name).toBe('ticket.interrupt-during-long-task-operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBeNull() expect(report.status).toBe('ok') expect(report.interruption).toMatchObject({ reason: 'matched-on-interrupt', }) }) it('timeouts the trace while deboucing AND the last span is past the timeout duration', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.debounce-then-interrupted-operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], debounceOnSpans: [{ name: 'debounce' }], captureInteractive: cpuIdleProcessorOptions, debounceWindow: 300, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----${Render('debounce', 0)}------${Check} Time: ${0} ${50} ${100} ${DEFAULT_COLDBOOT_TIMEOUT_DURATION + 100} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end debounce timeline | |-<⋯ +50 ⋯>-|-<⋯ +50 ⋯>-| time (ms) | 0 50 100 `) expect(report.name).toBe('ticket.debounce-then-interrupted-operation') expect(report.status).toBe('interrupted') expect(report.duration).toBeNull() expect(report.interruption).toMatchObject({ reason: 'timeout' }) expect(report.additionalDurations.startTillInteractive).toBeNull() }) it('completes the trace when debouncing is done AND is waiting for capture interactive to finish', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const CUSTOM_CAPTURE_INTERACTIVE_TIMEOUT = 300 const tracer = traceManager.createTracer({ name: 'ticket.debounce-then-interrupted-operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], debounceOnSpans: [{ name: 'debounce' }], captureInteractive: { ...cpuIdleProcessorOptions, timeout: CUSTOM_CAPTURE_INTERACTIVE_TIMEOUT, }, debounceWindow: 100, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}------------------------------${Render('debounce', 100)}---------------------------${Check} Time: ${0} ${DEFAULT_COLDBOOT_TIMEOUT_DURATION - 500} ${DEFAULT_COLDBOOT_TIMEOUT_DURATION - 499} ${DEFAULT_COLDBOOT_TIMEOUT_DURATION + 1} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end timeline | |-<⋯ +44500 ⋯>-| time (ms) | 0 44500 `) expect(report.name).toBe('ticket.debounce-then-interrupted-operation') expect(report.duration).toBe(44_500) expect(report.additionalDurations.startTillInteractive).toBeNull() expect(report.status).toBe('ok') expect(report.interruption).toMatchObject({ reason: 'timeout' }) }) it('uses getQuietWindowDuration from capture interactive config', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const CUSTOM_QUIET_WINDOW_DURATION = 2_000 const TRACE_DURATION = 1_000 const getQuietWindowDuration = jest .fn() .mockReturnValue(CUSTOM_QUIET_WINDOW_DURATION) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: { ...cpuIdleProcessorOptions, timeout: 100, getQuietWindowDuration, }, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----${LongTask(5)}------------${LongTask(5)}-----${Check} Time: ${0} ${TRACE_DURATION} ${1_001} ${1_050} ${1_200} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end timeline | |-<⋯ +1000 ⋯>-| time (ms) | 0 1000 `) expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(TRACE_DURATION) expect(report.additionalDurations.startTillInteractive).toBeNull() expect(report.status).toBe('ok') expect(report.interruption).toMatchObject({ reason: 'waiting-for-interactive-timeout', }) expect(getQuietWindowDuration).toHaveBeenCalled() expect(getQuietWindowDuration).toHaveBeenCalledWith(1_055, TRACE_DURATION) }) it('uses createQuietWindowDurationCalculator for getQuietWindowDuration in capture interactive config', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const TRACE_DURATION = 1_000 const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: { ...cpuIdleProcessorOptions, timeout: 1_000, getQuietWindowDuration: createQuietWindowDurationCalculator(), }, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----------${LongTask(5)}----------------${LongTask(50)}---${Check} Time: ${0} ${TRACE_DURATION} ${1_001} ${1_945} ${2_001} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end timeline | |-<⋯ +1000 ⋯>-| time (ms) | 0 1000 `) expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(TRACE_DURATION) expect(report.additionalDurations.startTillInteractive).toBeNull() expect(report.status).toBe('ok') expect(report.interruption).toMatchObject({ reason: 'waiting-for-interactive-timeout', }) }) it('No long tasks after FMP, FirstCPUIdle immediately after FMP + quiet window', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}----------${LongTask(400)}-----${Check} Time: ${0} ${200} ${4_600} ${5_000} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end timeline | |-<⋯ +200 ⋯>-| time (ms) | 0 200 `) expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBe(200) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) it('One light cluster after FMP, FirstCPUIdle at FMP', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}----------${LongTask(50)}------${LongTask(50)}-----${LongTask(50)}---------${Check} Time: ${0} ${200} ${300} ${400} ${450} ${3_050} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end timeline | |-<⋯ +200 ⋯>-| time (ms) | 0 200 `) expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBe(200) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) it('One heavy cluster after FMP, FirstCPUIdle after the cluster', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}--${LongTask(50)}}-${LongTask(200)}-----${LongTask(50)}-----------${Check} Time: ${0} ${200} ${300} ${400} ${650} ${3_200} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end task(50) task(200) task(50) timeline | |-<⋯ +200 ⋯>-|-----------[++++]------[+++++++++++++++++++++++]------[++++]- time (ms) | 0 200 300 400 650 `) expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBe(700) // TESTING TODO: are we testing enough of first idle time? expect(report.additionalDurations.completeTillInteractive).toBe(500) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) it('Multiple heavy clusters, FirstCPUIdle updated to end of last cluster', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----${LongTask(200)}-----${LongTask(200)}-----${LongTask(200)}-----${Check} Time: ${0} ${200} ${300} ${900} ${1_500} ${3_800} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end task(200) task(200) task(200) timeline | |------------|------[+++++++++++]-<⋯ +400 ⋯>--[+++++++++++]-<⋯ +400 ⋯>-[+++++++++++] time (ms) | 0 200 300 900 1500 `) const lastLongTask = spans.at(-2)! const expectedResult = lastLongTask.startTime.now + lastLongTask.duration expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBe(expectedResult) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) it('Checking before the quiet window has passed - no long tasks processed, FirstCPUIdle not found', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----${Check} Time: ${0} ${200} ${2_400} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end timeline | |-<⋯ +200 ⋯>-| time (ms) | 0 200 `) expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBe(200) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) it('completes the trace with one heavy cluster followed by two light clusters, value is after 1st heavy cluster', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----${LongTask(200)}-----${LongTask(100)}-----${LongTask(200)}-----${LongTask(200)}-----${Check} Time: ${0} ${200} ${300} ${600} ${1_700} ${2_900} ${4_650} } ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | start end task(200) task(100) timeline | |-<⋯ +200 ⋯>-|-----------[+++++++++++++++++++++++]------------[++++++++++]- time (ms) | 0 200 300 600 `) const lastHeavyClusterLongTask = spans.at(3)! const expectedResult = lastHeavyClusterLongTask.startTime.now + lastHeavyClusterLongTask.duration expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBe(expectedResult) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) it('completes the trace with continuous heavy clusters', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${LongTask(300)}-----${Check} Time: ${0} ${200} ${550} ${900} ${1_250} ${1_600} ${1_950} ${2_300} ${2_650} ${3_000} ${3_350} ${3_700} ${4_050} ${4_400} ${4_750} ${5_100} ${7_450} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | end task(300) task(300) task(300) task(300) task(300) task(300) task(300) events | start task(300) task(300) task(300) task(300) task(300) task(300) task(300) timeline | |-|-----[++]-[++]-[++]-[++]-[++]-[++]-[++]-[++]-[++]-[++]-[++]-[++]-[++]-[++]- time (ms) | 0 200 550 900 1250 1600 1950 2300 2650 3000 3350 3700 4050 4400 4750 5100 `) expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBe(5_400) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) it('completes the trace with a light cluster followed by a heavy cluster a second later, FirstCPUIdle updated', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}-----${Render('end', 0)}-----${LongTask(50)}----------${LongTask(50)}----------${LongTask(50)}--------${LongTask(50)}---------${LongTask(200)}-------${LongTask(200)}--------${Check} Time: ${0} ${200} ${300} ${350} ${1_450} ${1_550} ${1_650} ${1_900} ${4_150} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | task(50) task(50) events | start end task(50) task(50) task(200) task(200) timeline | |------------|------[+]--[+]-<⋯ +1050 ⋯>[+]--[+]---[+++++++++++]---[+++++++++++]- time (ms) | 0 200 300 350 1450 1550 1650 1900 `) const lastLongTask = spans.at(-2)! const expectedResult = lastLongTask.startTime.now + lastLongTask.duration expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBe(expectedResult) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) it('completes the trace with a long task overlapping FMP, FirstCPUIdle after the long task', () => { const traceManager = new TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn, generateId, reportErrorFn, }) const tracer = traceManager.createTracer({ name: 'ticket.operation', type: 'operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], captureInteractive: cpuIdleProcessorOptions, variants: { cold_boot: { timeout: DEFAULT_COLDBOOT_TIMEOUT_DURATION, }, }, }) tracer.start({ relatedTo: { ticketId: '1', }, variant: 'cold_boot', }) // prettier-ignore const { spans } = getSpansFromTimeline<TicketIdRelationSchemasFixture>` Events: ${Render('start', 0)}----------${LongTask(110, { start: 150 })}----${Render('end', 0)}-----${Check} Time: ${0} ${150} ${200} ${2_500} ` processSpans(spans, traceManager) expect(reportFn).toHaveBeenCalled() const report: Parameters< AnyPossibleReportFn<TicketIdRelationSchemasFixture> >[0] = reportFn.mock.calls[0]?.[0] ?? [] expect( report.entries.map( (spanAndAnnotation) => spanAndAnnotation.span.performanceEntry, ), ).toMatchInlineSnapshot(` events | events | start task(110) end timeline | |-<⋯ +150 ⋯>-[+++++++++++++++++++++++++++++++++++++++++++++++++++++] timeline | -<⋯ +200 ⋯>---------------------------|-<⋯ +60 ⋯>------------------- time (ms) | 0 150 200 `) const lastLongTask = spans.at(-3)! const expectedResult = lastLongTask.startTime.now + lastLongTask.duration expect(report.name).toBe('ticket.operation') expect(report.duration).toBe(200) expect(report.additionalDurations.startTillInteractive).toBe(expectedResult) expect(report.status).toBe('ok') expect(report.interruption).toBeUndefined() }) })