UNPKG

@zendesk/retrace

Version:

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

191 lines (166 loc) 5.41 kB
import type { HierarchicalSpanAndAnnotation, MappedSpanAndAnnotation, } from '../types' /** * Builds a hierarchical tree structure from flat spans using parentSpanId relationships */ export function buildSpanHierarchy( spans: MappedSpanAndAnnotation[], ): HierarchicalSpanAndAnnotation[] { // Create a map for quick lookup by span id const spanMap = new Map<string, HierarchicalSpanAndAnnotation>() const rootSpans: HierarchicalSpanAndAnnotation[] = [] const orphanedSpans: HierarchicalSpanAndAnnotation[] = [] // First pass: Convert all spans to hierarchical format and build lookup map for (const spAnn of spans) { const hierarchicalSpan: HierarchicalSpanAndAnnotation = { ...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: HierarchicalSpanAndAnnotation[], depth: number = 0, ): void { 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 */ export function flattenHierarchicalSpans( hierarchicalSpans: HierarchicalSpanAndAnnotation[], expandedSpanIds: Set<string>, ): HierarchicalSpanAndAnnotation[] { const result: HierarchicalSpanAndAnnotation[] = [] function traverse(spans: HierarchicalSpanAndAnnotation[]): void { 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 */ export function validateSpanHierarchy(spans: MappedSpanAndAnnotation[]): { isValid: boolean errors: string[] missingParentIds: string[] } { const errors: string[] = [] const visited = new Set<string>() const recursionStack = new Set<string>() // Build parent-child map const childrenMap = new Map<string, string[]>() 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: string): boolean { 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: string[] = [] // 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 */ export function getDescendantSpanIds( span: HierarchicalSpanAndAnnotation, ): string[] { const descendants: string[] = [] function traverse(currentSpan: HierarchicalSpanAndAnnotation): void { for (const child of currentSpan.children) { descendants.push(child.span.id) traverse(child) } } traverse(span) return descendants }