UNPKG

@zendesk/retrace

Version:

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

914 lines 56 kB
"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 }); exports.default = TraceManagerDebugger; const react_1 = require("react"); const React = __importStar(require("react")); const debugUtils_1 = require("./debugUtils"); const Trace_1 = require("./Trace"); const TraceManagerDebuggerStyles_1 = require("./TraceManagerDebuggerStyles"); // Constants to avoid magic numbers const MAX_STRING_LENGTH = 20; const LONG_STRING_THRESHOLD = 25; const NAME = 'Retrace Debugger'; // Helper function to organize traces into parent-child hierarchy function organizeTraces(traces) { const organized = []; const traceMap = new Map(); // Create a map for quick lookup for (const trace of traces) { traceMap.set(trace.traceId, trace); } // First pass: collect all parent traces const parentTraces = traces.filter((trace) => !trace.traceContext?.input.parentTraceId); // Recursive function to add children function addTraceWithChildren(trace) { organized.push(trace); // Find and add children const children = traces.filter((childTrace) => childTrace.traceContext?.input.parentTraceId === trace.traceId); // Sort children by start time children.sort((a, b) => a.startTime - b.startTime); for (const child of children) { addTraceWithChildren(child); } } // Sort parent traces by start time (newest first) parentTraces.sort((a, b) => b.startTime - a.startTime); // Add each parent and its children for (const parent of parentTraces) { addTraceWithChildren(parent); } return organized; } // Helper function to check if a trace is a child trace function isChildTrace(trace) { return !!trace.traceContext?.input.parentTraceId; } // Helper function to find parent trace function findParentTrace(trace, allTraces) { const parentId = trace.traceContext?.input.parentTraceId; return parentId ? allTraces.get(parentId) : undefined; } const TRACE_HISTORY_LIMIT = 15; function getFromRecord(record, key) { return record && Object.hasOwn(record, key) ? record[key] : undefined; } function TraceAttributes({ attributes, }) { if (!attributes || Object.keys(attributes).length === 0) return null; return (React.createElement("div", { className: "tmdb-section" }, React.createElement("div", { className: "tmdb-section-title" }, "Attributes"), React.createElement("div", { className: "tmdb-def-chip-container" }, Object.entries(attributes).map(([key, value]) => ( // eslint-disable-next-line @typescript-eslint/no-use-before-define React.createElement(DefinitionChip, { key: key, keyName: key, value: value, variant: "default" })))))); } function DefinitionChip({ keyName, value, variant = 'default', }) { const valueIsComplex = value !== null && (typeof value === 'object' || (typeof value === 'string' && value.length > LONG_STRING_THRESHOLD)); const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value); const needsTooltip = valueIsComplex || keyName.length + stringValue.length > MAX_STRING_LENGTH; const displayValue = stringValue.length > MAX_STRING_LENGTH ? `${stringValue.slice(0, MAX_STRING_LENGTH)}...` : stringValue; const getVariantClass = () => { switch (variant) { case 'pending': return 'tmdb-def-chip-pending'; case 'missing': return 'tmdb-def-chip-missing'; case 'success': return 'tmdb-def-chip-success'; case 'error': return 'tmdb-def-chip-error'; default: return ''; } }; const chipClassName = `tmdb-def-chip ${getVariantClass()} ${needsTooltip ? 'tmdb-def-chip-hoverable' : ''}`; const popoverId = `tooltip-${keyName}-${Math.random() .toString(36) .slice(2, 9)}`; return (React.createElement("div", { className: chipClassName }, React.createElement("button", { popoverTarget: popoverId, className: "tmdb-tooltip-trigger" }, keyName, ": ", React.createElement("span", { className: "tmdb-def-chip-value" }, displayValue)), needsTooltip && (React.createElement("div", { id: popoverId, role: "tooltip", popover: "auto", className: "tmdb-tooltip" }, typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value))))); } function RequiredSpansList({ requiredSpans, traceComplete, }) { return (React.createElement("div", { className: "tmdb-section" }, React.createElement("div", { className: "tmdb-section-title" }, "Required Spans (", requiredSpans.filter((s) => s.isMatched).length, "/", requiredSpans.length, ")"), React.createElement("div", null, requiredSpans.map((span, i) => (React.createElement("div", { key: i, className: `tmdb-required-item ${span.isMatched ? 'tmdb-required-item-matched' : 'tmdb-required-item-unmatched'}` }, React.createElement("div", { className: "tmdb-item-content" }, React.createElement("span", { className: `tmdb-matched-indicator ${span.isMatched ? 'tmdb-matched-indicator-matched' : 'tmdb-matched-indicator-unmatched'}`, title: span.isMatched ? 'Matched' : 'Pending' }), span.definition ? (React.createElement("div", { className: "tmdb-def-chip-container" }, React.createElement(DefinitionChip, { key: i, keyName: String(i), value: span.name, variant: span.isMatched ? 'default' : traceComplete ? 'missing' : 'pending' }))) : (span.name)))))))); } function RenderComputedSpan({ value }) { if (!value) return null; return (React.createElement("span", { style: { marginLeft: 'var(--tmdb-space-m)', color: 'var(--tmdb-color-link-primary)', } }, "start: ", value.startOffset.toFixed(2), "ms, duration:", ' ', value.duration.toFixed(2), "ms")); } const assignLanesToPoints = (pointsToAssign, currentScale, separationPercent) => { if (pointsToAssign.length === 0) return []; const sortedPoints = [...pointsToAssign].sort((a, b) => a.time - b.time); const assignments = []; const laneLastOccupiedX = {}; for (const currentPoint of sortedPoints) { const currentPointLeftPercent = currentPoint.time * currentScale; for (let l = 0;; l++) { const lastXInLane = laneLastOccupiedX[l]; if (lastXInLane === undefined || currentPointLeftPercent - lastXInLane >= separationPercent) { assignments.push({ pointData: currentPoint, lane: l }); laneLastOccupiedX[l] = currentPointLeftPercent; break; } } } return assignments; }; function RenderBeaconTimeline({ value, name, }) { if (!value) return null; const { firstRenderTillLoading: loading, firstRenderTillData: data, firstRenderTillContent: content, startOffset, } = value; const LABEL_ALIGN_LOW_THRESHOLD = 1; const LABEL_ALIGN_HIGH_THRESHOLD = 99; const MARKER_LINE_ALIGN_LOW_THRESHOLD = 0.1; const MARKER_LINE_ALIGN_HIGH_THRESHOLD = 99.9; const MIN_SEGMENT_WIDTH_PRODUCT_THRESHOLD = 0.001; const MIN_TEXT_SEPARATION_PERCENT = 8; const TIMELINE_MIDDLE_THRESHOLD = 50; const timePointsForDisplay = []; // Add start point with the startOffset value timePointsForDisplay.push({ name: 'start', time: 0, absoluteTime: startOffset, color: 'var(--tmdb-timeline-start-marker)', }); if (typeof loading === 'number') timePointsForDisplay.push({ name: 'loading', time: loading, absoluteTime: startOffset + loading, relativeTime: loading, previousEvent: 'start', color: 'var(--tmdb-timeline-loading-marker)', }); if (typeof data === 'number') timePointsForDisplay.push({ name: 'data', time: data, absoluteTime: startOffset + data, relativeTime: typeof loading === 'number' ? data - loading : data, previousEvent: typeof loading === 'number' ? 'loading' : 'start', color: 'var(--tmdb-timeline-data-marker)', }); if (typeof content === 'number') timePointsForDisplay.push({ name: 'content', time: content, absoluteTime: startOffset + content, relativeTime: typeof data === 'number' ? content - data : typeof loading === 'number' ? content - loading : content, previousEvent: typeof data === 'number' ? 'data' : typeof loading === 'number' ? 'loading' : 'start', color: 'var(--tmdb-timeline-content-marker)', }); const allRelevantTimes = [0, loading, data, content].filter((t) => typeof t === 'number'); const maxTime = allRelevantTimes.length > 0 ? Math.max(...allRelevantTimes) : 0; const scale = maxTime > 0 ? 100 / maxTime : 0; // Determine how many lanes we need for top and bottom areas const topPoints = timePointsForDisplay.filter((_, index) => index % 2 === 0); const bottomPoints = timePointsForDisplay.filter((_, index) => index % 2 !== 0); // Cast the TimelinePoint arrays to the type expected by assignLanesToPoints const processedTopPointsForDisplay = assignLanesToPoints(topPoints, scale, MIN_TEXT_SEPARATION_PERCENT); const processedBottomPointsForDisplay = assignLanesToPoints(bottomPoints, scale, MIN_TEXT_SEPARATION_PERCENT); const topLanes = processedTopPointsForDisplay.length > 0 ? Math.max(...processedTopPointsForDisplay.map((item) => item.lane)) + 1 : 1; const bottomLanes = processedBottomPointsForDisplay.length > 0 ? Math.max(...processedBottomPointsForDisplay.map((item) => item.lane)) + 1 : 1; // Set up the bar segments const barSegments = []; let currentSegmentTime = 0; if (typeof loading === 'number') { if (loading > currentSegmentTime) { barSegments.push({ start: currentSegmentTime, end: loading, color: 'var(--tmdb-timeline-loading-segment-bg)', key: 'segment-to-loading', }); } currentSegmentTime = Math.max(currentSegmentTime, loading); } if (typeof data === 'number') { if (data > currentSegmentTime) { barSegments.push({ start: currentSegmentTime, end: data, color: 'var(--tmdb-timeline-data-segment-bg)', key: 'segment-to-data', }); } currentSegmentTime = Math.max(currentSegmentTime, data); } if (typeof content === 'number' && content > currentSegmentTime) { barSegments.push({ start: currentSegmentTime, end: content, color: 'var(--tmdb-timeline-content-segment-bg)', key: 'segment-to-content', }); } if (barSegments.length === 0 && maxTime > 0) { let singleSegmentColor = 'var(--tmdb-timeline-default-segment-bg)'; if (typeof content === 'number' && content === maxTime) singleSegmentColor = 'var(--tmdb-timeline-content-segment-bg)'; else if (typeof data === 'number' && data === maxTime) singleSegmentColor = 'var(--tmdb-timeline-data-segment-bg)'; else if (typeof loading === 'number' && loading === maxTime) singleSegmentColor = 'var(--tmdb-timeline-loading-segment-bg)'; barSegments.push({ start: 0, end: maxTime, color: singleSegmentColor, key: 'single-segment-fallback', }); } const validBarSegments = barSegments.filter((seg) => seg.end > seg.start && (seg.end - seg.start) * scale > MIN_SEGMENT_WIDTH_PRODUCT_THRESHOLD); const uniqueTimesForLines = [ ...new Set(timePointsForDisplay.map((p) => p.time)), ].sort((a, b) => a - b); // Calculate the height for each row based on lane count const TEXT_AREA_HEIGHT = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--tmdb-timeline-text-area-height') || '22'); const TIMELINE_PADDING_BETWEEN_AREAS = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--tmdb-timeline-padding-between-areas') || '2'); const BAR_HEIGHT_VALUE = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--tmdb-timeline-bar-height') || '25'); const topAreaHeight = topLanes * TEXT_AREA_HEIGHT; const bottomAreaHeight = bottomLanes * TEXT_AREA_HEIGHT; // Function to generate display text with relative timing const getDisplayText = (point) => { if (point.name === 'start') { return `${point.name} @ ${startOffset.toFixed(0)}ms`; } if (point.relativeTime !== undefined) { return `${point.name} +${point.relativeTime.toFixed(0)}ms`; } return `${point.name} @ ${point.time.toFixed(0)}ms`; }; return (React.createElement("div", { style: { width: '100%' } }, React.createElement("div", { style: { display: 'flex', alignItems: 'center', marginBottom: '8px' } }, React.createElement("div", { className: "tmdb-render-beacon-timeline-name" }, name), React.createElement("div", { className: "tmdb-render-stats-group" }, React.createElement("span", { className: "tmdb-render-stats-label" }, "Renders"), React.createElement("span", { className: "tmdb-render-stats-value" }, value.renderCount)), React.createElement("div", { className: "tmdb-render-stats-group" }, React.createElement("span", { className: "tmdb-render-stats-label" }, "Sum of Render Durations"), React.createElement("span", { className: "tmdb-render-stats-value" }, value.sumOfRenderDurations.toFixed(0), "ms"))), React.createElement("div", { style: { display: 'flex', flexDirection: 'column', width: '100%', position: 'relative', // For absolute positioned markers } }, React.createElement("div", { style: { minHeight: topAreaHeight, width: '100%', position: 'relative', marginBottom: '2px', } }, processedTopPointsForDisplay.map(({ pointData: point, lane: currentLane }, index) => { const leftPercent = point.time * scale; // Determine text positioning based on which half of the timeline it's on let transform = 'translateX(-50%)'; if (leftPercent < LABEL_ALIGN_LOW_THRESHOLD) { transform = 'translateX(0%)'; } else if (leftPercent > LABEL_ALIGN_HIGH_THRESHOLD) { transform = 'translateX(-100%)'; } else if (leftPercent < TIMELINE_MIDDLE_THRESHOLD) { transform = 'translateX(5px)'; // Add a small offset to the right } else { transform = 'translateX(calc(-100% - 5px))'; // Offset to the left } return (React.createElement("div", { key: `${point.name}-combined-${point.time}`, className: "tmdb-timeline-point-label", style: { position: 'absolute', top: TIMELINE_PADDING_BETWEEN_AREAS + currentLane * TEXT_AREA_HEIGHT, left: `${leftPercent}%`, transform, color: point.color, lineHeight: `var(--tmdb-timeline-text-height)`, [leftPercent < TIMELINE_MIDDLE_THRESHOLD ? 'borderLeft' : 'borderRight']: `2px solid ${point.color}`, } }, getDisplayText(point))); })), React.createElement("div", { className: "tmdb-timeline-bar", style: { height: BAR_HEIGHT_VALUE, position: 'relative', // Changed from absolute to relative } }, validBarSegments.map((seg) => { const segmentWidthPercent = (seg.end - seg.start) * scale; const segmentLeftPercent = seg.start * scale; if (segmentWidthPercent <= 0) return null; return (React.createElement("div", { key: seg.key, className: "tmdb-timeline-segment", style: { left: `${segmentLeftPercent}%`, width: `${segmentWidthPercent}%`, background: seg.color, }, title: `${seg.key} (${seg.end - seg.start}ms)` })); })), React.createElement("div", { style: { minHeight: bottomAreaHeight, width: '100%', position: 'relative', marginTop: '2px', } }, processedBottomPointsForDisplay.map(({ pointData: point, lane: currentLane }, index) => { const leftPercent = point.time * scale; // Determine text positioning based on which half of the timeline it's on let transform = 'translateX(-50%)'; if (leftPercent < LABEL_ALIGN_LOW_THRESHOLD) { transform = 'translateX(0%)'; } else if (leftPercent > LABEL_ALIGN_HIGH_THRESHOLD) { transform = 'translateX(-100%)'; } else if (leftPercent < TIMELINE_MIDDLE_THRESHOLD) { transform = 'translateX(5px)'; // Add a small offset to the right } else { transform = 'translateX(calc(-100% - 5px))'; // Offset to the left } return (React.createElement("div", { key: `${point.name}-combined-${point.time}`, className: "tmdb-timeline-point-label", style: { position: 'absolute', top: TIMELINE_PADDING_BETWEEN_AREAS + currentLane * TEXT_AREA_HEIGHT, left: `${leftPercent}%`, transform, color: point.color, lineHeight: `var(--tmdb-timeline-text-height)`, [leftPercent < TIMELINE_MIDDLE_THRESHOLD ? 'borderLeft' : 'borderRight']: `2px solid ${point.color}`, } }, getDisplayText(point))); })), uniqueTimesForLines.map((timeVal) => { const pointConfig = timePointsForDisplay.find((p) => p.time === timeVal) ?? timePointsForDisplay[0]; const leftPercent = timeVal * scale; let lineLeftPositionStyle = `${leftPercent}%`; let lineTransformStyle = 'translateX(-50%)'; const markerLineWidth = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--tmdb-timeline-marker-line-width') || '2'); if (leftPercent < MARKER_LINE_ALIGN_LOW_THRESHOLD) { lineLeftPositionStyle = '0%'; lineTransformStyle = 'translateX(0)'; } else if (leftPercent > MARKER_LINE_ALIGN_HIGH_THRESHOLD) { lineLeftPositionStyle = `calc(100% - ${markerLineWidth}px)`; lineTransformStyle = 'translateX(0)'; } // Determine if this marker needs top line, bottom line, or both const pointIndex = timePointsForDisplay.findIndex((p) => p.time === timeVal); const needsTopLine = pointIndex % 2 === 0; // Even indexes (0, 2) are in top area const needsBottomLine = pointIndex % 2 !== 0; // Odd indexes (1, 3) are in bottom area return (React.createElement(React.Fragment, { key: `line-${timeVal}` }, needsTopLine && (React.createElement("div", { className: "tmdb-timeline-marker-line", style: { position: 'absolute', top: 0, bottom: 'auto', left: lineLeftPositionStyle, transform: lineTransformStyle, borderColor: pointConfig.color, height: `calc(${topAreaHeight}px + 2px)`, // Extend to timeline bar with overlap borderRight: 'none', borderBottom: 'none', borderTop: 'none', } })), needsBottomLine && (React.createElement("div", { className: "tmdb-timeline-marker-line", style: { position: 'absolute', top: `calc(${topAreaHeight}px + ${BAR_HEIGHT_VALUE}px - 2px)`, // Start slightly inside the bar bottom: 0, left: lineLeftPositionStyle, transform: lineTransformStyle, borderColor: pointConfig.color, height: `calc(${bottomAreaHeight}px + 4px)`, // Extended height to ensure it covers full area borderRight: 'none', borderBottom: 'none', borderTop: 'none', } })))); })))); } function RenderComputedRenderBeaconSpans({ computedRenderBeaconSpans, }) { return (React.createElement("div", { className: "tmdb-section" }, React.createElement("div", { className: "tmdb-section-title" }, "Computed Render Beacon Spans"), React.createElement("ul", { className: "tmdb-no-style-list" }, Object.entries(computedRenderBeaconSpans).map(([name, value]) => (React.createElement("li", { key: name, className: "tmdb-list-item", style: { display: 'block' } }, ' ', React.createElement(RenderBeaconTimeline, { value: value, name: name }))))))); } function downloadTraceRecording(recording) { try { const recordingJson = JSON.stringify(recording, null, 2); const blob = new Blob([recordingJson], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `trace-${recording.id}-${recording.name}.json`; document.body.append(a); a.click(); setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 0); } catch (error) { // eslint-disable-next-line no-console console.error('Failed to generate trace recording:', error); } } function TraceItem({ trace, isExpanded, onToggleExpand, onDismiss, isCurrentTrace = false, allTraces, }) { const [isDefinitionDetailsExpanded, setIsDefinitionDetailsExpanded] = (0, react_1.useState)(false); const canDownloadRecording = (trace.state === 'complete' || trace.state === 'interrupted') && !!trace.traceContext && !!trace.finalTransition; const isChild = isChildTrace(trace); const parentTrace = isChild ? findParentTrace(trace, allTraces) : undefined; const traceRecording = (0, react_1.useMemo)(() => { if (trace.traceContext && trace.finalTransition) { return (0, debugUtils_1.getComputedResults)(trace.traceContext, trace.finalTransition); } return undefined; }, [trace.traceContext, trace.finalTransition]); const handleDownloadClick = (e) => { e.stopPropagation(); if (traceRecording) { downloadTraceRecording(traceRecording); } }; const handleDismissClick = (e) => { e.stopPropagation(); onDismiss(); }; // Determine the appropriate border class based on trace state const getBorderClass = () => { if (isCurrentTrace) return 'tmdb-history-item-current'; if (trace.hasErrorSpan) return 'tmdb-history-item-error-border'; if (trace.state === 'complete') return 'tmdb-history-item-complete'; if (trace.state === 'interrupted') return 'tmdb-history-item-interrupted'; return 'tmdb-history-item-default'; }; return (React.createElement("div", { className: `tmdb-history-item ${getBorderClass()} ${isChild ? 'tmdb-history-item-child' : ''} ${trace.hasErrorSpan ? 'tmdb-history-item-error' : ''}`, style: { marginLeft: isChild ? 'var(--tmdb-space-xl)' : '0', position: 'relative', } }, isChild && (React.createElement("div", { className: "tmdb-child-trace-indicator", style: { position: 'absolute', left: 'calc(-1 * var(--tmdb-space-xl) + 8px)', top: '50%', transform: 'translateY(-50%)', width: 'calc(var(--tmdb-space-xl) - 16px)', height: '2px', backgroundColor: 'var(--tmdb-color-border-light)', zIndex: 1, } })), React.createElement("div", { className: `tmdb-history-header ${isExpanded ? 'tmdb-history-header-sticky' : ''}`, onClick: onToggleExpand }, React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: 'var(--tmdb-space-m)', } }, isChild && (React.createElement("span", { className: "tmdb-child-trace-badge", title: `Child trace of ${parentTrace?.traceName ?? 'unknown parent'}`, style: { fontSize: 'var(--tmdb-font-size-s)', color: 'var(--tmdb-color-text-secondary)', fontWeight: 'normal', marginRight: 'var(--tmdb-space-s)', } }, "\u21B3")), React.createElement("strong", { style: { fontSize: 'var(--tmdb-font-size-l)' } }, trace.traceName), React.createElement("span", { className: (0, TraceManagerDebuggerStyles_1.getDynamicStateStyle)(trace.state) }, trace.state), canDownloadRecording && (React.createElement("button", { className: "tmdb-button tmdb-download-button", onClick: handleDownloadClick, title: "Download trace recording as JSON" }, React.createElement("span", { className: "tmdb-download-icon" }, "\uD83D\uDD3D\u00A0JSON"))), (trace.hasErrorSpan || trace.hasSuppressedErrorSpan) && (React.createElement("span", { className: "tmdb-error-indicator", title: trace.hasSuppressedErrorSpan ? 'Suppressed error span(s) seen' : 'Error span(s) seen' }, trace.hasErrorSpan ? '🚨' : '⚠️')), trace.definitionModifications && trace.definitionModifications.length > 0 && (React.createElement("span", { className: "tmdb-definition-modified-indicator", title: "Definition modified" }, "\uD83D\uDD27"))), React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: 'var(--tmdb-space-m)', } }, React.createElement("span", { className: "tmdb-time-display" }, "(", (0, debugUtils_1.formatMs)(trace.liveDuration), ")"), React.createElement("div", { className: "tmdb-chip tmdb-id-chip", title: "Trace ID" }, trace.traceId), React.createElement("span", { className: "tmdb-time-display" }, new Date(trace.startTime).toLocaleTimeString()), React.createElement("button", { className: "tmdb-dismiss-button", onClick: handleDismissClick, title: "Dismiss this trace" }, "\u2715"))), React.createElement("div", { className: "tmdb-trace-info-row" }, React.createElement("div", { style: { display: 'flex', gap: 'var(--tmdb-space-m)', flexWrap: 'wrap', alignItems: 'center', } }, React.createElement("div", { className: "tmdb-chip-group tmdb-variant-group" }, React.createElement("span", { className: "tmdb-chip-group-label" }, "Variant"), React.createElement("span", { className: "tmdb-chip-group-value" }, trace.variant)), React.createElement("div", { className: "tmdb-chip-group tmdb-items-group" }, React.createElement("span", { className: "tmdb-chip-group-label" }, "Required"), React.createElement("span", { className: "tmdb-chip-group-value" }, trace.requiredSpans.filter((s) => s.isMatched).length, "/", trace.requiredSpans.length)), trace.relatedTo && Object.keys(trace.relatedTo).length > 0 && (React.createElement("div", { className: "tmdb-chip-group tmdb-related-group" }, React.createElement("span", { className: "tmdb-chip-group-label" }, "Related"), React.createElement("div", { className: "tmdb-related-items" }, Object.entries(trace.relatedTo).map(([key, value]) => (React.createElement("span", { key: key, className: "tmdb-related-item" }, key, ": ", JSON.stringify(value))))))), trace.interruption && (React.createElement("div", { className: "tmdb-chip-group tmdb-reason-group" }, React.createElement("span", { className: "tmdb-chip-group-label" }, "Reason"), React.createElement("span", { className: "tmdb-chip-group-value" }, trace.interruption.reason, trace.interruption.reason === 'another-trace-started' ? ` (${trace.interruption.anotherTrace.name})` : ''))), trace.lastRequiredSpanOffset !== undefined && (React.createElement("div", { className: "tmdb-chip-group tmdb-fcr-group", title: "First Contentful Render (Last Required Span)" }, React.createElement("span", { className: "tmdb-chip-group-label" }, "FCR"), React.createElement("span", { className: "tmdb-chip-group-value" }, (0, debugUtils_1.formatMs)(trace.lastRequiredSpanOffset)))), trace.completeSpanOffset !== undefined && (React.createElement("div", { className: "tmdb-chip-group tmdb-lcr-group", title: "Last Contentful Render (Trace Complete)" }, React.createElement("span", { className: "tmdb-chip-group-label" }, "LCR"), React.createElement("span", { className: "tmdb-chip-group-value" }, (0, debugUtils_1.formatMs)(trace.completeSpanOffset)))), trace.cpuIdleSpanOffset !== undefined && (React.createElement("div", { className: "tmdb-chip-group tmdb-tti-group", title: "Time To Interactive (CPU Idle Span)" }, React.createElement("span", { className: "tmdb-chip-group-label" }, "TTI"), React.createElement("span", { className: "tmdb-chip-group-value" }, (0, debugUtils_1.formatMs)(trace.cpuIdleSpanOffset)))), React.createElement("div", { className: "tmdb-chip-group tmdb-item-count-group" }, React.createElement("span", { className: "tmdb-chip-group-label" }, "Spans"), React.createElement("span", { className: "tmdb-chip-group-value" }, trace.totalSpanCount ?? 0))), React.createElement("div", { className: `tmdb-expand-arrow ${isExpanded ? 'tmdb-expand-arrow-up' : 'tmdb-expand-arrow-down'}`, onClick: onToggleExpand }, "\u25BC")), isExpanded && (React.createElement("div", { className: "tmdb-expanded-history", onClick: (e) => { void e.stopPropagation(); } }, traceRecording?.status === 'error' && traceRecording?.error && (React.createElement("div", { className: "tmdb-section tmdb-error-section" }, React.createElement("div", { className: "tmdb-section-title tmdb-error-title" }, "\uD83D\uDEA8 Error Details"), React.createElement("div", { className: "tmdb-error-content" }, React.createElement("pre", { className: "tmdb-error-text" }, traceRecording.error instanceof Error ? `${traceRecording.error.name}: ${traceRecording.error.message}\n\n${traceRecording.error.stack ?? ''}` : JSON.stringify(traceRecording.error, null, 2))))), React.createElement(TraceAttributes, { attributes: trace.attributes }), React.createElement(RequiredSpansList, { requiredSpans: trace.requiredSpans, traceComplete: trace.state === 'interrupted' }), (trace.computedValues?.length ?? 0) > 0 && (React.createElement("div", { className: "tmdb-section" }, React.createElement("div", { className: "tmdb-section-title" }, "Computed Values"), React.createElement("div", { className: "tmdb-def-chip-container" }, (trace.computedValues ?? []).map((name) => { const value = getFromRecord(traceRecording?.computedValues, name); // Determine variant and display value based on trace state and value availability let variant = 'default'; let displayValue; if (trace.state === 'complete' || trace.state === 'interrupted') { if (value !== undefined) { variant = 'success'; displayValue = value; } else { variant = 'missing'; displayValue = 'N/A'; } } else { variant = 'pending'; displayValue = 'pending'; } return (React.createElement(DefinitionChip, { key: name, keyName: name, value: displayValue, variant: variant })); })))), (trace.computedSpans?.length ?? 0) > 0 && (React.createElement("div", { className: "tmdb-section" }, React.createElement("div", { className: "tmdb-section-title" }, "Computed Spans"), React.createElement("ul", { className: "tmdb-no-style-list" }, (trace.computedSpans ?? []).map((name) => { const value = getFromRecord(traceRecording?.computedSpans, name); return (React.createElement("li", { key: name, className: "tmdb-list-item" }, name, trace.state === 'complete' || trace.state === 'interrupted' ? (value ? (React.createElement(RenderComputedSpan, { value: value })) : (React.createElement("span", { className: "tmdb-computed-item-missing" }, "missing"))) : (React.createElement("span", { className: "tmdb-computed-item-pending" }, "pending")))); })))), traceRecording?.computedRenderBeaconSpans ? (React.createElement(RenderComputedRenderBeaconSpans, { computedRenderBeaconSpans: traceRecording.computedRenderBeaconSpans })) : null, React.createElement("div", { className: "tmdb-definition-details-toggle", onClick: (e) => { e.stopPropagation(); setIsDefinitionDetailsExpanded((prev) => !prev); } }, React.createElement("span", null, isDefinitionDetailsExpanded ? '−' : '+'), "Definition Details"), isDefinitionDetailsExpanded && (React.createElement(React.Fragment, null, React.createElement("div", { className: "tmdb-section" }, React.createElement("div", { className: "tmdb-section-title" }, "Trace Definition"), React.createElement("div", { className: "tmdb-def-chip-container" }, (() => { const { timeout, debounce, interactive } = trace.traceContext ? (0, debugUtils_1.getConfigSummary)(trace.traceContext) : {}; return (React.createElement(React.Fragment, null, timeout != null && (React.createElement(DefinitionChip, { keyName: "Timeout", value: `${(0, debugUtils_1.formatMs)(timeout)}`, variant: "default" })), debounce != null && (React.createElement(DefinitionChip, { keyName: "Debounce", value: `${(0, debugUtils_1.formatMs)(debounce)}`, variant: "default" })), interactive != null && (React.createElement(DefinitionChip, { keyName: "Interactive", value: `${(0, debugUtils_1.formatMs)(interactive)}`, variant: "default" })))); })())), trace.definitionModifications && trace.definitionModifications.length > 0 && (React.createElement("div", { className: "tmdb-section" }, React.createElement("div", { className: "tmdb-section-title" }, "Trace Definition Modifications"), React.createElement("ul", { className: "tmdb-no-style-list" }, trace.definitionModifications.map((mod, i) => (React.createElement("li", { key: i, className: "tmdb-list-item" }, JSON.stringify(mod))))))))))))); } /** * A component that visualizes the current state of the TraceManager and its Traces */ // eslint-disable-next-line import/no-default-export function TraceManagerDebugger({ traceManager, float = false, traceHistoryLimit = TRACE_HISTORY_LIMIT, }) { const [, setDummyRerenderState] = (0, react_1.useState)(0); const performActionAndRerender = (0, react_1.useCallback)((action) => { action(); setDummyRerenderState((prev) => prev + 1); }, [setDummyRerenderState]); const [expandedTraceId, setExpandedTraceId] = (0, react_1.useState)(null); const tracesRef = (0, react_1.useRef)(new Map()); const removeTrace = (traceId) => { performActionAndRerender(() => { tracesRef.current.delete(traceId); }); }; const [position, setPosition] = (0, react_1.useState)({ x: 10, y: 10 }); const isDraggingRef = (0, react_1.useRef)(false); const dragOffsetRef = (0, react_1.useRef)({ x: 0, y: 0 }); const [isMinimized, setIsMinimized] = (0, react_1.useState)(false); const containerRef = (0, react_1.useRef)(null); const handleMouseDown = (e) => { if (!containerRef.current) return; e.preventDefault(); isDraggingRef.current = true; dragOffsetRef.current = { x: e.clientX - position.x, y: e.clientY - position.y, }; }; const handleMouseMove = (e) => { if (!isDraggingRef.current) return; e.preventDefault(); requestAnimationFrame(() => { setPosition({ x: e.clientX - dragOffsetRef.current.x, y: e.clientY - dragOffsetRef.current.y, }); }); }; const handleMouseUp = (e) => { if (isDraggingRef.current) { e.preventDefault(); } isDraggingRef.current = false; dragOffsetRef.current = { x: 0, y: 0 }; }; (0, react_1.useEffect)(() => { if (float) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; } return undefined; }, [float]); (0, react_1.useEffect)(() => { const schedule = (fn) => void setTimeout(fn, 0); const traceEntriesMap = new Map(); const startSub = traceManager.when('trace-start').subscribe((event) => { const trace = event.traceContext; const traceId = trace.input.id; const existingTrace = tracesRef.current.get(traceId); traceEntriesMap.set(traceId, []); const traceInfo = { traceId, traceName: trace.definition.name, variant: trace.input.variant, state: trace.stateMachine.currentState, startTime: trace.input.startTime.epoch, attributes: trace.input.attributes ? { ...trace.input.attributes } : undefined, relatedTo: trace.input.relatedTo ? { ...trace.input.relatedTo } : undefined, requiredSpans: trace.definition.requiredSpans.map((matcher, index) => { const name = (0, debugUtils_1.formatMatcher)(matcher, index); return { name, // retain previous match state if it exists // this allows us to keep the match state when the trace has its definition changed and restarts isMatched: existingTrace?.requiredSpans[index]?.isMatched ?? false, definition: matcher.fromDefinition ?? undefined, }; }), traceContext: { definition: trace.definition, input: trace.input, recordedItemsByLabel: trace.recordedItemsByLabel, recordedItems: new Map(trace.recordedItems), }, liveDuration: 0, totalSpanCount: 0, hasErrorSpan: false, hasSuppressedErrorSpan: false, definitionModifications: [], computedSpans: Object.keys(trace.definition.computedSpanDefinitions ?? {}), computedValues: Object.keys(trace.definition.computedValueDefinitions ?? {}), }; schedule(() => { performActionAndRerender(() => { tracesRef.current.set(traceId, traceInfo); // Keep only the most recent TRACE_HISTORY_LIMIT traces if (tracesRef.current.size > traceHistoryLimit) { const entries = [...tracesRef.current.entries()]; const oldestEntries = entries.slice(0, tracesRef.current.size - traceHistoryLimit); for (const [oldTraceId] of oldestEntries) { tracesRef.current.delete(oldTraceId); } } }); }); }); const stateSub = traceManager .when('state-transition') .subscribe((event) => { const trace = event.traceContext; const transition = event.stateTransition; const traceId = trace.input.id; const partialNewTrace = { traceContext: { definition: trace.definition, input: trace.input, recordedItemsByLabel: trace.recordedItemsByLabel, recordedItems: new Map(trace.recordedItems), }, state: transition.transitionToState, attributes: trace.input.attributes ? { ...trace.input.attributes } : undefined, relatedTo: trace.input.relatedTo ? { ...trace.input.relatedTo } : undefined, }; schedule(() => { performActionAndRerender(() => { const existingTrace = tracesRef.current.get(traceId); if (!existingTrace) return; const updatedTrace = { ...existingTrace, ...partialNewTrace, }; if ('interruption' in transition) { updatedTrace.interruption = transition.interruption; } if ('lastRequiredSpanAndAnnotation' in transition && transition.lastRequiredSpanAndAnnotation) { updatedTrace.lastRequiredSpanOffset = transition.lastRequiredSpanAndAnnotation.annotation.operationRelativeEndTime; } if ('completeSpanAndAnnotation' in transition && transition.completeSpanAndAnnotation) { updatedTrace.completeSpanOffset = transition.completeSpanAndAnnotation.annotation.operationRelativeEndTime; } if ('cpuIdleSpanAndAnnotation' in transition && transition.cpuIdleSpanAndAnnotation) { updatedTrace.cpuIdleSpanOffset = transition.cpuIdleSpanAndAnnotation.annotation.operationRelativeEndTime; } if ((0, Trace_1.isTerminalState)(transition.transitionToState)) { updatedTrace.finalTransition = transition; } tracesRef.current.set(traceId, updatedTrace); }); }); }); const spanSeenSub = traceManager .when('required-span-seen') .subscribe((event) => { const trace = event.traceContext; const traceId = trace.input.id; schedule(() => { performActionAndRerender(() => { const existingTrace = tracesRef.current.get(traceId); if (!existingTrace) return; const updatedRequiredSpans = [...existingTrace.requiredSpans]; const matchedSpan = event.spanAndAnnotation; trace.definition.requiredSpans.forEach((matcher, index) => { if (matcher(matchedSpan, trace)) { updatedRequiredSpans[index] = { ...updatedRequiredSpans[index], isMatched: true, }; } }); tracesRef.current.set(traceId, { ...existingTrace, requiredSpans: updatedRequiredSpans, }); }); }); }); const addSpanSub = traceManager .when('add-span-to-recording') .subscribe((event) => { const trace = event.traceContext; const traceId = trace.input.id; if (!traceEntriesMap.has(traceId)) { traceEntriesMap.set(traceId, []); } const entries = traceEntriesMap.get(traceId); entries.push(event.spanAndAnnotation); schedule(() => { performActionAndRerender(() => { const existingTrace = tracesRef.current.get(traceId); if (!existingTrace) return; const liveDuration = entries.length > 0 ? Math.round(Math.max(...entries.map((e) => e.span.startTime.epoch + e.span.duration)) - trace.input.startTime.epoch) : 0; const totalSpanCount = entries.length; const hasErrorSpan = entries.some((e) => e.span.status === 'error' && !(0, debugUtils_1.isSuppressedError)(trace, e)); const hasSuppressedErrorSpan = entries.some((e) => e.span.st