@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
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 = __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,