UNPKG

@zendesk/react-measure-timing-hooks

Version:

react hooks for measuring time to interactive and time to render of components

1,189 lines 62.4 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 = __importStar(require("react")); const debugUtils_1 = require("./debugUtils"); const recordingComputeUtils_1 = require("./recordingComputeUtils"); const Trace_1 = require("./Trace"); // Constants to avoid magic numbers const MAX_STRING_LENGTH = 20; const LONG_STRING_THRESHOLD = 25; const NAME = 'Retrace Debugger'; const styles = { container: { fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif', maxWidth: '800px', margin: '20px auto', padding: '20px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', backgroundColor: '#f9f9f9', }, header: { borderBottom: '1px solid #eee', paddingBottom: '15px', marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', }, title: { margin: '0', fontSize: '20px', fontWeight: 'bold', color: '#333', }, activeTrace: { padding: '20px', backgroundColor: '#fff', borderRadius: '8px', marginBottom: '20px', border: '1px solid #e0e0e0', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)', }, section: { marginBottom: '15px', }, sectionTitle: { fontWeight: 'bold', marginBottom: '10px', fontSize: '14px', color: '#555', }, statusTag: { display: 'inline-block', padding: '4px 10px', borderRadius: '12px', fontSize: '12px', fontWeight: '500', marginLeft: '10px', }, activeTag: { backgroundColor: '#e3f2fd', color: '#1565c0', }, completedTag: { backgroundColor: '#e8f5e9', color: '#2e7d32', }, interruptedTag: { backgroundColor: '#ffebee', color: '#c62828', }, draftTag: { backgroundColor: '#f5f5f5', color: '#616161', }, listItem: { padding: '5px 15px', backgroundColor: '#f5f5f5', borderRadius: '6px', marginBottom: '3px', fontSize: '13px', display: 'flex', justifyContent: 'space-between', }, requiredSpan: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 15px', backgroundColor: '#f5f5f5', borderRadius: '6px', marginBottom: '8px', fontSize: '13px', }, matched: { borderLeft: '4px solid #2e7d32', }, unmatched: { borderLeft: '4px solid #757575', }, matchedIndicator: { display: 'inline-block', width: '12px', height: '12px', borderRadius: '50%', marginRight: '10px', }, matchedDot: { backgroundColor: '#2e7d32', }, unmatchedDot: { backgroundColor: '#bdbdbd', }, noTrace: { padding: '30px', textAlign: 'center', color: '#757575', fontStyle: 'italic', backgroundColor: '#fff', borderRadius: '8px', border: '1px solid #e0e0e0', }, historyTitle: { paddingTop: '20px', marginBottom: '10px', borderTop: '1px solid #eee', fontSize: '18px', fontWeight: 'bold', color: '#333', display: 'flex', alignItems: 'center', gap: '10px', }, visualizerLink: { backgroundColor: '#1976d2', color: 'white', border: 'none', borderRadius: '4px', padding: '4px 10px', fontSize: '12px', cursor: 'pointer', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', }, historyItem: { padding: '15px', backgroundColor: '#fff', borderRadius: '8px', marginBottom: '12px', border: '1px solid #e0e0e0', cursor: 'pointer', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)', transition: 'box-shadow 0.2s ease', position: 'relative', // For positioning the arrow }, historyItemHover: { boxShadow: '0 4px 8px rgba(0, 0, 0, 0.15)', }, historyHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px', }, expandArrow: { position: 'absolute', bottom: '0px', left: '50%', transform: 'translateX(-50%)', width: '24px', height: '24px', display: 'flex', justifyContent: 'center', alignItems: 'center', transition: 'transform 0.2s ease', }, expandArrowDown: { transform: 'translateX(-50%) rotate(0deg)', }, expandArrowUp: { transform: 'translateX(-50%) rotate(180deg)', }, expandedHistory: { marginTop: '15px', paddingTop: '15px', borderTop: '1px dashed #e0e0e0', }, timeDisplay: { fontSize: '12px', color: '#666', fontWeight: '500', }, keyInfo: { display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '10px', }, traceInfoRow: { display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '10px', padding: '8px 0', }, configInfoRow: { display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '2px', fontSize: '90%', }, infoChip: { backgroundColor: '#f1f1f1', padding: '3px 10px', borderRadius: '12px', fontSize: '12px', color: '#333', border: '1px solid #e0e0e0', }, configChip: { backgroundColor: '#f8f8f8', padding: '2px 8px', borderRadius: '10px', fontSize: '11px', color: '#555', border: '1px solid #e8e8e8', }, idChip: { backgroundColor: '#f5f5f5', color: '#757575', padding: '3px 10px', borderRadius: '12px', fontSize: '11px', border: '1px solid #e0e0e0', }, variantChip: { backgroundColor: '#e8f5e9', color: '#2e7d32', padding: '3px 10px', borderRadius: '12px', fontSize: '12px', border: '1px solid #c8e6c9', fontWeight: '500', }, reasonChip: { backgroundColor: '#ffebee', color: '#c62828', padding: '3px 10px', borderRadius: '12px', fontSize: '12px', border: '1px solid #ffcdd2', fontWeight: '500', }, relatedChip: { backgroundColor: '#e3f2fd', color: '#1565c0', padding: '3px 10px', borderRadius: '12px', fontSize: '12px', border: '1px solid #bbdefb', }, relatedGroup: { display: 'inline-flex', flexWrap: 'nowrap', overflow: 'hidden', borderRadius: '12px', border: '1px solid #bbdefb', backgroundColor: '#e3f2fd', }, relatedLabel: { color: '#1565c0', padding: '3px 8px', fontSize: '12px', }, relatedItems: { backgroundColor: '#1565c0', display: 'flex', gap: '2px', padding: '0 6px', }, relatedItem: { backgroundColor: '#1565c0', color: 'white', padding: '3px 4px', fontSize: '12px', }, // Variant chip group variantGroup: { display: 'inline-flex', flexWrap: 'nowrap', overflow: 'hidden', borderRadius: '12px', border: '1px solid #c8e6c9', backgroundColor: '#e8f5e9', }, variantLabel: { color: '#2e7d32', padding: '3px 8px', fontSize: '12px', }, variantValue: { backgroundColor: '#2e7d32', color: 'white', padding: '3px 8px', fontSize: '12px', fontWeight: '500', }, // Required spans chip group spansGroup: { display: 'inline-flex', flexWrap: 'nowrap', overflow: 'hidden', borderRadius: '12px', border: '1px solid #ffe0b2', backgroundColor: '#fff3e0', }, spansLabel: { color: '#e65100', padding: '3px 8px', fontSize: '12px', }, spansValue: { backgroundColor: '#e65100', color: 'white', padding: '3px 8px', fontSize: '12px', fontWeight: '500', }, // Interruption reason chip group reasonGroup: { display: 'inline-flex', flexWrap: 'nowrap', overflow: 'hidden', borderRadius: '12px', border: '1px solid #ffcdd2', backgroundColor: '#ffebee', }, reasonLabel: { color: '#c62828', padding: '3px 8px', fontSize: '12px', }, reasonValue: { backgroundColor: '#c62828', color: 'white', padding: '3px 8px', fontSize: '12px', fontWeight: '500', }, spansChip: { backgroundColor: '#fff3e0', color: '#e65100', padding: '3px 10px', borderRadius: '12px', fontSize: '12px', border: '1px solid #ffe0b2', fontWeight: '500', }, preWrap: { whiteSpace: 'pre-wrap', fontSize: '12px', backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '6px', overflowX: 'auto', maxHeight: '200px', border: '1px solid #e0e0e0', }, timeMarkerValue: { fontFamily: 'monospace', textAlign: 'right', display: 'inline-block', width: '80px', fontWeight: '500', }, floatingContainer: { position: 'fixed', top: '10px', right: '10px', minWidth: '600px', maxWidth: '750px', width: '100%', zIndex: 1_000, resize: 'both', overflow: 'auto', maxHeight: '90vh', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.15)', }, handle: { padding: '12px 15px', backgroundColor: '#2a2a2a', color: 'white', cursor: 'move', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTopLeftRadius: '8px', borderTopRightRadius: '8px', }, handleTitle: { margin: 0, fontSize: '14px', fontWeight: 'bold', }, closeButton: { background: 'none', border: 'none', color: 'white', cursor: 'pointer', fontSize: '18px', padding: '2px 8px', }, minimizedButton: { position: 'fixed', bottom: '20px', right: '20px', backgroundColor: '#1565c0', color: 'white', border: 'none', borderRadius: '50%', width: '74px', height: '74px', opacity: 0.9, display: 'flex', justifyContent: 'center', alignItems: 'center', cursor: 'pointer', boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)', zIndex: 1_000, fontSize: '14px', fontWeight: 'bold', }, defChipContainer: { display: 'flex', flexWrap: 'wrap', gap: '5px', alignItems: 'center', }, defChip: { backgroundColor: '#e3f2fd', color: '#1565c0', padding: '2px 8px', borderRadius: '10px', fontSize: '11px', maxWidth: '200px', textOverflow: 'ellipsis', whiteSpace: 'nowrap', position: 'relative', border: '1px solid #90caf9', }, defChipValue: { fontWeight: 'bold', }, defChipTooltip: { position: 'absolute', bottom: '100%', left: '50%', transform: 'translateX(-50%)', backgroundColor: 'rgba(0,0,0,0.8)', color: '#fff', padding: '5px 10px', borderRadius: '4px', fontSize: '11px', // eslint-disable-next-line no-magic-numbers zIndex: 1_001, minWidth: '200px', maxWidth: '300px', wordBreak: 'break-word', pointerEvents: 'none', opacity: 0, transition: 'opacity 0.3s ease', visibility: 'hidden', }, defChipHoverable: { cursor: 'help', }, defChipHoverVisible: { opacity: 1, visibility: 'visible', }, spanContent: { display: 'flex', alignItems: 'center', flex: 1, }, downloadButton: { backgroundColor: '#1976d2', color: 'white', border: 'none', borderRadius: '4px', padding: '4px 10px', fontSize: '12px', cursor: 'pointer', alignItems: 'center', marginLeft: '10px', }, downloadIcon: { fontSize: '14px', }, // Render stats chip group renderStatsGroup: { display: 'inline-flex', flexWrap: 'nowrap', overflow: 'hidden', borderRadius: '12px', border: '1px solid #e0e0e0', // Neutral border backgroundColor: '#f5f5f5', // Neutral background marginRight: '8px', // Add some margin if there are multiple groups }, renderStatsLabel: { color: '#555', // Darker grey for label padding: '3px 8px', fontSize: '12px', }, renderStatsValue: { backgroundColor: '#e0e0e0', // Slightly darker background for value color: '#333', // Black for value text padding: '3px 8px', fontSize: '12px', fontWeight: '500', }, }; function getStateStyle(state) { if (state === 'complete') return styles.completedTag; if (state === 'interrupted') return styles.interruptedTag; if (state === 'draft') return styles.draftTag; return styles.activeTag; } const TRACE_HISTORY_LIMIT = 15; // Helper to safely get a value from a possibly empty object function getFromRecord(record, key) { return record && Object.hasOwn(record, key) ? record[key] : undefined; } // TraceAttributes component to display attributes as chips function TraceAttributes({ attributes, }) { if (!attributes || Object.keys(attributes).length === 0) return null; return (react_1.default.createElement("div", { style: styles.section }, react_1.default.createElement("div", { style: styles.sectionTitle }, "Attributes:"), react_1.default.createElement("div", { style: styles.defChipContainer }, Object.entries(attributes).map(([key, value]) => ( // eslint-disable-next-line @typescript-eslint/no-use-before-define react_1.default.createElement(DefinitionChip, { key: key, keyName: key, value: value })))))); } // TimeMarkers component to display time markers // eslint-disable-next-line @typescript-eslint/no-unused-vars function TimeMarkers({ lastRequiredSpanOffset, completeSpanOffset, cpuIdleSpanOffset, }) { if (lastRequiredSpanOffset === undefined && completeSpanOffset === undefined && cpuIdleSpanOffset === undefined) { return null; } return (react_1.default.createElement("div", { style: styles.section }, react_1.default.createElement("div", { style: styles.sectionTitle }, "Time Markers:"), react_1.default.createElement("ul", { style: { listStyle: 'none', margin: 0, padding: 0 } }, lastRequiredSpanOffset !== undefined && (react_1.default.createElement("li", { style: styles.listItem }, react_1.default.createElement("span", { style: { display: 'inline-block' } }, "First Contentful Render (Last Required Span):"), react_1.default.createElement("span", { style: { fontFamily: 'monospace', textAlign: 'right', display: 'inline-block', width: '80px', fontWeight: '500', } }, "+", (0, debugUtils_1.formatMs)(lastRequiredSpanOffset)))), completeSpanOffset !== undefined && (react_1.default.createElement("li", { style: styles.listItem }, react_1.default.createElement("span", { style: { display: 'inline-block' } }, "Last Contentful Render (Trace Complete):"), react_1.default.createElement("span", { style: { fontFamily: 'monospace', textAlign: 'right', display: 'inline-block', width: '80px', fontWeight: '500', } }, "+", (0, debugUtils_1.formatMs)(completeSpanOffset)))), cpuIdleSpanOffset !== undefined && (react_1.default.createElement("li", { style: styles.listItem }, react_1.default.createElement("span", { style: { display: 'inline-block' } }, "Time To Interactive (CPU Idle Span):"), react_1.default.createElement("span", { style: { fontFamily: 'monospace', textAlign: 'right', display: 'inline-block', width: '80px', fontWeight: '500', } }, "+", (0, debugUtils_1.formatMs)(cpuIdleSpanOffset))))))); } // DefinitionChip component for displaying matcher definition key/value pairs function DefinitionChip({ keyName, value, }) { const [showTooltip, setShowTooltip] = (0, react_1.useState)(false); 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; // Create truncated display value const displayValue = stringValue.length > MAX_STRING_LENGTH ? `${stringValue.slice(0, MAX_STRING_LENGTH)}...` : stringValue; return (react_1.default.createElement("div", { style: { ...styles.defChip, ...(needsTooltip ? styles.defChipHoverable : {}), }, onMouseEnter: () => void setShowTooltip(true), onMouseLeave: () => void setShowTooltip(false) }, keyName, ": ", react_1.default.createElement("span", { style: styles.defChipValue }, displayValue), needsTooltip && (react_1.default.createElement("div", { style: { ...styles.defChipTooltip, opacity: showTooltip ? 1 : 0, visibility: showTooltip ? 'visible' : 'hidden', } }, typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value))))); } // RequiredSpansList component to display required spans // eslint-disable-next-line @typescript-eslint/no-unused-vars function RequiredSpansList({ requiredSpans, }) { return (react_1.default.createElement("div", { style: styles.section }, react_1.default.createElement("div", { style: styles.sectionTitle }, "Required Spans (", requiredSpans.filter((s) => s.isMatched).length, "/", requiredSpans.length, "):"), react_1.default.createElement("div", null, requiredSpans.map((span, i) => (react_1.default.createElement("div", { key: i, style: { ...styles.requiredSpan, ...(span.isMatched ? styles.matched : styles.unmatched), } }, react_1.default.createElement("div", { style: styles.spanContent }, react_1.default.createElement("span", { style: { ...styles.matchedIndicator, ...(span.isMatched ? styles.matchedDot : styles.unmatchedDot), }, title: span.isMatched ? 'Matched' : 'Pending' }), span.definition ? (react_1.default.createElement("div", { style: styles.defChipContainer }, Object.entries(span.definition).map(([key, value]) => (react_1.default.createElement(DefinitionChip, { key: key, keyName: key, value: value }))))) : (span.name)))))))); } // Helper to render ComputedSpan nicely function RenderComputedSpan({ value }) { if (!value) return null; return (react_1.default.createElement("span", { style: { marginLeft: 8, color: '#1976d2' } }, "start: ", value.startOffset.toFixed(2), "ms, duration:", ' ', value.duration.toFixed(2), "ms")); } // Helper function to assign lanes to points to prevent text overlap const assignLanesToPoints = (pointsToAssign, currentScale, separationPercent) => { if (pointsToAssign.length === 0) return []; const sortedPoints = [...pointsToAssign].sort((a, b) => a.time - b.time); const assignments = []; // For each lane, stores the `leftPercent` of the last point added to it. const laneLastOccupiedX = {}; for (const currentPoint of sortedPoints) { const currentPointLeftPercent = currentPoint.time * currentScale; for (let l = 0;; l++) { // Iterate through lanes 0, 1, 2... const lastXInLane = laneLastOccupiedX[l]; if (lastXInLane === undefined || currentPointLeftPercent - lastXInLane >= separationPercent) { // This point can fit in this lane (or it's a new lane) assignments.push({ pointData: currentPoint, lane: l }); laneLastOccupiedX[l] = currentPointLeftPercent; break; // Move to the next point } } } return assignments; // This array is sorted by time due to the initial sort. }; // Visual timeline for ComputedRenderSpan (like the provided sketch) function RenderBeaconTimeline({ value, name, }) { if (!value) return null; // Extract times (all relative to start) const { firstRenderTillLoading: loading, firstRenderTillData: data, firstRenderTillContent: content, } = value; // --- Base Constants --- const BAR_HEIGHT = 25; const TEXT_AREA_HEIGHT = 22; // Height per lane of text const MARKER_LINE_WIDTH = 2; const VERTICAL_PADDING_BETWEEN_AREAS = 2; // Thresholds for individual label/line alignment (percentages) 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; // Min horizontal separation (%) for text in the same lane // --- Prepare Data for Rendering --- const timePointsForDisplay = []; timePointsForDisplay.push({ name: 'start', time: 0, color: '#757575' }); if (typeof loading === 'number') timePointsForDisplay.push({ name: 'loading', time: loading, color: '#ff9800', }); if (typeof data === 'number') timePointsForDisplay.push({ name: 'data', time: data, color: '#1976d2' }); if (typeof content === 'number') timePointsForDisplay.push({ name: 'content', time: content, color: '#2e7d32', }); 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; // Assign lanes to points const processedPointsForDisplay = assignLanesToPoints(timePointsForDisplay, scale, MIN_TEXT_SEPARATION_PERCENT); const numLanes = processedPointsForDisplay.length > 0 ? Math.max(...processedPointsForDisplay.map((item) => item.lane)) + 1 : 1; // --- Dynamic Height and Offset Calculations based on lanes --- const TOTAL_LABEL_AREA_HEIGHT = numLanes * TEXT_AREA_HEIGHT; const TOTAL_TIME_VALUE_AREA_HEIGHT = numLanes * TEXT_AREA_HEIGHT; // Assuming same number of lanes for times const TOTAL_VIS_CONTENT_HEIGHT = TOTAL_LABEL_AREA_HEIGHT + VERTICAL_PADDING_BETWEEN_AREAS + BAR_HEIGHT + VERTICAL_PADDING_BETWEEN_AREAS + TOTAL_TIME_VALUE_AREA_HEIGHT; const BAR_TOP_OFFSET = TOTAL_LABEL_AREA_HEIGHT + VERTICAL_PADDING_BETWEEN_AREAS; const TIME_VALUES_AREA_TOP = BAR_TOP_OFFSET + BAR_HEIGHT + VERTICAL_PADDING_BETWEEN_AREAS; // Bar Segments (same logic as before) const barSegments = []; let currentSegmentTime = 0; if (typeof loading === 'number') { if (loading > currentSegmentTime) { barSegments.push({ start: currentSegmentTime, end: loading, color: '#fff176', key: 'segment-to-loading', }); } currentSegmentTime = Math.max(currentSegmentTime, loading); } if (typeof data === 'number') { if (data > currentSegmentTime) { barSegments.push({ start: currentSegmentTime, end: data, color: '#90caf9', key: 'segment-to-data', }); } currentSegmentTime = Math.max(currentSegmentTime, data); } if (typeof content === 'number' && content > currentSegmentTime) { barSegments.push({ start: currentSegmentTime, end: content, color: '#a5d6a7', key: 'segment-to-content', }); } if (barSegments.length === 0 && maxTime > 0) { let singleSegmentColor = '#e0e0e0'; if (typeof content === 'number' && content === maxTime) singleSegmentColor = '#a5d6a7'; else if (typeof data === 'number' && data === maxTime) singleSegmentColor = '#90caf9'; else if (typeof loading === 'number' && loading === maxTime) singleSegmentColor = '#fff176'; 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); // Unique times for vertical lines (based on original, unsorted list for consistency if order mattered) const uniqueTimesForLines = [ ...new Set(timePointsForDisplay.map((p) => p.time)), ].sort((a, b) => a - b); return (react_1.default.createElement("div", { style: { width: '100%' } }, react_1.default.createElement("div", { style: { display: 'flex', alignItems: 'center' } }, react_1.default.createElement("div", { style: { fontWeight: 600, fontSize: 16, marginRight: '10px' } }, name), react_1.default.createElement("div", { style: styles.renderStatsGroup }, react_1.default.createElement("span", { style: styles.renderStatsLabel }, "Renders"), react_1.default.createElement("span", { style: styles.renderStatsValue }, value.renderCount)), react_1.default.createElement("div", { style: styles.renderStatsGroup }, react_1.default.createElement("span", { style: styles.renderStatsLabel }, "Duration"), react_1.default.createElement("span", { style: styles.renderStatsValue }, value.sumOfRenderDurations.toFixed(0), "ms"))), react_1.default.createElement("div", { style: { position: 'relative', width: '100%', height: TOTAL_VIS_CONTENT_HEIGHT, } }, react_1.default.createElement("div", { style: { position: 'absolute', top: 0, width: '100%', height: TOTAL_LABEL_AREA_HEIGHT, // Dynamic height zIndex: 2, } }, processedPointsForDisplay.map(({ pointData: point, lane: currentLane }) => { const leftPercent = point.time * scale; let transform = 'translateX(-50%)'; if (leftPercent < LABEL_ALIGN_LOW_THRESHOLD) transform = 'translateX(0%)'; else if (leftPercent > LABEL_ALIGN_HIGH_THRESHOLD) transform = 'translateX(-100%)'; return (react_1.default.createElement("div", { key: `${point.name}-label-${point.time}`, style: { position: 'absolute', top: currentLane * TEXT_AREA_HEIGHT, // Position based on lane left: `${leftPercent}%`, transform, fontSize: 11, color: point.color, whiteSpace: 'nowrap', lineHeight: `${TEXT_AREA_HEIGHT}px`, padding: '0 5px', } }, point.name)); })), react_1.default.createElement("div", { style: { position: 'absolute', top: BAR_TOP_OFFSET, // Dynamic offset width: '100%', height: BAR_HEIGHT, borderRadius: 3, background: '#e0e0e0', boxSizing: 'border-box', zIndex: 1, display: 'flex', } }, validBarSegments.map((seg) => { const segmentWidthPercent = (seg.end - seg.start) * scale; const segmentLeftPercent = seg.start * scale; if (segmentWidthPercent <= 0) return null; return (react_1.default.createElement("div", { key: seg.key, style: { position: 'absolute', left: `${segmentLeftPercent}%`, width: `${segmentWidthPercent}%`, height: '100%', background: seg.color, } })); })), react_1.default.createElement("div", { style: { position: 'absolute', top: TIME_VALUES_AREA_TOP, // Dynamic offset width: '100%', height: TOTAL_TIME_VALUE_AREA_HEIGHT, // Dynamic height zIndex: 2, } }, processedPointsForDisplay.map(({ pointData: point, lane: currentLane }) => { if (point.name === 'start' && point.time === 0) return null; const leftPercent = point.time * scale; let transform = 'translateX(-50%)'; if (leftPercent < LABEL_ALIGN_LOW_THRESHOLD) transform = 'translateX(0%)'; else if (leftPercent > LABEL_ALIGN_HIGH_THRESHOLD) transform = 'translateX(-100%)'; return (react_1.default.createElement("div", { key: `${point.name}-time-${point.time}`, style: { position: 'absolute', top: currentLane * TEXT_AREA_HEIGHT, // Position based on lane left: `${leftPercent}%`, transform, fontSize: 11, color: point.color, whiteSpace: 'nowrap', lineHeight: `${TEXT_AREA_HEIGHT}px`, padding: '0 5px', } }, "+", point.time.toFixed(0), "ms")); })), uniqueTimesForLines.map((timeVal) => { // Find any point config for color, preferably from original list for consistency const pointConfig = timePointsForDisplay.find((p) => p.time === timeVal) ?? timePointsForDisplay[0]; const leftPercent = timeVal * scale; let lineLeftPositionStyle = `${leftPercent}%`; let lineTransformStyle = 'translateX(-50%)'; if (leftPercent < MARKER_LINE_ALIGN_LOW_THRESHOLD) { lineLeftPositionStyle = '0%'; lineTransformStyle = 'translateX(0)'; } else if (leftPercent > MARKER_LINE_ALIGN_HIGH_THRESHOLD) { lineLeftPositionStyle = `calc(100% - ${MARKER_LINE_WIDTH}px)`; lineTransformStyle = 'translateX(0)'; } return (react_1.default.createElement("div", { key: `line-${timeVal}`, style: { position: 'absolute', left: lineLeftPositionStyle, transform: lineTransformStyle, top: 0, width: MARKER_LINE_WIDTH, background: pointConfig.color, height: '100%', // Spans full TOTAL_VIS_CONTENT_HEIGHT zIndex: 0, } })); })))); } // Helper to render ComputedRenderBeaconSpans nicely function RenderComputedRenderBeaconSpans({ computedRenderBeaconSpans, }) { // if (Object.keys(computedRenderBeaconSpans).length === 0) return null return (react_1.default.createElement("div", { style: styles.section }, react_1.default.createElement("div", { style: styles.sectionTitle }, "Computed Render Beacon Spans:"), react_1.default.createElement("ul", { style: { listStyle: 'none', margin: 0, padding: 0 } }, Object.entries(computedRenderBeaconSpans).map(([name, value]) => (react_1.default.createElement("li", { key: name, style: styles.listItem }, react_1.default.createElement(RenderBeaconTimeline, { value: value, name: name }))))))); } // Function to download trace recording as a JSON file function downloadTraceRecording(trace) { if (!trace.traceContext || !trace.finalTransition) { return; } try { // Generate the trace recording const recording = (0, recordingComputeUtils_1.createTraceRecording)(trace.traceContext, trace.finalTransition); // Create a blob with the JSON data const recordingJson = JSON.stringify(recording, null, 2); const blob = new Blob([recordingJson], { type: 'application/json' }); // Create download link const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `trace-${trace.traceId}-${trace.traceName}.json`; document.body.append(a); a.click(); // Clean up setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 0); } catch (error) { // eslint-disable-next-line no-console console.error('Failed to generate trace recording:', error); } } // TraceItem component to display a trace (used for both active and history traces) function TraceItem({ trace, isExpanded, onToggleExpand, isCurrentTrace = false, }) { const [isHovered, setIsHovered] = (0, react_1.useState)(false); const [isDefinitionDetailsExpanded, setIsDefinitionDetailsExpanded] = (0, react_1.useState)(false); // Determine if we can download a trace recording (only for completed/interrupted traces) const canDownloadRecording = (trace.state === 'complete' || trace.state === 'interrupted') && !!trace.traceContext && !!trace.finalTransition; // Memoize computed results for this trace const computedResults = (0, react_1.useMemo)(() => { if (trace.traceContext && trace.finalTransition) { const results = (0, debugUtils_1.getComputedResults)(trace.traceContext, trace.finalTransition); return results; } return {}; }, [trace.traceContext, trace.finalTransition]); // Handle download button click without triggering the expand/collapse const handleDownloadClick = (e) => { e.stopPropagation(); downloadTraceRecording(trace); }; return (react_1.default.createElement("div", { style: { ...styles.historyItem, ...(isHovered ? styles.historyItemHover : {}), borderLeft: '3px solid', borderLeftColor: isCurrentTrace ? '#1565c0' : trace.state === 'complete' ? '#2e7d32' : trace.state === 'interrupted' ? '#c62828' : '#e0e0e0', }, onMouseEnter: () => void setIsHovered(true), onMouseLeave: () => void setIsHovered(false) }, react_1.default.createElement("div", { style: styles.historyHeader, onClick: onToggleExpand }, react_1.default.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, react_1.default.createElement("strong", { style: { fontSize: '15px' } }, trace.traceName), react_1.default.createElement("span", { style: { ...styles.statusTag, ...getStateStyle(trace.state), } }, trace.state), canDownloadRecording && (react_1.default.createElement("button", { style: styles.downloadButton, onClick: handleDownloadClick, title: "Download trace recording as JSON" }, react_1.default.createElement("span", { style: styles.downloadIcon }, "\uD83D\uDD3D JSON"))), (trace.hasErrorSpan || trace.hasSuppressedErrorSpan) && (react_1.default.createElement("span", { title: trace.hasSuppressedErrorSpan ? 'Suppressed error span(s) seen' : 'Error span(s) seen', style: { color: '#c62828', fontSize: '18px', } }, "\u26A0\uFE0F")), trace.definitionModifications && trace.definitionModifications.length > 0 && (react_1.default.createElement("span", { title: "Definition modified", style: { color: '#1976d2', fontSize: '18px', } }, "\uD83D\uDD27"))), react_1.default.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, react_1.default.createElement("span", { style: styles.timeDisplay }, "(", (0, debugUtils_1.formatMs)(trace.liveDuration), ")"), react_1.default.createElement("div", { style: styles.idChip, title: "Trace ID" }, trace.traceId), react_1.default.createElement("span", { style: styles.timeDisplay }, new Date(trace.startTime).toLocaleTimeString()))), react_1.default.createElement("div", { style: styles.traceInfoRow }, react_1.default.createElement("div", { style: styles.variantGroup }, react_1.default.createElement("span", { style: styles.variantLabel }, "Variant"), react_1.default.createElement("span", { style: styles.variantValue }, trace.variant)), react_1.default.createElement("div", { style: styles.spansGroup }, react_1.default.createElement("span", { style: styles.spansLabel }, "Required"), react_1.default.createElement("span", { style: styles.spansValue }, trace.requiredSpans.filter((s) => s.isMatched).length, "/", trace.requiredSpans.length)), trace.relatedTo && Object.keys(trace.relatedTo).length > 0 && (react_1.default.createElement("div", { style: styles.relatedGroup }, react_1.default.createElement("span", { style: styles.relatedLabel }, "Related"), react_1.default.createElement("div", { style: styles.relatedItems }, Object.entries(trace.relatedTo).map(([key, value]) => (react_1.default.createElement("span", { key: key, style: styles.relatedItem }, key, ": ", JSON.stringify(value))))))), trace.interruptionReason && (react_1.default.createElement("div", { style: styles.reasonGroup }, react_1.default.createElement("span", { style: styles.reasonLabel }, "Reason"), react_1.default.createElement("span", { style: styles.reasonValue }, trace.interruptionReason))), react_1.default.createElement("span", { style: styles.infoChip }, "Spans: ", trace.totalSpanCount ?? 0)), ' ', isExpanded && (react_1.default.createElement("div", { style: styles.expandedHistory, onClick: (e) => { void e.stopPropagation(); } }, react_1.default.createElement(TraceAttributes, { attributes: trace.attributes }), react_1.default.createElement(RequiredSpansList, { requiredSpans: trace.requiredSpans }), react_1.default.createElement(TimeMarkers, { lastRequiredSpanOffset: trace.lastRequiredSpanOffset, completeSpanOffset: trace.completeSpanOffset, cpuIdleSpanOffset: trace.cpuIdleSpanOffset }), (trace.computedSpans?.length ?? 0) > 0 && (react_1.default.createElement("div", { style: styles.section }, react_1.default.createElement("div", { style: styles.sectionTitle }, "Computed Spans:"), react_1.default.createElement("ul", { style: { listStyle: 'none', margin: 0, padding: 0 } }, (trace.computedSpans ?? []).map((name) => { const value = getFromRecord(computedResults.computedSpans, name); return (react_1.default.createElement("li", { key: name, style: styles.listItem }, name, trace.state === 'complete' || trace.state === 'interrupted' ? (value ? (react_1.default.createElement(RenderComputedSpan, { value: value })) : (react_1.default.createElement("span", { style: { marginLeft: 8, color: 'red', fontWeight: 500, } }, "missing"))) : (react_1.default.createElement("span", { style: { marginLeft: 8, color: '#757575', fontStyle: 'italic', } }, "pending")))); })))), computedResults.computedRenderBeaconSpans ? (react_1.default.createElement(RenderComputedRenderBeaconSpans, { computedRenderBeaconSpans: computedResults.computedRenderBeaconSpans })) : null, (trace.computedValues?.length ?? 0) > 0 && (react_1.default.createElement("div", { style: styles.section }, react_1.default.createElement("div", { style: styles.sectionTitle }, "Computed Values:"), react_1.default.createElement("ul", { style: { listStyle: 'none', margin: 0, padding: 0 } }, (trace.computedValues ?? []).map((name) => { const value = getFromRecord(computedResults.computedValues, name); return (react_1.default.createElement("li", { key: name, style: styles.listItem }, name, trace.state === 'complete' || trace.state === 'interrupted' ? (value !== undefined ? (react_1.default.createElement("span", { style: { marginLeft: 8, color: '#1976d2' } }, String(value))) : (react_1.default.createElement("span", { style: { marginLeft: 8, color: 'red', fontWeight: 500, } }, "N/A"))) : (react_1.default.createElement("span", { style: { marginLeft: 8, color: '#757575', fontStyle: 'italic', } }, "pending")))); })))), react_1.default.createElement("div", { style: { display: 'flex', alignItems: 'center', cursor: 'pointer', marginTop: '8px', fontSize: '12px', color: '#555', }, onClick: (e) => { e.stopPropagation(); setIsDefinitionDetailsExpanded((prev) => !prev); } }, react_1.default.createElement("span", { style: { marginRight: '5px' } }, isDefinitionDetailsExpanded ? '−' : '+', " Definition Details")), isDefinitionDetailsExpanded && (react_1.default.createElement("div", { style: styles.section }, react_1.default.createElement("div", { style: styles.sectionTitle }, "Configuration:"), react_1.default.createElement("div", { style: styles.configInfoRow }, (() => { const { timeout, debounce, interactive } = trace.traceContext ? (0, debugUtils_1.getConfigSummary)(trace.traceContext) : {}; return (react_1.default.createElement(react_1.default.Fragment, null, timeout != null && (react_1.default.createElement("span", { style: styles.configChip }, "Timeout: ", (0, debugUtils_1.formatMs)(timeout))), debounce != null && (react_1.default.createElement("span", { style: styles.configChip }, "Debounce: ", (0, debugUtils_1.formatMs)(debounce))), interactive != null && (react_1.default.createElement("span", { style: styles.configChip }, "Interactive: ", (0, debugUtils_1.formatMs)(interactive))))); })()), trace.definitionModifications && trace.definitionModifications.length > 0 && (react_1.default.createElement("div", null, react_1.default.createElement("div", { style: styles.sectionTitle }, "Definition Modifications:"), react_1.default.createElement("ul", { style: { listStyle: 'none', margin: 0, padding: 0 } }, trace.definitionModifications.map((mod, i) => (react_1.default.createElement("li", { key: i, style: styles.listItem }, JSON.stringify(mod))))))))))), react_1.default.createElement("div", { style: { ...styles.expandArrow, ...(isExpanded ? styles.expandArrowUp : styles.expandArrowDown), }, onClick: onToggleExpand }, "\u25BC"))); } /** * 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 [currentTrace, setCurrentTrace] = (0, react_1.useState)(null); const [traceHistory, setTraceHistory] = (0, react_1.useState)([]); const [expandedHistoryIndex, setExpandedHistoryIndex] = (0, react_1.useState)(0); // For floating panel functionality 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; // Prevent default browser behavior like text selection e.preventDefault(); isDraggingRef.current = true; // Calculate the offset from cursor to container top-left corner dragOffsetRef.current = { x: e.clientX - position.x, y: e.clientY - position.y, }; }; const handleMouseMove = (e) => { if (!isDraggingRef.current) return; // Prevent default behavior during dragging e.preventDefault(); // Calculate new position by subtracting the initial offset requestAnimationFrame(() => { setPosition({ x: e.clientX - dragOffsetRef.current.x, y: e.clientY - dragOffsetRef.current.y, }); }); }; const handleMouseUp = (e) => { if (isDraggingRef.current) { // Prevent default only if we were dragging e.preventDefault(); } isDraggingRef.current = false; dragOffsetRef.current = { x: 0, y: 0 }; }; (0,