@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
2,340 lines • 126 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
require("./testUtility/asciiTimelineSerializer");
const vitest_1 = require("vitest");
const matchSpan = __importStar(require("./matchSpan"));
const relationSchemas_1 = require("./testUtility/fixtures/relationSchemas");
const makeTimeline_1 = require("./testUtility/makeTimeline");
const processSpans_1 = require("./testUtility/processSpans");
const TraceManager_1 = require("./TraceManager");
(0, vitest_1.describe)('TraceManager - Child Traces (Nested Proposal)', () => {
let reportFn;
// TS doesn't like that reportFn is wrapped in Mock<> type
const getReportFn = () => reportFn;
let generateId;
let reportErrorFn;
const DEFAULT_TIMEOUT_DURATION = 45_000;
vitest_1.vitest.useFakeTimers({
now: 0,
});
let idPerType = {
span: 0,
trace: 0,
tick: 0,
};
(0, vitest_1.beforeEach)(() => {
idPerType = {
span: 0,
trace: 0,
tick: 0,
};
generateId = vitest_1.vitest.fn((type) => {
const seq = idPerType[type]++;
return type === 'span'
? `id-${seq}`
: type === 'trace'
? `trace-${seq}`
: `tick-${seq}`;
});
reportFn = vitest_1.vitest.fn();
reportErrorFn = vitest_1.vitest.fn();
});
(0, vitest_1.afterEach)(() => {
vitest_1.vitest.clearAllMocks();
vitest_1.vitest.clearAllTimers();
});
(0, vitest_1.describe)('Basic Child Adoption (F-1, F-2)', () => {
(0, vitest_1.it)('should adopt child trace instead of interrupting parent when child trace name is in adoptAsChildren', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer that can adopt 'child-operation' traces
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent trace
const parentTraceId = parentTracer.start({
id: 'parent-trace-id',
relatedTo: { ticketId: '1' },
variant: 'default',
});
(0, vitest_1.expect)(parentTraceId).toBe('parent-trace-id');
// Start child trace - should be adopted, not interrupt parent
const childTraceId = childTracer.start({
id: 'child-trace-id',
relatedTo: { ticketId: '1' },
variant: 'default',
});
(0, vitest_1.expect)(childTraceId).toBe('child-trace-id');
// the Trace Manager should still have a reference to the parent trace, child never overrides it:
(0, vitest_1.expect)(traceManager.currentTraceContext?.input.id).toBe('parent-trace-id');
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${50} ${100} ${150}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2); // Both parent and child should complete
// Verify parent trace completed with child
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
// Verify child trace completed
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(childReport).toBeDefined();
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('parent-trace-id'); // Child adopted by parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should interrupt parent trace when child trace name is NOT in adoptAsChildren (existing behavior)', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer that does NOT adopt 'other-operation' traces
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'], // Only adopts child-operation, not other-operation
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create tracer for a different operation that should interrupt
const otherTracer = traceManager.createTracer({
name: 'ticket.other-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'other-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent trace
const parentTraceId = parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
(0, vitest_1.expect)(parentTraceId).toBe('trace-0');
// Start other trace - should interrupt parent (existing behavior)
const otherTraceId = otherTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
(0, vitest_1.expect)(otherTraceId).toBe('trace-1');
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('other-end', 0)}
Time: ${0} ${50}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
// Verify parent trace was interrupted
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('interrupted');
(0, vitest_1.expect)(parentReport?.interruption).toMatchObject({
reason: 'another-trace-started',
});
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
// Verify other trace completed
const otherReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.other-operation')?.[0];
(0, vitest_1.expect)(otherReport).toBeDefined();
(0, vitest_1.expect)(otherReport?.status).toBe('ok');
(0, vitest_1.expect)(otherReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(otherReport?.parentTraceId).toBeUndefined(); // Other trace has no parent (it interrupted)
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should handle multiple children being adopted by the same parent', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer that can adopt multiple child trace types
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: [
'ticket.child-operation-1',
'ticket.child-operation-2',
],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create first child tracer
const childTracer1 = traceManager.createTracer({
name: 'ticket.child-operation-1',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-1-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create second child tracer
const childTracer2 = traceManager.createTracer({
name: 'ticket.child-operation-2',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-2-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent trace
const parentTraceId = parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
(0, vitest_1.expect)(parentTraceId).toBe('trace-0');
// Start first child trace - should be adopted
const childTraceId1 = childTracer1.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
(0, vitest_1.expect)(childTraceId1).toBe('trace-1');
// Start second child trace - should also be adopted
const childTraceId2 = childTracer2.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
(0, vitest_1.expect)(childTraceId2).toBe('trace-2');
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-1-start', 0)}---${(0, makeTimeline_1.Render)('child-2-start', 0)}---${(0, makeTimeline_1.Render)('child-1-end', 0)}---${(0, makeTimeline_1.Render)('child-2-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${50} ${75} ${100} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(3); // Parent and both children should complete
// Verify parent trace completed
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
// Verify first child trace completed
const child1Report = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation-1')?.[0];
(0, vitest_1.expect)(child1Report).toBeDefined();
(0, vitest_1.expect)(child1Report?.status).toBe('ok');
(0, vitest_1.expect)(child1Report?.interruption).toBeUndefined();
(0, vitest_1.expect)(child1Report?.parentTraceId).toBe('trace-0'); // Child-1 adopted by parent
// Verify second child trace completed
const child2Report = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation-2')?.[0];
(0, vitest_1.expect)(child2Report).toBeDefined();
(0, vitest_1.expect)(child2Report?.status).toBe('ok');
(0, vitest_1.expect)(child2Report?.interruption).toBeUndefined();
(0, vitest_1.expect)(child2Report?.parentTraceId).toBe('trace-0'); // Child-2 adopted by parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should transition parent to waiting-for-children state immediately after completing its own requirements', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer that can adopt children
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent trace
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Start child trace
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}---${makeTimeline_1.Check}
Time: ${0} ${50} ${100} ${150}
`;
// Process only parent completion first - child is still running
(0, processSpans_1.processSpans)(spans.slice(0, 3), traceManager);
// Parent should not have reported yet (waiting for child)
(0, vitest_1.expect)(reportFn).not.toHaveBeenCalled();
// Now complete the child
// prettier-ignore
const { spans: childEndSpans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('child-end', 0)}
Time: ${200}
`;
(0, processSpans_1.processSpans)(childEndSpans, traceManager);
// Now both should be reported
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
// Verify parent completed after child
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Parent Completion (F-3)', () => {
(0, vitest_1.it)('should transition parent from waiting-for-children to complete when all children finish', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: [
'ticket.child-operation-1',
'ticket.child-operation-2',
],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracers
const childTracer1 = traceManager.createTracer({
name: 'ticket.child-operation-1',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-1-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer2 = traceManager.createTracer({
name: 'ticket.child-operation-2',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-2-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer1.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer2.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete parent first, but children are still running
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-1-start', 0)}---${(0, makeTimeline_1.Render)('child-2-start', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${50} ${75}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Parent should not be completed yet (waiting for children)
(0, vitest_1.expect)(reportFn).not.toHaveBeenCalled();
// Complete first child
// prettier-ignore
const { spans: child1EndSpans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('child-1-end', 0)}
Time: ${100}
`;
(0, processSpans_1.processSpans)(child1EndSpans, traceManager);
// Still waiting for second child
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(1); // Only child-1 should be reported
// Complete second child
// prettier-ignore
const { spans: child2EndSpans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('child-2-end', 0)}
Time: ${125}
`;
(0, processSpans_1.processSpans)(child2EndSpans, traceManager);
// Now all should be completed
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(3); // Both children + parent
// Verify all traces completed successfully
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should transition parent directly to complete if no children exist when entering waiting-for-children', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer that CAN adopt children but none are started
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'], // Can adopt but won't
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start only parent trace (no children)
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${50}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Parent should complete immediately since no children exist
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(1);
const parentReport = reportFn.mock.calls[0][0];
(0, vitest_1.expect)(parentReport.name).toBe('ticket.parent-operation');
(0, vitest_1.expect)(parentReport.status).toBe('ok');
(0, vitest_1.expect)(parentReport.interruption).toBeUndefined();
(0, vitest_1.expect)(parentReport.parentTraceId).toBeUndefined(); // Parent has no parent
});
(0, vitest_1.it)('should maintain parent in waiting-for-children state while any child is still running', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer with long-running requirement
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete parent but keep child running
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}---${makeTimeline_1.Check}
Time: ${0} ${25} ${50} ${1_000}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Parent should still be waiting (not reported)
(0, vitest_1.expect)(reportFn).not.toHaveBeenCalled();
// Now complete the child
// prettier-ignore
const { spans: childEndSpans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('child-end', 0)}
Time: ${6_050}
`;
(0, processSpans_1.processSpans)(childEndSpans, traceManager);
// Now both should complete
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Parent Interruption Propagation (F-4)', () => {
(0, vitest_1.it)('should interrupt all children with parent-interrupted when parent is interrupted manually', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer that can be interrupted
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
interruptOnSpans: [matchSpan.withName('interrupt')],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Interrupt parent manually
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('interrupt', 0)}
Time: ${0} ${25} ${50}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2); // Both parent and child should be interrupted
// Verify parent was interrupted
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('interrupted');
(0, vitest_1.expect)(parentReport?.interruption).toMatchObject({
reason: 'matched-on-interrupt',
});
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
// Verify child was interrupted with parent-interrupted
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(childReport).toBeDefined();
(0, vitest_1.expect)(childReport?.status).toBe('interrupted');
(0, vitest_1.expect)(childReport?.interruption).toMatchObject({
reason: 'parent-interrupted',
});
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0'); // Child was adopted by parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should interrupt all children with parent-interrupted when parent times out', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer with short timeout
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: 100 }, // Very short timeout
},
});
// Create child tracer
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---<===+125ms===>----${makeTimeline_1.Check}
Time: ${0} ${25} ${150}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2); // Both parent and child should be interrupted
// Verify parent timed out
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('interrupted');
(0, vitest_1.expect)(parentReport?.interruption).toMatchObject({ reason: 'timeout' });
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
// Verify child was interrupted with parent-interrupted
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(childReport).toBeDefined();
(0, vitest_1.expect)(childReport?.status).toBe('interrupted');
(0, vitest_1.expect)(childReport?.interruption).toMatchObject({
reason: 'parent-interrupted',
});
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0'); // Child was adopted by parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should clear children set when parent is interrupted', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
interruptOnSpans: [matchSpan.withName('interrupt')],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Verify traces are running
(0, vitest_1.expect)(traceManager.currentTraceContext).toBeDefined();
// Interrupt parent
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('interrupt', 0)}
Time: ${0} ${25} ${50}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Both traces should be interrupted and cleaned up
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
// Verify no active traces remain (memory cleaned up)
// This is a basic check - in real implementation, we'd verify children sets are cleared
(0, vitest_1.expect)(traceManager.currentTraceContext).toBeUndefined();
// Verify parentTraceId relationships
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0'); // Child was adopted by parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Child Interruption Propagation (F-5, F-6)', () => {
(0, vitest_1.it)('should interrupt parent with child-timeout when child times out', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer with short timeout
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: 100 }, // Very short timeout
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---<===+150ms===>----${makeTimeline_1.Check}
Time: ${0} ${25} ${150}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2); // Both child and parent should be interrupted
// Verify child timed out
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(childReport).toBeDefined();
(0, vitest_1.expect)(childReport?.status).toBe('interrupted');
(0, vitest_1.expect)(childReport?.interruption).toMatchObject({ reason: 'timeout' });
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0'); // Child was adopted by parent
// Verify parent was interrupted due to child timeout
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('interrupted');
(0, vitest_1.expect)(parentReport?.interruption).toMatchObject({
reason: 'child-timeout',
});
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should interrupt parent with child-interrupted when child is interrupted for other reasons', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer that can be interrupted
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
interruptOnSpans: [matchSpan.withName('child-interrupt')],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Interrupt child manually
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-interrupt', 0)}
Time: ${0} ${25} ${50}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2); // Both child and parent should be interrupted
// Verify child was interrupted
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(childReport).toBeDefined();
(0, vitest_1.expect)(childReport?.status).toBe('interrupted');
(0, vitest_1.expect)(childReport?.interruption).toMatchObject({
reason: 'matched-on-interrupt',
});
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0'); // Child was adopted by parent
// Verify parent was interrupted due to child interruption
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('interrupted');
(0, vitest_1.expect)(parentReport?.interruption).toMatchObject({
reason: 'child-interrupted',
});
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should NOT interrupt parent when child is interrupted with child-swap reason', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent and child
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
const childId = childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Modify the child tracer's definition - this should cause child-swap
childTracer.addRequirementsToCurrentTraceOnly({
additionalRequiredSpans: [{ name: 'new-requirement' }],
});
// Complete child and parent
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}--${(0, makeTimeline_1.Render)('new-requirement', 0)}
Time: ${0} ${25} ${75} ${125} ${175}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Should get reports for: new child (ok), parent (ok)
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const completedChildReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation' && call[0].status === 'ok')?.[0];
// Verify new child completed successfully
(0, vitest_1.expect)(completedChildReport?.status).toBe('ok');
(0, vitest_1.expect)(completedChildReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(completedChildReport?.parentTraceId).toBe('trace-0');
// Verify parent was NOT interrupted (continued successfully)
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined();
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should handle onChildEnd event and remove child from children set', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete child first
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}
Time: ${0} ${25} ${75}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Child should be reported
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(1);
const childReport = reportFn.mock.calls[0][0];
(0, vitest_1.expect)(childReport.name).toBe('ticket.child-operation');
(0, vitest_1.expect)(childReport.status).toBe('ok');
(0, vitest_1.expect)(childReport.parentTraceId).toBe('trace-0');
// Parent should still be waiting, not yet reported
// Now complete parent
// prettier-ignore
const { spans: parentEndSpans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${125}
`;
(0, processSpans_1.processSpans)(parentEndSpans, traceManager);
// Now parent should also be reported
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const parentReport = reportFn.mock.calls[1][0];
(0, vitest_1.expect)(parentReport.name).toBe('ticket.parent-operation');
(0, vitest_1.expect)(parentReport.status).toBe('ok');
(0, vitest_1.expect)(parentReport.parentTraceId).toBeUndefined();
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should move completed child to completedChildren set', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete child, then parent
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${75} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Both should be reported
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
// Child completed and was moved to completedChildren
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0');
// Parent completed after all children
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined();
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Span Forwarding (F-7)', () => {
(0, vitest_1.it)('should forward spans from parent to all running children after processing in parent', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent and child
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('shared-span', 50)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${75} ${125} ${175}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
// Verify both parent and child received the shared-span
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.status).toBe('ok');
// Verify parentTraceId relationships
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0'); // Child was adopted by parent
// Both should have processed the shared-span
const parentSpanNames = parentReport?.entries.map((entry) => entry.span.performanceEntry?.name);
const childSpanNames = childReport?.entries.map((entry) => entry.span.performanceEntry?.name);
(0, vitest_1.expect)(parentSpanNames).toContain('shared-span');
(0, vitest_1.expect)(childSpanNames).toContain('shared-span');
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should not forward spans to children that have already completed', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent and child
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete child first, then add span to parent, then complete parent
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('late-span', 50)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${75} ${125} ${175}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
// Parent should have the late-span
const parentSpanNames = parentReport?.entries.map((entry) => entry.span.performanceEntry?.name);
(0, vitest_1.expect)(parentSpanNames).toContain('late-span');
// Child should NOT have the late-span (it completed before the span arrived)
const childSpanNames = childReport?.entries.map((entry) => entry.span.performanceEntry?.name);
(0, vitest_1.expect)(childSpanNames).not.toContain('late-span');
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should handle span forwarding when children are in different states', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: [
'ticket.child-operation-1',
'ticket.child-operation-2',
],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const child1Tracer = traceManager.createTracer({
name: 'ticket.child-operation-1',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-1-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const child2Tracer = traceManager.createTracer({
name: 'ticket.child-operation-2',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-2-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start all traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
child1Tracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
child2Tracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete child1, then add shared span, then complete child2 and parent
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-1-start', 0)}---${(0, makeTimeline_1.Render)('child-2-start', 0)}---${(0, makeTimeline_1.Render)('child-1-end', 0)}---${(0, makeTimeline_1.Render)('shared-span', 50)}---${(0, makeTimeline_1.Render)('child-2-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${50} ${75} ${125} ${175} ${225}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(3);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const child1Report = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation-1')?.[0];
const child2Report = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation-2')?.[0];
// Parent should have shared-span
const parentSpanNames = parentReport?.entries.map((entry) => entry.span.performanceEntry?.name);
(0, vitest_1.expect)(parentSpanNames).toContain('shared-span');
// Child1 should NOT have shared-span (completed before it arrived)
const child1SpanNames = child1Report?.entries.map((entry) => entry.span.performanceEntry?.name);
(0, vitest_1.expect)(child1SpanNames).not.toContain('shared-span');
// Child2 should have shared-span (was still running when it arrived)
const child2SpanNames = child2Report?.entries.map((entry) => entry.span.performanceEntry?.name);
(0, vitest_1.expect)(child2SpanNames).toContain('shared-span');
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(child1Report?.status).toBe('ok');
(0, vitest_1.expect)(child2Report?.status).toBe('ok');
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Memory Management (F-8)', () => {
(0, vitest_1.it)('should clear children and completedChildren sets in onTerminalStateReached', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete both traces
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${75} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Both should be completed and reported
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.status).toBe('ok');
// Memory should be cleaned up - no active traces should remain
(0, vitest_1.expect)(traceManager.currentTraceContext).toBeUndefined();
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should not create memory leaks with parent-child references', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start multiple parent-child cycles to test memory cleanup
for (let i = 0; i < 3; i++) {
parentTracer.start({
relatedTo: { ticketId: `${i}` },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: `${i}` },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${i * 200} ${i * 200 + 25} ${i * 200 + 75} ${i * 200 + 125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
}
// All traces should complete successfully
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(6); // 3 parents + 3 children
// Memory should be clean - no dangling references
(0, vitest_1.expect)(traceManager.currentTraceContext).toBeUndefined();
// All reports should have correct parent-child relationships
const parentReports = reportFn.mock.calls
.map((call) => call[0])
.filter((report) => report.name === 'ticket.parent-operation');
const childReports = reportFn.mock.calls
.map((call) => call[0])
.filter((report) => report.name === 'ticket.child-operation');
(0, vitest_1.expect)(parentReports).toHaveLength(3);
(0, vitest_1.expect)(childReports).toHaveLength(3);
// Each parent should have no parentTraceId, each child should have a parentTraceId
parentReports.forEach((report) => {
(0, vitest_1.expect)(report.parentTraceId).toBeUndefined();
(0, vitest_1.expect)(report.status).toBe('ok');
});
childReports.forEach((report) => {
(0, vitest_1.expect)(report.parentTraceId).toBeDefined();
(0, vitest_1.expect)(report.status).toBe('ok');
});
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Child Utilities and Scope', () => {
(0, vitest_1.it)('should provide child-scoped utilities that return child as current trace', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent and child
const parentTraceId = parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
const childTraceId = childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// The main trace manager should still reference the parent as current
(0, vitest_1.expect)(traceManager.currentTraceContext?.input.id).toBe(parentTraceId);
// Complete both traces
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${75} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.parentTraceId).toBe(parentTraceId);
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should handle multiple children', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation', 'ticket.sibling-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const siblingTracer = traceManager.createTracer({
name: 'ticket.sibling-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'sibling-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent
const parentTraceId = parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Start child
const firstChildId = childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
const siblingId = siblingTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Parent should still be the current trace in main manager
(0, vitest_1.expect)(traceManager.currentTraceContext?.input.id).toBe(parentTraceId);
// Complete new child and parent
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}-----${(0, makeTimeline_1.Render)('sibling-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${75} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Should get: first child (ok), second child (ok), parent (ok)
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(3);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const firstChildReport = reportFn.mock.calls.find((call) => call[0].id === firstChildId)?.[0];
const siblingReport = reportFn.mock.calls.find((call) => call[0].id === siblingId)?.[0];
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(firstChildReport?.status).toBe('ok');
(0, vitest_1.expect)(siblingReport?.status).toBe('ok');
// Both children should reference parent
(0, vitest_1.expect)(firstChildReport?.parentTraceId).toBe(parentTraceId);
(0, vitest_1.expect)(siblingReport?.parentTraceId).toBe(parentTraceId);
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Edge Cases', () => {
(0, vitest_1.it)('should prevent self-nested traces (trace adopting itself)', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const name = 'ticket.self-referencing-operation';
// Create a tracer that tries to adopt itself
const selfReferencingTracer = traceManager.createTracer({
name,
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'end' }],
// Self-reference - this should either throw or be filtered out
adoptAsChildren: [name],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
(0, vitest_1.expect)(reportErrorFn).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
message: vitest_1.expect.stringContaining(`A tracer cannot adopt its own traces as children. Please remove "${name}" from the adoptAsChildren array.`),
}), vitest_1.expect.anything());
// Start first trace
const firstTraceId = selfReferencingTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Start second trace of same type - should NOT be adopted (should interrupt first one)
const secondTraceId = selfReferencingTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('start', 0)}---${(0, makeTimeline_1.Render)('end', 0)}
Time: ${0} ${50}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// Both traces should be reported as separate (not parent-child)
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const reports = reportFn.mock.calls.map((call) => call[0]);
// First should be interrupted, second should complete
const firstReport = reports.find((r) => r.id === firstTraceId);
const secondReport = reports.find((r) => r.id === secondTraceId);
(0, vitest_1.expect)(firstReport?.status).toBe('interrupted');
(0, vitest_1.expect)(firstReport?.interruption).toMatchObject({
reason: 'another-trace-started',
});
(0, vitest_1.expect)(firstReport?.parentTraceId).toBeUndefined(); // First trace has no parent
(0, vitest_1.expect)(secondReport?.status).toBe('ok');
(0, vitest_1.expect)(secondReport?.parentTraceId).toBeUndefined(); // Second trace has no parent (interrupted first)
});
(0, vitest_1.it)('should handle grandchildren (children of children)', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Parent can adopt child
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Child can adopt grandchild
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
adoptAsChildren: ['ticket.grandchild-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Grandchild
const grandchildTracer = traceManager.createTracer({
name: 'ticket.grandchild-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'grandchild-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start all traces in order
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
grandchildTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('start', 0)}---${(0, makeTimeline_1.Render)('grandchild-end', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${100} ${150} ${200}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
// All should complete successfully, though there might be more reports due to implementation details
(0, vitest_1.expect)(reportFn).toHaveBeenCalled();
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
const grandchildReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.grandchild-operation')?.[0];
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(grandchildReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(childReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(grandchildReport?.interruption).toBeUndefined();
// Verify parentTraceId hierarchy
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined(); // Parent has no parent
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0'); // Child adopted by parent
(0, vitest_1.expect)(grandchildReport?.parentTraceId).toBe('trace-1'); // Grandchild adopted by child
});
(0, vitest_1.it)('should handle complex nested hierarchies', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create a 3-level hierarchy: root -> level1 -> level2
const rootTracer = traceManager.createTracer({
name: 'ticket.root-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'root-end' }],
adoptAsChildren: ['ticket.level1-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const level1Tracer = traceManager.createTracer({
name: 'ticket.level1-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'level1-end' }],
adoptAsChildren: ['ticket.level2-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const level2Tracer = traceManager.createTracer({
name: 'ticket.level2-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'level2-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start all traces
rootTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
level1Tracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
level2Tracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete from deepest to shallowest
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('root-start', 0)}---${(0, makeTimeline_1.Render)('level1-start', 0)}---${(0, makeTimeline_1.Render)('level2-start', 0)}---${(0, makeTimeline_1.Render)('level2-end', 0)}---${(0, makeTimeline_1.Render)('level1-end', 0)}---${(0, makeTimeline_1.Render)('root-end', 0)}
Time: ${0} ${25} ${50} ${75} ${100} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(3);
const rootReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.root-operation')?.[0];
const level1Report = reportFn.mock.calls.find((call) => call[0].name === 'ticket.level1-operation')?.[0];
const level2Report = reportFn.mock.calls.find((call) => call[0].name === 'ticket.level2-operation')?.[0];
// All should complete successfully
(0, vitest_1.expect)(rootReport?.status).toBe('ok');
(0, vitest_1.expect)(level1Report?.status).toBe('ok');
(0, vitest_1.expect)(level2Report?.status).toBe('ok');
// Verify parent-child hierarchy
(0, vitest_1.expect)(rootReport?.parentTraceId).toBeUndefined(); // Root has no parent
(0, vitest_1.expect)(level1Report?.parentTraceId).toBe('trace-0'); // Level1 adopted by root
(0, vitest_1.expect)(level2Report?.parentTraceId).toBe('trace-1'); // Level2 adopted by level1
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should skip waiting-for-interactive state for children', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
// Parent uses interactive:
captureInteractive: {
heavyClusterThreshold: 100,
},
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
// Child also defines interactive, but should be ignored
captureInteractive: {},
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
startTime: { now: 100 },
});
// Complete child and parent quickly (no interactive wait)
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}--${(0, makeTimeline_1.Render)('parent-end', 0)}--${(0, makeTimeline_1.LongTask)(300)}--${(0, makeTimeline_1.Render)('child-end', 0)}--${makeTimeline_1.Check}
Time: ${0} ${100} ${125} ${200} ${220} ${5_000}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
// Both should complete successfully
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0');
(0, vitest_1.expect)(childReport?.duration).toBe(120); // child completes immediately
(0, vitest_1.expect)(childReport?.additionalDurations.startTillInteractive).toBe(null);
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined();
(0, vitest_1.expect)(parentReport?.duration).toBe(220); // parent waits for child to complete
(0, vitest_1.expect)(parentReport?.additionalDurations.startTillInteractive).toBe(500); // last long task starts at 200 + 300 duration
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should ignore captureInteractive setting for children', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
captureInteractive: true, // This should be ignored for children
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete child immediately without any interactive delay
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${75} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
// Child should complete immediately without waiting for interactive
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0');
// Parent should also complete
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(parentReport?.parentTraceId).toBeUndefined();
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Backward Compatibility (F-9)', () => {
(0, vitest_1.it)('should preserve existing single-trace behavior when adoptAsChildren is not defined', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create first tracer without adoptAsChildren
const firstTracer = traceManager.createTracer({
name: 'ticket.first-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'first-end' }],
// No adoptAsChildren defined
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create second tracer
const secondTracer = traceManager.createTracer({
name: 'ticket.second-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'second-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start first trace
const firstTraceId = firstTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
(0, vitest_1.expect)(firstTraceId).toBe('trace-0');
// Start second trace - should interrupt first (existing behavior)
const secondTraceId = secondTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
(0, vitest_1.expect)(secondTraceId).toBe('trace-1');
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('first-start', 0)}---${(0, makeTimeline_1.Render)('second-start', 0)}---${(0, makeTimeline_1.Render)('second-end', 0)}
Time: ${0} ${25} ${75}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
// Verify first trace was interrupted
const firstReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.first-operation')?.[0];
(0, vitest_1.expect)(firstReport?.status).toBe('interrupted');
(0, vitest_1.expect)(firstReport?.interruption).toMatchObject({
reason: 'another-trace-started',
});
(0, vitest_1.expect)(firstReport?.parentTraceId).toBeUndefined(); // First trace has no parent
// Verify second trace completed successfully
const secondReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.second-operation')?.[0];
(0, vitest_1.expect)(secondReport?.status).toBe('ok');
(0, vitest_1.expect)(secondReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(secondReport?.parentTraceId).toBeUndefined(); // Second trace has no parent (interrupted first)
});
(0, vitest_1.it)('should preserve existing interruption behavior when no adoption occurs', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create first tracer with adoptAsChildren but for different trace names
const firstTracer = traceManager.createTracer({
name: 'ticket.first-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'first-end' }],
adoptAsChildren: ['ticket.some-other-operation'], // Doesn't match second tracer
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create second tracer that won't be adopted
const secondTracer = traceManager.createTracer({
name: 'ticket.second-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'second-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start first trace
const firstTraceId = firstTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Start second trace - should interrupt first (no adoption)
const secondTraceId = secondTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('first-start', 0)}---${(0, makeTimeline_1.Render)('second-start', 0)}---${(0, makeTimeline_1.Render)('second-end', 0)}
Time: ${0} ${25} ${75}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
// Verify first trace was interrupted (existing behavior)
const firstReport = reportFn.mock.calls.find((call) => call[0].id === firstTraceId)?.[0];
(0, vitest_1.expect)(firstReport?.status).toBe('interrupted');
(0, vitest_1.expect)(firstReport?.interruption).toMatchObject({
reason: 'another-trace-started',
});
(0, vitest_1.expect)(firstReport?.parentTraceId).toBeUndefined();
// Verify second trace completed successfully
const secondReport = reportFn.mock.calls.find((call) => call[0].id === secondTraceId)?.[0];
(0, vitest_1.expect)(secondReport?.status).toBe('ok');
(0, vitest_1.expect)(secondReport?.interruption).toBeUndefined();
(0, vitest_1.expect)(secondReport?.parentTraceId).toBeUndefined(); // Not a child
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Integration Scenarios', () => {
(0, vitest_1.it)('should handle child adoption with debouncing', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
debounceOnSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
debounceWindow: 200,
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
debounceOnSpans: [{ name: 'child-end' }],
debounceWindow: 150,
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Create debouncing events for both parent and child
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${makeTimeline_1.Check}
Time: ${0} ${25} ${50} ${100} ${220} ${240} ${1_600}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
// Both should complete successfully with debouncing applied
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0');
// Verify debouncing was applied (duration should reflect debounced end times)
(0, vitest_1.expect)(parentReport?.duration).toBe(240); // parent-start to child-end
(0, vitest_1.expect)(childReport?.duration).toBe(240); // child-start to child-end, accounting for debounce
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should handle child adoption with computed spans', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Define computed spans for both tracers
parentTracer.defineComputedSpan({
name: 'parent-computed',
startSpan: matchSpan.withName('parent-start'),
endSpan: matchSpan.withName('parent-middle'),
});
childTracer.defineComputedSpan({
name: 'child-computed',
startSpan: matchSpan.withName('child-start'),
endSpan: matchSpan.withName('child-middle'),
});
// Start traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
startTime: { epoch: 25, now: 25 },
});
// Create spans for computed span calculation
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('parent-middle', 0)}---${(0, makeTimeline_1.Render)('child-middle', 50)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${75} ${125} ${175} ${225}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
// Both should complete successfully
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0');
// Verify computed spans were calculated
(0, vitest_1.expect)(parentReport?.computedSpans?.['parent-computed']).toBeDefined();
(0, vitest_1.expect)(parentReport?.computedSpans?.['parent-computed']?.startOffset).toBe(0);
(0, vitest_1.expect)(parentReport?.computedSpans?.['parent-computed']?.duration).toBe(75);
(0, vitest_1.expect)(childReport?.computedSpans?.['child-computed']).toBeDefined();
(0, vitest_1.expect)(childReport?.computedSpans?.['child-computed']?.startOffset).toBe(0); // Relative to child start
(0, vitest_1.expect)(childReport?.computedSpans?.['child-computed']?.duration).toBe(150);
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should handle child adoption with required spans and matching relations', () => {
// Create separate error/warning functions for the extended schema
const extendedReportErrorFn = vitest_1.vitest.fn();
const extendedReportWarningFn = vitest_1.vitest.fn();
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: relationSchemas_1.ticketAndUserAndGlobalRelationSchemasFixture,
reportFn: reportFn,
generateId,
reportErrorFn: extendedReportErrorFn,
reportWarningFn: extendedReportWarningFn,
});
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end', matchingRelations: true }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end', matchingRelations: true }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const relatedTo = {
ticketId: '123',
userId: '456',
};
// Start traces with specific relations
parentTracer.start({
relatedTo,
variant: 'default',
});
childTracer.start({
relatedTo,
variant: 'default',
});
// Create spans with matching relations
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0, { relatedTo })}---${(0, makeTimeline_1.Render)('parent-end', 0, { relatedTo })}
Time: ${0} ${25} ${75} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
// Both should complete successfully with matching relations
(0, vitest_1.expect)(parentReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.status).toBe('ok');
(0, vitest_1.expect)(childReport?.parentTraceId).toBe('trace-0');
// Verify relations are preserved
(0, vitest_1.expect)(parentReport?.relatedTo).toEqual(relatedTo);
(0, vitest_1.expect)(childReport?.relatedTo).toEqual(relatedTo);
(0, vitest_1.expect)(extendedReportErrorFn).not.toHaveBeenCalled();
});
});
(0, vitest_1.describe)('Parent receives child span', () => {
(0, vitest_1.it)('should fire a type: operation span in parent when child completes successfully', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer that can adopt children
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent and child traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${75} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2);
// Verify parent trace received the child operation span
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
// Look for the operation span in parent's entries
const operationSpan = parentReport?.entries.find((entry) => entry.span.type === 'operation');
(0, vitest_1.expect)(operationSpan).toBeDefined();
(0, vitest_1.expect)(operationSpan?.span.name).toBe('ticket.child-operation');
(0, vitest_1.expect)(operationSpan?.span.status).toBe('ok');
(0, vitest_1.expect)(typeof operationSpan?.span.duration).toBe('number');
(0, vitest_1.expect)(operationSpan?.span.duration).toBeGreaterThan(0);
// Verify the child report details are preserved in the operation span
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(childReport).toBeDefined();
(0, vitest_1.expect)(operationSpan?.span.duration).toBe(childReport?.duration);
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should NOT fire operation span in parent when child is interrupted', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer that can adopt children
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: ['ticket.child-operation'],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracer that can be interrupted
const childTracer = traceManager.createTracer({
name: 'ticket.child-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-end' }],
interruptOnSpans: [matchSpan.withName('child-interrupt')],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start parent and child traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
childTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Interrupt the child before it completes
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-start', 0)}---${(0, makeTimeline_1.Render)('child-interrupt', 0)}
Time: ${0} ${25} ${50}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(2); // Both parent and child interrupted
// Verify parent trace was interrupted (due to child interruption propagation)
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
(0, vitest_1.expect)(parentReport?.status).toBe('interrupted');
// Verify NO operation span was created in parent (child was interrupted)
const operationSpan = parentReport?.entries.find((entry) => entry.span.type === 'operation');
(0, vitest_1.expect)(operationSpan).toBeUndefined();
// Verify child was interrupted
const childReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.child-operation')?.[0];
(0, vitest_1.expect)(childReport).toBeDefined();
(0, vitest_1.expect)(childReport?.status).toBe('interrupted');
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should fire operation spans for multiple children completing', () => {
const traceManager = new TraceManager_1.TraceManager({
relationSchemas: { ticket: { ticketId: String } },
reportFn: getReportFn(),
generateId,
reportErrorFn,
reportWarningFn: reportErrorFn,
});
// Create parent tracer that can adopt multiple children
const parentTracer = traceManager.createTracer({
name: 'ticket.parent-operation',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'parent-end' }],
adoptAsChildren: [
'ticket.child-operation-1',
'ticket.child-operation-2',
],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Create child tracers
const child1Tracer = traceManager.createTracer({
name: 'ticket.child-operation-1',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-1-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
const child2Tracer = traceManager.createTracer({
name: 'ticket.child-operation-2',
type: 'operation',
relationSchemaName: 'ticket',
requiredSpans: [{ name: 'child-2-end' }],
variants: {
default: { timeout: DEFAULT_TIMEOUT_DURATION },
},
});
// Start all traces
parentTracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
child1Tracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
child2Tracer.start({
relatedTo: { ticketId: '1' },
variant: 'default',
});
// Complete all traces
// prettier-ignore
const { spans } = (0, makeTimeline_1.getSpansFromTimeline) `
Events: ${(0, makeTimeline_1.Render)('parent-start', 0)}---${(0, makeTimeline_1.Render)('child-1-start', 0)}---${(0, makeTimeline_1.Render)('child-2-start', 0)}---${(0, makeTimeline_1.Render)('child-1-end', 0)}---${(0, makeTimeline_1.Render)('child-2-end', 0)}---${(0, makeTimeline_1.Render)('parent-end', 0)}
Time: ${0} ${25} ${50} ${75} ${100} ${125}
`;
(0, processSpans_1.processSpans)(spans, traceManager);
(0, vitest_1.expect)(reportFn).toHaveBeenCalledTimes(3); // Parent + 2 children
// Verify parent trace received both child operation spans
const parentReport = reportFn.mock.calls.find((call) => call[0].name === 'ticket.parent-operation')?.[0];
(0, vitest_1.expect)(parentReport).toBeDefined();
// Look for the operation spans in parent's entries
const operationSpans = parentReport?.entries.filter((entry) => entry.span.type === 'operation');
(0, vitest_1.expect)(operationSpans).toHaveLength(2);
const child1OperationSpan = operationSpans?.find((entry) => entry.span.name === 'ticket.child-operation-1');
const child2OperationSpan = operationSpans?.find((entry) => entry.span.name === 'ticket.child-operation-2');
(0, vitest_1.expect)(child1OperationSpan).toBeDefined();
(0, vitest_1.expect)(child1OperationSpan?.span.status).toBe('ok');
(0, vitest_1.expect)(child2OperationSpan).toBeDefined();
(0, vitest_1.expect)(child2OperationSpan?.span.status).toBe('ok');
(0, vitest_1.expect)(reportErrorFn).not.toHaveBeenCalled();
});
});
});
//# sourceMappingURL=TraceManagerChildTraces.test.js.map