@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
914 lines • 56 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 });
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