@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
JavaScript
"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' },