UNPKG

@zendesk/retrace

Version:

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

1,072 lines (1,071 loc) 50.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const node_util_1 = require("node:util"); const vitest_1 = require("vitest"); const spanTypes_1 = require("./spanTypes"); const TraceManager_1 = require("./TraceManager"); const waitOneTick = (0, node_util_1.promisify)(setImmediate); (0, vitest_1.describe)('creating spans', () => { let reportFn; let generateId; let reportErrorFn; let traceManager; vitest_1.vitest.useFakeTimers({ now: 0, }); let id = 0; (0, vitest_1.beforeEach)(() => { reportFn = vitest_1.vitest.fn(); id = 0; generateId = vitest_1.vitest.fn(() => `id-${id++}`); reportErrorFn = vitest_1.vitest.fn(); traceManager = new TraceManager_1.TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn: reportFn, generateId, reportErrorFn, enableTickTracking: true, }); }); (0, vitest_1.afterEach)(() => { vitest_1.vitest.clearAllMocks(); vitest_1.vitest.clearAllTimers(); }); (0, vitest_1.describe)('TraceManager with tick tracking enabled', () => { (0, vitest_1.it)('should have tickParentResolver initialized when enableTickTracking is true', () => { (0, vitest_1.expect)(traceManager.tickParentResolver).toBeDefined(); }); (0, vitest_1.it)('should not have tickParentResolver when enableTickTracking is false', () => { const traceManagerWithoutTicks = new TraceManager_1.TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn: reportFn, generateId, reportErrorFn, enableTickTracking: false, }); (0, vitest_1.expect)(traceManagerWithoutTicks.tickParentResolver).toBeUndefined(); }); (0, vitest_1.it)('should assign tickId to spans when processing them', () => { const span = traceManager.ensureCompleteSpan({ type: 'mark', name: 'test-span', }); traceManager.processSpan(span); (0, vitest_1.expect)(span.tickId).toBeDefined(); // first generated id goes to TickParentResolver, second to span (0, vitest_1.expect)(span.tickId).toBe('id-0'); (0, vitest_1.expect)(span.id).toBe('id-1'); }); }); (0, vitest_1.describe)('startSpan and endSpan functionality', () => { (0, vitest_1.it)('should create and process start span correctly', () => { const startResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'operation-start', relatedTo: { ticketId: '123' }, }); (0, vitest_1.expect)(startResult.span).toBeDefined(); (0, vitest_1.expect)(startResult.span.tickId).toBe('id-0'); (0, vitest_1.expect)(startResult.span.id).toBe('id-1'); (0, vitest_1.expect)(startResult.span.name).toBe('operation-start'); (0, vitest_1.expect)(startResult.span.type).toBe('mark'); (0, vitest_1.expect)(startResult.span.relatedTo).toEqual({ ticketId: '123' }); (0, vitest_1.expect)(startResult.annotations).toBeUndefined(); // no active trace }); (0, vitest_1.it)('should create and process end span with reference to start span', () => { const startSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'operation-start', relatedTo: { ticketId: '123' }, }); const endResult = traceManager.endSpan(startSpan, { name: 'operation-end', duration: 100, }); (0, vitest_1.expect)(endResult.span).toBeDefined(); (0, vitest_1.expect)(endResult.span).toBe(startSpan); (0, vitest_1.expect)(endResult.span.name).toBe('operation-end'); (0, vitest_1.expect)(endResult.span.duration).toBe(100); (0, vitest_1.expect)(endResult.span.relatedTo).toEqual({ ticketId: '123' }); (0, vitest_1.expect)(endResult.span.tickId).toBeDefined(); }); (0, vitest_1.it)('should handle startRenderSpan and endRenderSpan', () => { const startResult = traceManager.startRenderSpan({ name: 'MyComponent', relatedTo: { ticketId: '123' }, isIdle: false, renderCount: 1, renderedOutput: 'loading', }); (0, vitest_1.expect)(startResult.span.type).toBe('component-render-start'); (0, vitest_1.expect)(startResult.span.name).toBe('MyComponent'); (0, vitest_1.expect)(startResult.span.isIdle).toBe(false); (0, vitest_1.expect)(startResult.span.renderCount).toBe(1); const endResult = traceManager.endRenderSpan(startResult.span, { duration: 50, isIdle: true, }); (0, vitest_1.expect)(endResult.span.type).toBe('component-render'); (0, vitest_1.expect)(endResult.span.duration).toBe(50); (0, vitest_1.expect)(endResult.span.isIdle).toBe(true); (0, vitest_1.expect)(endResult.span).toBe(startResult.span); }); }); (0, vitest_1.describe)('processErrorSpan functionality', () => { (0, vitest_1.it)('should create and process error span', () => { const error = new Error('Test error'); const errorResult = traceManager.processErrorSpan({ error, relatedTo: { ticketId: '123' }, }); const parent = errorResult.resolveParent(); (0, vitest_1.expect)(errorResult.span).toBeDefined(); (0, vitest_1.expect)(errorResult.span.name).toBe('Error'); (0, vitest_1.expect)(errorResult.span.type).toBe('error'); (0, vitest_1.expect)(errorResult.span.status).toBe('error'); (0, vitest_1.expect)(errorResult.span.error).toBe(error); (0, vitest_1.expect)(errorResult.span.relatedTo).toEqual({ ticketId: '123' }); (0, vitest_1.expect)(errorResult.span.tickId).toBeDefined(); (0, vitest_1.expect)(parent).toBeUndefined(); // no parent found }); (0, vitest_1.it)('should handle custom error span name and type', () => { const error = new Error('Custom error'); const errorResult = traceManager.processErrorSpan({ error, name: 'CustomErrorName', relatedTo: { ticketId: '456' }, }); (0, vitest_1.expect)(errorResult.span.name).toBe('CustomErrorName'); (0, vitest_1.expect)(errorResult.span.type).toBe('error'); (0, vitest_1.expect)(errorResult.span.status).toBe('error'); }); }); (0, vitest_1.describe)('createAndProcessSpan functionality', () => { (0, vitest_1.it)('should create and process any type of span', () => { const spanResult = traceManager.createAndProcessSpan({ type: 'measure', name: 'custom-measure', duration: 200, relatedTo: { ticketId: '789' }, attributes: { customAttribute: 'value' }, }); (0, vitest_1.expect)(spanResult.span).toBeDefined(); (0, vitest_1.expect)(spanResult.span.type).toBe('measure'); (0, vitest_1.expect)(spanResult.span.name).toBe('custom-measure'); (0, vitest_1.expect)(spanResult.span.duration).toBe(200); (0, vitest_1.expect)(spanResult.span.relatedTo).toEqual({ ticketId: '789' }); (0, vitest_1.expect)(spanResult.span.attributes).toEqual({ customAttribute: 'value' }); (0, vitest_1.expect)(spanResult.span.tickId).toBeDefined(); (0, vitest_1.expect)(spanResult.resolveParent()).toBeUndefined(); // no parent resolved }); (0, vitest_1.it)('should handle component render span creation', () => { const renderResult = traceManager.createAndProcessSpan({ type: 'component-render', name: 'TestComponent', isIdle: true, renderCount: 3, relatedTo: { ticketId: '999' }, duration: 25, renderedOutput: 'content', }); (0, vitest_1.expect)(renderResult.span.type).toBe('component-render'); (0, vitest_1.expect)(renderResult.span.isIdle).toBe(true); (0, vitest_1.expect)(renderResult.span.renderCount).toBe(3); (0, vitest_1.expect)(renderResult.span.duration).toBe(25); }); }); (0, vitest_1.describe)('parent span resolution with parentSpanMatcher', () => { let activeTrace; (0, vitest_1.beforeEach)(() => { // Create a tracer to enable trace recording const tracer = traceManager.createTracer({ name: 'test-operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], variants: { default: { timeout: 10_000 }, }, }); // Start a trace to have an active context activeTrace = tracer.start({ relatedTo: { ticketId: '123' }, variant: 'default', }); }); (0, vitest_1.it)('should create getParentSpan function from parentSpanMatcher for span-created-tick search', () => { // Create spans with parent resolution in the same tick const parentSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'parent-span', relatedTo: { ticketId: '123' }, }); traceManager.processSpan(parentSpan); const childSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'child-span', relatedTo: { ticketId: '123' }, parentSpanMatcher: { search: 'span-created-tick', searchDirection: 'before-self', match: { name: 'parent-span' }, }, }); (0, vitest_1.expect)(childSpan.getParentSpan).toBeDefined(); (0, vitest_1.expect)(typeof childSpan.getParentSpan).toBe('function'); // Process the child span traceManager.processSpan(childSpan); // trace hasn't ended yet: (0, vitest_1.expect)(reportFn).not.toHaveBeenCalled(); const trace = traceManager.currentTraceContext; (0, vitest_1.assert)(trace); const childSpanAndAnnotation = trace.recordedItems.get(childSpan.id); (0, vitest_1.assert)(childSpanAndAnnotation); (0, vitest_1.expect)(childSpanAndAnnotation.span).toBe(childSpan); const getParentSpan = vitest_1.vitest.spyOn(childSpan, 'getParentSpan'); const endSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'end', relatedTo: { ticketId: '123' }, }); // complete the trace to trigger parent resolution traceManager.processSpan(endSpan); // trace should have finished (0, vitest_1.expect)(traceManager.currentTraceContext).toBeUndefined(); (0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(1); (0, vitest_1.expect)(getParentSpan).toHaveBeenCalledTimes(1); (0, vitest_1.expect)(getParentSpan).toHaveReturnedWith(parentSpan); // The parentSpanMatcher should have generated a working getParentSpan (0, vitest_1.expect)(childSpan[spanTypes_1.PARENT_SPAN]).toBe(parentSpan); }); (0, vitest_1.it)('should create getParentSpan function from parentSpanMatcher for span-created-tick search with after-self direction', () => { // Process the child span const childSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'child-span', relatedTo: { ticketId: '123' }, parentSpanMatcher: { search: 'span-created-tick', searchDirection: 'after-self', match: { name: 'parent-span' }, }, }); (0, vitest_1.expect)(childSpan.getParentSpan).toBeDefined(); (0, vitest_1.expect)(typeof childSpan.getParentSpan).toBe('function'); traceManager.processSpan(childSpan); // Create parent span in the same tick AFTER the child span const parentSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'parent-span', relatedTo: { ticketId: '123' }, }); traceManager.processSpan(parentSpan); // trace hasn't ended yet: (0, vitest_1.expect)(reportFn).not.toHaveBeenCalled(); const trace = traceManager.currentTraceContext; (0, vitest_1.assert)(trace); const childSpanAndAnnotation = trace.recordedItems.get(childSpan.id); (0, vitest_1.assert)(childSpanAndAnnotation); (0, vitest_1.expect)(childSpanAndAnnotation.span).toBe(childSpan); const getParentSpan = vitest_1.vitest.spyOn(childSpan, 'getParentSpan'); const endSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'end', relatedTo: { ticketId: '123' }, }); // complete the trace to trigger parent resolution traceManager.processSpan(endSpan); // trace should have finished (0, vitest_1.expect)(traceManager.currentTraceContext).toBeUndefined(); (0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(1); (0, vitest_1.expect)(getParentSpan).toHaveBeenCalledTimes(1); (0, vitest_1.expect)(getParentSpan).toHaveReturnedWith(parentSpan); // The parentSpanMatcher should have generated a working getParentSpan (0, vitest_1.expect)(childSpan[spanTypes_1.PARENT_SPAN]).toBe(parentSpan); }); (0, vitest_1.it)('should create getParentSpan function from parentSpanMatcher for entire-recording search', async () => { // Create parent span first const parentSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'parent-span', relatedTo: { ticketId: '123' }, }); traceManager.processSpan(parentSpan); await waitOneTick(); // trace hasn't ended yet: (0, vitest_1.expect)(reportFn).not.toHaveBeenCalled(); const childSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'child-span', relatedTo: { ticketId: '123' }, parentSpanMatcher: { search: 'entire-recording', searchDirection: 'before-self', match: { type: 'mark', name: 'parent-span' }, }, }); (0, vitest_1.expect)(childSpan.getParentSpan).toBeDefined(); // Process the child span and complete trace traceManager.processSpan(childSpan); const endSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'end', relatedTo: { ticketId: '123' }, }); traceManager.processSpan(endSpan); // trace should have finished (0, vitest_1.expect)(traceManager.currentTraceContext).toBeUndefined(); // The parentSpanMatcher should have generated a working getParentSpan (0, vitest_1.expect)(childSpan[spanTypes_1.PARENT_SPAN]).toBe(parentSpan); }); }); (0, vitest_1.describe)('tick tracking in same event loop tick', () => { (0, vitest_1.it)('should assign the same tickId to spans created in the same synchronous execution', () => { const span1 = traceManager.ensureCompleteSpan({ type: 'mark', name: 'span1', }); traceManager.processSpan(span1); const span2 = traceManager.ensureCompleteSpan({ type: 'mark', name: 'span2', }); traceManager.processSpan(span2); const span3 = traceManager.ensureCompleteSpan({ type: 'mark', name: 'span3', }); traceManager.processSpan(span3); (0, vitest_1.expect)(span1.tickId).toBe(span2.tickId); (0, vitest_1.expect)(span2.tickId).toBe(span3.tickId); (0, vitest_1.expect)(span1.tickId).toBeDefined(); }); (0, vitest_1.it)('should assign different tickIds to spans created in different ticks', async () => { const span1 = traceManager.ensureCompleteSpan({ type: 'mark', name: 'span1', }); traceManager.processSpan(span1); // Ensure all microtasks are processed await waitOneTick(); const span2 = traceManager.ensureCompleteSpan({ type: 'mark', name: 'span2', }); traceManager.processSpan(span2); (0, vitest_1.expect)(span1.tickId).not.toBe(span2.tickId); (0, vitest_1.expect)(span1.tickId).toBeDefined(); (0, vitest_1.expect)(span2.tickId).toBeDefined(); }); }); (0, vitest_1.describe)('convenience span creation methods', () => { (0, vitest_1.it)('should create performance entry spans using makePerformanceEntrySpan', () => { const span = traceManager.makePerformanceEntrySpan({ type: 'measure', name: 'custom-measure', relatedTo: { ticketId: '123' }, duration: 50, }); (0, vitest_1.expect)(span.type).toBe('measure'); (0, vitest_1.expect)(span.name).toBe('custom-measure'); (0, vitest_1.expect)(span.duration).toBe(50); (0, vitest_1.expect)(span.id).toBeDefined(); (0, vitest_1.expect)(span.startTime).toBeDefined(); }); (0, vitest_1.it)('should create render spans using makeRenderSpan', () => { const span = traceManager.makeRenderSpan({ type: 'component-render', name: 'MyComponent', isIdle: true, renderCount: 5, relatedTo: { ticketId: '456' }, renderedOutput: 'content', }); (0, vitest_1.expect)(span.type).toBe('component-render'); (0, vitest_1.expect)(span.name).toBe('MyComponent'); (0, vitest_1.expect)(span.isIdle).toBe(true); (0, vitest_1.expect)(span.renderCount).toBe(5); (0, vitest_1.expect)(span.id).toBeDefined(); (0, vitest_1.expect)(span.startTime).toBeDefined(); }); }); (0, vitest_1.describe)('ensureCompleteSpan functionality', () => { (0, vitest_1.it)('should auto-generate id when not provided', () => { const span = traceManager.ensureCompleteSpan({ type: 'mark', name: 'test-span', }); (0, vitest_1.expect)(span.id).toBe('id-1'); (0, vitest_1.expect)(span.startTime).toBeDefined(); (0, vitest_1.expect)(span.attributes).toEqual({}); (0, vitest_1.expect)(span.duration).toBe(0); }); (0, vitest_1.it)('should use provided id when given', () => { const span = traceManager.ensureCompleteSpan({ type: 'mark', name: 'test-span', id: 'custom-id', }); (0, vitest_1.expect)(span.id).toBe('custom-id'); }); (0, vitest_1.it)('should merge provided attributes', () => { const span = traceManager.ensureCompleteSpan({ type: 'mark', name: 'test-span', attributes: { custom: 'value', other: 123 }, }); (0, vitest_1.expect)(span.attributes).toEqual({ custom: 'value', other: 123 }); }); (0, vitest_1.it)('should handle partial startTime', () => { const span = traceManager.ensureCompleteSpan({ type: 'mark', name: 'test-span', startTime: { now: 100 }, }); (0, vitest_1.expect)(span.startTime.now).toBe(100); (0, vitest_1.expect)(span.startTime.epoch).toBeDefined(); }); }); (0, vitest_1.describe)('edge cases and error handling', () => { (0, vitest_1.it)('should handle spans without tick tracking', () => { const traceManagerNoTicks = new TraceManager_1.TraceManager({ relationSchemas: { ticket: { ticketId: String } }, reportFn: reportFn, generateId, reportErrorFn, enableTickTracking: false, }); const span = traceManagerNoTicks.ensureCompleteSpan({ type: 'mark', name: 'test-span', }); const processed = traceManagerNoTicks.processSpan(span); (0, vitest_1.expect)(span.tickId).toBeUndefined(); (0, vitest_1.expect)(span.id).toBeDefined(); }); }); (0, vitest_1.describe)('updateSpan functionality', () => { let activeTrace; (0, vitest_1.beforeEach)(() => { // Create a tracer to enable trace recording const tracer = traceManager.createTracer({ name: 'test-operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], variants: { default: { timeout: 10_000 }, }, }); // Start a trace to have an active context activeTrace = tracer.start({ relatedTo: { ticketId: '123' }, variant: 'default', }); }); (0, vitest_1.it)('should merge object properties like attributes', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, attributes: { originalProp: 'original', keepThis: 'value1', }, }); // Update attributes - should merge with existing attributes result.updateSpan({ attributes: { originalProp: 'updated', newProp: 'added', }, }); (0, vitest_1.expect)(result.span.attributes).toEqual({ originalProp: 'updated', keepThis: 'value1', newProp: 'added', }); }); (0, vitest_1.it)('should merge relatedTo object properties', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, }); // Update relatedTo - should merge with existing relatedTo result.updateSpan({ relatedTo: { ticketId: '456' }, }); (0, vitest_1.expect)(result.span.relatedTo).toEqual({ ticketId: '456' }); }); (0, vitest_1.it)('should update component render span specific properties', () => { const result = traceManager.createAndProcessSpan({ type: 'component-render', name: 'TestComponent', relatedTo: { ticketId: '123' }, isIdle: false, renderCount: 1, renderedOutput: 'loading', attributes: {}, }); // Update component-specific properties result.updateSpan({ isIdle: true, renderedOutput: 'content', }); (0, vitest_1.expect)(result.span.isIdle).toBe(true); (0, vitest_1.expect)(result.span.renderedOutput).toBe('content'); }); (0, vitest_1.it)('should handle undefined values to remove properties from objects', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, attributes: { prop1: 'value1', prop2: 'value2', }, }); // Update attributes to remove prop1 by setting it to undefined result.updateSpan({ attributes: { prop1: undefined, prop3: 'new value', }, }); (0, vitest_1.expect)(result.span.attributes).toEqual({ prop1: undefined, prop2: 'value2', prop3: 'new value', }); }); (0, vitest_1.it)('should ignore updates when trace has changed', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, }); const originalAttributes = { ...result.span.attributes }; // Complete the current trace by adding the required end span const endSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'end', relatedTo: { ticketId: '123' }, }); traceManager.processSpan(endSpan); // Now the trace has ended, updateSpan should be ignored result.updateSpan({ attributes: { shouldNotUpdate: 'ignored' }, }); // Span should remain unchanged (0, vitest_1.expect)(result.span.attributes).toEqual(originalAttributes); }); (0, vitest_1.it)('should work with error spans', () => { const error = new Error('Test error'); const result = traceManager.processErrorSpan({ error, relatedTo: { ticketId: '123' }, attributes: { errorType: 'validation' }, }); // Update error span attributes result.updateSpan({ attributes: { errorType: 'network', severity: 'high', }, }); (0, vitest_1.expect)(result.span.attributes).toEqual({ errorType: 'network', severity: 'high', }); (0, vitest_1.expect)(result.span.error).toBe(error); // Error object should remain unchanged }); (0, vitest_1.it)('should update multiple properties in one call for component render spans', () => { const result = traceManager.createAndProcessSpan({ type: 'component-render', name: 'TestComponent', relatedTo: { ticketId: '123' }, isIdle: false, renderCount: 1, renderedOutput: 'loading', attributes: { version: '1.0' }, }); // Update multiple properties at once result.updateSpan({ isIdle: true, renderedOutput: 'content', attributes: { version: '2.0', updated: true, }, }); (0, vitest_1.expect)(result.span.isIdle).toBe(true); (0, vitest_1.expect)(result.span.renderedOutput).toBe('content'); (0, vitest_1.expect)(result.span.attributes).toEqual({ version: '2.0', updated: true, }); }); (0, vitest_1.it)('should not affect span matching since spans are processed synchronously', () => { // This test documents the caveat mentioned in the updateSpan documentation const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, attributes: { matchable: false }, }); // Update the attribute after processing result.updateSpan({ attributes: { matchable: true }, }); // The span's attribute is updated (0, vitest_1.expect)(result.span.attributes?.matchable).toBe(true); // But if there were matchers that relied on this attribute, // they would have already been evaluated during processSpan() // This test just documents this behavior - the actual matching // logic would be tested in the tracer/matcher tests }); (0, vitest_1.it)('should not do anything without an active trace', () => { const endSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'end', relatedTo: { ticketId: '123' }, }); traceManager.processSpan(endSpan); // Create a span without an active trace const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'standalone-span', }); const originalAttributes = { ...result.span.attributes }; // updateSpan should be a no-op, as there is no active trace result.updateSpan({ attributes: { updated: true }, }); (0, vitest_1.expect)(result.span.attributes).toEqual(originalAttributes); }); (0, vitest_1.it)('should only allow updating specific properties defined in UpdatableSpanProperties', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, attributes: { prop: 'value' }, }); // These properties should be updatable result.updateSpan({ attributes: { newProp: 'new' }, relatedTo: { ticketId: '456' }, }); (0, vitest_1.expect)(result.span.attributes).toEqual({ prop: 'value', newProp: 'new' }); (0, vitest_1.expect)(result.span.relatedTo).toEqual({ ticketId: '456' }); // Properties like name, duration, id, etc. are not in UpdatableSpanProperties // so they cannot be updated via updateSpan (this is by design) }); (0, vitest_1.it)('should preserve object references when merging', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, attributes: { nested: { prop1: 'value1' }, topLevel: 'keep' }, }); const originalAttributesRef = result.span.attributes; // Update should merge into the existing attributes object result.updateSpan({ attributes: { nested: { prop2: 'value2' }, newTopLevel: 'added' }, }); // The attributes object reference should remain the same (0, vitest_1.expect)(result.span.attributes).toBe(originalAttributesRef); // Top-level properties should be merged (Object.assign behavior) (0, vitest_1.expect)(result.span.attributes).toEqual({ nested: { prop2: 'value2' }, // nested object is replaced, not merged topLevel: 'keep', newTopLevel: 'added', }); }); }); (0, vitest_1.describe)('span re-processing after updateSpan', () => { let activeTrace; (0, vitest_1.beforeEach)(() => { // Create a tracer to enable trace recording const tracer = traceManager.createTracer({ name: 'reprocessing-test', relationSchemaName: 'ticket', requiredSpans: [ { name: 'test-span', attributes: { status: 'ready' } }, { name: 'final-span', attributes: { completed: true } }, ], variants: { default: { timeout: 10_000 }, }, }); // Start a trace to have an active context activeTrace = tracer.start({ relatedTo: { ticketId: '123' }, variant: 'default', }); }); (0, vitest_1.it)('should re-evaluate matchers when updateSpan is called', () => { // Create a span with the correct name but wrong attribute const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, attributes: { status: 'pending' }, // Wrong status, should be 'ready' }); // Verify the trace is still active (required spans not met) (0, vitest_1.expect)(traceManager.currentTraceContext?.stateMachine?.currentState).toBe('active'); // Update the span attributes to match the requirement - this should trigger re-processing result.updateSpan({ attributes: { status: 'ready' }, // Now matches the required attribute }); // Verify the attribute was updated (0, vitest_1.expect)(result.span.attributes?.status).toBe('ready'); // Create the second required span to complete the trace const finalSpan = traceManager.createAndProcessSpan({ type: 'mark', name: 'final-span', relatedTo: { ticketId: '123' }, attributes: { completed: true }, }); // The trace should now be complete (0, vitest_1.expect)(traceManager.currentTraceContext).toBeUndefined(); (0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(1); }); (0, vitest_1.it)('should re-evaluate attribute-based matchers when attributes are updated', () => { // End the current trace first const endCurrentTrace = traceManager.ensureCompleteSpan({ type: 'mark', name: 'final-span', relatedTo: { ticketId: '123' }, attributes: { completed: true }, }); traceManager.processSpan(endCurrentTrace); // Create a tracer that requires a specific attribute value const attributeTracer = traceManager.createTracer({ name: 'attribute-test', relationSchemaName: 'ticket', requiredSpans: [ { name: 'test-span', attributes: { required: true } }, { name: 'end' }, ], variants: { default: { timeout: 10_000 }, }, }); // Start the new trace attributeTracer.start({ relatedTo: { ticketId: '456' }, variant: 'default', }); // Create a span without the required attribute const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '456' }, attributes: { required: false, other: 'value' }, }); // Verify the trace is still active (required span not matched due to attribute) (0, vitest_1.expect)(traceManager.currentTraceContext?.stateMachine?.currentState).toBe('active'); // Update the span attributes to match the requirement result.updateSpan({ attributes: { required: true, additional: 'new' }, }); // Verify the attributes were merged correctly (0, vitest_1.expect)(result.span.attributes).toEqual({ required: true, other: 'value', additional: 'new', }); // Add the end span to complete the trace const endSpan = traceManager.createAndProcessSpan({ type: 'mark', name: 'end', relatedTo: { ticketId: '456' }, }); // The trace should now be complete (0, vitest_1.expect)(traceManager.currentTraceContext).toBeUndefined(); (0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2); // Original trace + this new one }); (0, vitest_1.it)('should not re-process spans after trace has ended', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, attributes: { original: 'value' }, }); // Complete the trace const initialSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, attributes: { status: 'ready' }, }); traceManager.processSpan(initialSpan); const finalSpan = traceManager.ensureCompleteSpan({ type: 'mark', name: 'final-span', relatedTo: { ticketId: '123' }, attributes: { completed: true }, }); traceManager.processSpan(finalSpan); // Trace should be complete (0, vitest_1.expect)(traceManager.currentTraceContext).toBeUndefined(); const originalAttributes = { ...result.span.attributes }; // Try to update the span after trace completion - should be ignored result.updateSpan({ attributes: { updated: 'ignored' }, }); // Attributes should remain unchanged (0, vitest_1.expect)(result.span.attributes).toEqual(originalAttributes); }); (0, vitest_1.it)('should preserve span object references during re-processing', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'test-span', relatedTo: { ticketId: '123' }, attributes: { prop: 'value' }, }); const originalSpanRef = result.span; const originalAttributesRef = result.span.attributes; // Update the span result.updateSpan({ attributes: { newProp: 'newValue' }, }); // Object references should be preserved (0, vitest_1.expect)(result.span).toBe(originalSpanRef); (0, vitest_1.expect)(result.span.attributes).toBe(originalAttributesRef); // But content should be updated (0, vitest_1.expect)(result.span.attributes).toEqual({ prop: 'value', newProp: 'newValue', }); }); (0, vitest_1.it)('should handle multiple updateSpan calls correctly', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'evolving-span', relatedTo: { ticketId: '123' }, attributes: { step: 1 }, }); // First update result.updateSpan({ attributes: { step: 2, phase: 'early' }, }); (0, vitest_1.expect)(result.span.attributes).toEqual({ step: 2, phase: 'early', }); // Second update result.updateSpan({ attributes: { step: 3, phase: 'late', final: true }, }); (0, vitest_1.expect)(result.span.attributes).toEqual({ step: 3, phase: 'late', final: true, }); // Third update - partial result.updateSpan({ attributes: { phase: 'complete' }, }); (0, vitest_1.expect)(result.span.attributes).toEqual({ step: 3, phase: 'complete', final: true, }); }); }); (0, vitest_1.describe)('findSpanInParentHierarchy functionality', () => { let activeTrace; (0, vitest_1.beforeEach)(() => { // Create a tracer to enable trace recording const tracer = traceManager.createTracer({ name: 'test-operation', relationSchemaName: 'ticket', requiredSpans: [{ name: 'end' }], variants: { default: { timeout: 10_000 }, }, }); // Start a trace to have an active context activeTrace = tracer.start({ relatedTo: { ticketId: '123' }, variant: 'default', }); }); (0, vitest_1.it)('should find the span itself when it matches the criteria', () => { const result = traceManager.createAndProcessSpan({ type: 'mark', name: 'target-span', relatedTo: { ticketId: '123' }, attributes: { category: 'test' }, }); const found = traceManager.findSpanInParentHierarchy(result.span, { name: 'target-span', }); (0, vitest_1.expect)(found).toBeDefined(); (0, vitest_1.expect)(found?.id).toBe(result.span.id); (0, vitest_1.expect)(found?.name).toBe('target-span'); }); (0, vitest_1.it)('should find parent span when child does not match but parent does', () => { // Create grandparent const grandparentResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'grandparent', relatedTo: { ticketId: '123' }, attributes: { level: 'root' }, }); // Create parent with explicit parentSpan const parentResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'parent', relatedTo: { ticketId: '123' }, parentSpan: grandparentResult.span, attributes: { level: 'middle' }, }); // Create child with explicit parentSpan const childResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'child', relatedTo: { ticketId: '123' }, parentSpan: parentResult.span, attributes: { level: 'leaf' }, }); // Search for parent from child const found = traceManager.findSpanInParentHierarchy(childResult.span, { name: 'parent', }); (0, vitest_1.expect)(found).toBeDefined(); (0, vitest_1.expect)(found?.id).toBe(parentResult.span.id); (0, vitest_1.expect)(found?.name).toBe('parent'); }); (0, vitest_1.it)('should traverse multiple levels to find matching ancestor', () => { // Create a hierarchy: root -> middle -> leaf const rootResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'root', relatedTo: { ticketId: '123' }, attributes: { category: 'system' }, }); const middleResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'middle', relatedTo: { ticketId: '123' }, parentSpan: rootResult.span, attributes: { category: 'business' }, }); const leafResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'leaf', relatedTo: { ticketId: '123' }, parentSpan: middleResult.span, attributes: { category: 'ui' }, }); // Search for root from leaf (should skip middle) const found = traceManager.findSpanInParentHierarchy(leafResult.span, { attributes: { category: 'system' }, }); (0, vitest_1.expect)(found).toBeDefined(); (0, vitest_1.expect)(found?.id).toBe(rootResult.span.id); (0, vitest_1.expect)(found?.name).toBe('root'); }); (0, vitest_1.it)('should return undefined when no match is found in hierarchy', () => { const parentResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'parent', relatedTo: { ticketId: '123' }, }); const childResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'child', relatedTo: { ticketId: '123' }, parentSpan: parentResult.span, }); // Search for non-existent span name const found = traceManager.findSpanInParentHierarchy(childResult.span, { name: 'non-existent', }); (0, vitest_1.expect)(found).toBeUndefined(); }); (0, vitest_1.it)('should work with complex matchers', () => { const parentResult = traceManager.createAndProcessSpan({ type: 'measure', name: 'complex-parent', relatedTo: { ticketId: '123' }, attributes: { category: 'performance', priority: 'high' }, }); const childResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'simple-child', relatedTo: { ticketId: '123' }, parentSpan: parentResult.span, attributes: { category: 'ui' }, }); // Use complex matcher with multiple conditions const found = traceManager.findSpanInParentHierarchy(childResult.span, { type: 'measure', attributes: { category: 'performance', priority: 'high' }, }); (0, vitest_1.expect)(found).toBeDefined(); (0, vitest_1.expect)(found?.id).toBe(parentResult.span.id); (0, vitest_1.expect)(found?.type).toBe('measure'); }); (0, vitest_1.it)('should work with function-based matchers', () => { const parentResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'dynamic-parent', relatedTo: { ticketId: '123' }, attributes: { timestamp: Date.now() }, }); const childResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'child', relatedTo: { ticketId: '123' }, parentSpan: parentResult.span, }); // Use function matcher const found = traceManager.findSpanInParentHierarchy(childResult.span, ({ span }) => span.name.startsWith('dynamic-')); (0, vitest_1.expect)(found).toBeDefined(); (0, vitest_1.expect)(found?.id).toBe(parentResult.span.id); (0, vitest_1.expect)(found?.name).toBe('dynamic-parent'); }); (0, vitest_1.it)('should stop traversal when parent span is not found in recorded items', () => { const childResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'orphaned-child', relatedTo: { ticketId: '123' }, parentSpan: undefined, }); const found = traceManager.findSpanInParentHierarchy(childResult.span, { name: 'any-name', }); // Should only check the child itself, then stop when parent is not found (0, vitest_1.expect)(found).toBeUndefined(); }); (0, vitest_1.it)('should work with component render spans', () => { const parentRenderResult = traceManager.createAndProcessSpan({ type: 'component-render', name: 'ParentComponent', relatedTo: { ticketId: '123' }, isIdle: false, renderCount: 1, renderedOutput: 'content', }); const childRenderResult = traceManager.createAndProcessSpan({ type: 'component-render', name: 'ChildComponent', relatedTo: { ticketId: '123' }, parentSpan: parentRenderResult.span, isIdle: false, renderCount: 2, renderedOutput: 'loading', }); const found = traceManager.findSpanInParentHierarchy(childRenderResult.span, { name: 'ParentComponent' }); (0, vitest_1.expect)(found).toBeDefined(); (0, vitest_1.expect)(found?.id).toBe(parentRenderResult.span.id); (0, vitest_1.expect)(found .renderCount).toBe(1); }); (0, vitest_1.it)('should work with error spans', () => { const contextResult = traceManager.createAndProcessSpan({ type: 'mark', name: 'context-span', relatedTo: { ticketId: '123' },