@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
157 lines • 5.48 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildSpanHierarchy = buildSpanHierarchy;
exports.flattenHierarchicalSpans = flattenHierarchicalSpans;
exports.validateSpanHierarchy = validateSpanHierarchy;
exports.getDescendantSpanIds = getDescendantSpanIds;
/**
* Builds a hierarchical tree structure from flat spans using parentSpanId relationships
*/
function buildSpanHierarchy(spans) {
// Create a map for quick lookup by span id
const spanMap = new Map();
const rootSpans = [];
const orphanedSpans = [];
// First pass: Convert all spans to hierarchical format and build lookup map
for (const spAnn of spans) {
const hierarchicalSpan = {
...spAnn,
children: [],
isExpanded: false, // Default to collapsed
depth: 0, // Will be calculated later
parentId: spAnn.span.parentSpanId,
};
spanMap.set(spAnn.span.id, hierarchicalSpan);
}
// Second pass: Build parent-child relationships
for (const span of spanMap.values()) {
if (span.parentId && spanMap.has(span.parentId)) {
// This span has a valid parent
const parent = spanMap.get(span.parentId);
parent.children.push(span);
}
else if (span.parentId) {
// Parent ID exists but parent span not found - orphaned span
orphanedSpans.push(span);
}
else {
// No parent ID - root span
rootSpans.push(span);
}
}
// Third pass: Calculate depths and sort children
function calculateDepthsAndSort(spansToProcess, depth = 0) {
for (const span of spansToProcess) {
span.depth = depth;
// Sort children by start time
span.children.sort((a, b) => a.span.startTime.now - b.span.startTime.now);
// Recursively calculate depths for children
calculateDepthsAndSort(span.children, depth + 1);
}
}
// Sort root spans by start time
rootSpans.sort((a, b) => a.span.startTime.now - b.span.startTime.now);
calculateDepthsAndSort(rootSpans);
// Handle orphaned spans - add them as root spans with a warning
if (orphanedSpans.length > 0) {
// console.warn(
// `Found ${orphanedSpans.length} orphaned spans with invalid parentSpanId:`,
// orphanedSpans.map((s) => ({ id: s.span.id, parentId: s.parentId })),
// )
// Add orphaned spans as root spans
for (const orphan of orphanedSpans) {
orphan.depth = 0;
calculateDepthsAndSort([orphan]);
}
rootSpans.push(...orphanedSpans);
}
return rootSpans;
}
/**
* Flattens a hierarchical span tree into a flat array, respecting expansion states
*/
function flattenHierarchicalSpans(hierarchicalSpans, expandedSpanIds) {
const result = [];
function traverse(spans) {
for (const span of spans) {
result.push(span);
// Only include children if this span is expanded
if (span.children.length > 0 && expandedSpanIds.has(span.span.id)) {
traverse(span.children);
}
}
}
traverse(hierarchicalSpans);
return result;
}
/**
* Validates span hierarchy for circular references and other issues
*/
function validateSpanHierarchy(spans) {
const errors = [];
const visited = new Set();
const recursionStack = new Set();
// Build parent-child map
const childrenMap = new Map();
for (const span of spans) {
if (span.span.parentSpanId) {
const children = childrenMap.get(span.span.parentSpanId) ?? [];
children.push(span.span.id);
childrenMap.set(span.span.parentSpanId, children);
}
}
// Check for circular references using DFS
function detectCycle(spanId) {
if (recursionStack.has(spanId)) {
errors.push(`Circular reference detected involving span: ${spanId}`);
return true;
}
if (visited.has(spanId)) {
return false;
}
visited.add(spanId);
recursionStack.add(spanId);
const children = childrenMap.get(spanId) ?? [];
for (const childId of children) {
if (detectCycle(childId)) {
return true;
}
}
recursionStack.delete(spanId);
return false;
}
// Check all spans for cycles (not just roots, as cycles can exist anywhere)
const allSpanIds = new Set(spans.map((s) => s.span.id));
for (const span of spans) {
if (!visited.has(span.span.id)) {
detectCycle(span.span.id);
}
}
const missingParentIds = [];
// Check for invalid parent references
for (const span of spans) {
if (span.span.parentSpanId && !allSpanIds.has(span.span.parentSpanId)) {
missingParentIds.push(span.span.parentSpanId);
}
}
return {
isValid: errors.length === 0,
errors,
missingParentIds,
};
}
/**
* Gets all descendant span IDs for a given span
*/
function getDescendantSpanIds(span) {
const descendants = [];
function traverse(currentSpan) {
for (const child of currentSpan.children) {
descendants.push(child.span.id);
traverse(child);
}
}
traverse(span);
return descendants;
}
//# sourceMappingURL=buildSpanHierarchy.js.map