proteum 2.1.2 → 2.1.3-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +8 -0
- package/cli/presentation/commands.ts +27 -1
- package/cli/runtime/commands.ts +28 -0
- package/client/dev/profiler/index.tsx +338 -163
- package/common/dev/requestTrace.ts +4 -0
- package/common/dev/session.ts +24 -0
- package/package.json +1 -1
- package/server/app/container/trace/index.ts +48 -0
- package/server/services/router/http/index.ts +86 -0
- package/server/services/router/response/index.ts +1 -0
|
@@ -367,6 +367,47 @@ const profilerStyles = `
|
|
|
367
367
|
line-height: 1.45;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
.proteum-profiler__rowTitle {
|
|
371
|
+
min-width: 0;
|
|
372
|
+
word-break: break-word;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.proteum-profiler__rowMeta {
|
|
376
|
+
display: inline-flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
justify-content: flex-end;
|
|
379
|
+
gap: 8px;
|
|
380
|
+
margin-left: auto;
|
|
381
|
+
white-space: nowrap;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.proteum-profiler__statusBadge {
|
|
385
|
+
display: inline-flex;
|
|
386
|
+
align-items: center;
|
|
387
|
+
justify-content: center;
|
|
388
|
+
min-height: 18px;
|
|
389
|
+
padding: 0 8px;
|
|
390
|
+
border: 1px solid currentColor;
|
|
391
|
+
color: var(--profiler-muted);
|
|
392
|
+
background: transparent;
|
|
393
|
+
font-size: 10px;
|
|
394
|
+
font-weight: 700;
|
|
395
|
+
letter-spacing: 0.08em;
|
|
396
|
+
text-transform: uppercase;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.proteum-profiler__statusBadge--ok {
|
|
400
|
+
color: var(--profiler-ok);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.proteum-profiler__statusBadge--warn {
|
|
404
|
+
color: var(--profiler-warn);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.proteum-profiler__statusBadge--error {
|
|
408
|
+
color: var(--profiler-error);
|
|
409
|
+
}
|
|
410
|
+
|
|
370
411
|
.proteum-profiler__mono {
|
|
371
412
|
font-family: inherit;
|
|
372
413
|
font-size: 11px;
|
|
@@ -727,22 +768,23 @@ type TApiRequestItem = {
|
|
|
727
768
|
method: string;
|
|
728
769
|
path: string;
|
|
729
770
|
requestData?: TTraceSummaryValue;
|
|
771
|
+
requestDataJson?: unknown;
|
|
730
772
|
result?: TTraceSummaryValue;
|
|
773
|
+
resultJson?: unknown;
|
|
731
774
|
startedAt: string;
|
|
732
775
|
statusCode?: number;
|
|
733
776
|
statusLabel?: string;
|
|
734
777
|
tags: string[];
|
|
735
778
|
};
|
|
736
|
-
type
|
|
737
|
-
|
|
779
|
+
type TWaterfallChartItem = {
|
|
780
|
+
barLabel: string;
|
|
738
781
|
color: string;
|
|
739
|
-
|
|
740
|
-
endMs: number;
|
|
782
|
+
detailLines: string[];
|
|
741
783
|
endOffsetMs: number;
|
|
742
|
-
|
|
743
|
-
startMs: number;
|
|
784
|
+
id: string;
|
|
744
785
|
startOffsetMs: number;
|
|
745
|
-
|
|
786
|
+
subtitle?: string;
|
|
787
|
+
title: string;
|
|
746
788
|
};
|
|
747
789
|
type TProfilerState = ReturnType<typeof profilerRuntime.getState>;
|
|
748
790
|
|
|
@@ -835,6 +877,9 @@ const formatSummaryJson = (value: TTraceSummaryValue | undefined) => {
|
|
|
835
877
|
return JSON.stringify(toSummaryJsonValue(value), null, 2);
|
|
836
878
|
};
|
|
837
879
|
|
|
880
|
+
const formatApiPanelJson = (jsonValue: unknown, summaryValue: TTraceSummaryValue | undefined) =>
|
|
881
|
+
jsonValue !== undefined ? formatStructuredValue(jsonValue) : formatSummaryJson(summaryValue);
|
|
882
|
+
|
|
838
883
|
const formatTraceEventDetailsJson = (details: Record<string, TTraceSummaryValue>) =>
|
|
839
884
|
JSON.stringify(
|
|
840
885
|
Object.fromEntries(Object.entries(details).map(([key, value]) => [key, toSummaryJsonValue(value)])),
|
|
@@ -951,6 +996,14 @@ const getTraceResultData = (trace: TRequestTrace | undefined) =>
|
|
|
951
996
|
const getRequestStatusText = (statusCode?: number, statusLabel?: string) =>
|
|
952
997
|
statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
|
|
953
998
|
|
|
999
|
+
const getRequestStatusTone = (statusCode?: number, statusLabel?: string): 'ok' | 'warn' | 'error' => {
|
|
1000
|
+
if (statusCode === undefined) return statusLabel === 'pending' ? 'warn' : 'ok';
|
|
1001
|
+
if (statusCode >= 500) return 'error';
|
|
1002
|
+
if (statusCode >= 400) return 'error';
|
|
1003
|
+
if (statusCode >= 300) return 'warn';
|
|
1004
|
+
return 'ok';
|
|
1005
|
+
};
|
|
1006
|
+
|
|
954
1007
|
const findTraceEvents = (trace: TRequestTrace | undefined, eventTypes: string[]) =>
|
|
955
1008
|
trace?.events.filter((event) => eventTypes.includes(event.type)) || [];
|
|
956
1009
|
|
|
@@ -1084,6 +1137,7 @@ const ApiRequestListEntry = ({
|
|
|
1084
1137
|
onSelect: () => void;
|
|
1085
1138
|
}) => {
|
|
1086
1139
|
const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
|
|
1140
|
+
const statusTone = getRequestStatusTone(item.statusCode, item.statusLabel);
|
|
1087
1141
|
|
|
1088
1142
|
return (
|
|
1089
1143
|
<button
|
|
@@ -1093,19 +1147,14 @@ const ApiRequestListEntry = ({
|
|
|
1093
1147
|
type="button"
|
|
1094
1148
|
>
|
|
1095
1149
|
<div className="proteum-profiler__rowHeader">
|
|
1096
|
-
<strong
|
|
1097
|
-
|
|
1098
|
-
|
|
1150
|
+
<strong className="proteum-profiler__rowTitle">
|
|
1151
|
+
{formatApiReference(item.method, item.path, item.requestData, item.label)}
|
|
1152
|
+
</strong>
|
|
1153
|
+
<span className="proteum-profiler__rowMeta">
|
|
1154
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(item.durationMs)}</span>
|
|
1155
|
+
<span className={`proteum-profiler__statusBadge proteum-profiler__statusBadge--${statusTone}`}>{statusText}</span>
|
|
1099
1156
|
</span>
|
|
1100
1157
|
</div>
|
|
1101
|
-
<div className="proteum-profiler__tags">
|
|
1102
|
-
{item.tags.map((tag) => (
|
|
1103
|
-
<span className="proteum-profiler__tag" key={`${item.id}:${tag}`}>
|
|
1104
|
-
{tag}
|
|
1105
|
-
</span>
|
|
1106
|
-
))}
|
|
1107
|
-
{item.errorMessage ? <span className="proteum-profiler__tag">{truncate(item.errorMessage, 72)}</span> : null}
|
|
1108
|
-
</div>
|
|
1109
1158
|
</button>
|
|
1110
1159
|
);
|
|
1111
1160
|
};
|
|
@@ -1177,12 +1226,12 @@ const ApiRequestSidebar = ({ item }: { item?: TApiRequestItem }) => {
|
|
|
1177
1226
|
|
|
1178
1227
|
<div className="proteum-profiler__sidebarSection">
|
|
1179
1228
|
<div className="proteum-profiler__sidebarSectionTitle">Arguments</div>
|
|
1180
|
-
<JsonCodeBlock value={
|
|
1229
|
+
<JsonCodeBlock value={formatApiPanelJson(item.requestDataJson, item.requestData)} />
|
|
1181
1230
|
</div>
|
|
1182
1231
|
|
|
1183
1232
|
<div className="proteum-profiler__sidebarSection">
|
|
1184
1233
|
<div className="proteum-profiler__sidebarSectionTitle">Result</div>
|
|
1185
|
-
<JsonCodeBlock value={
|
|
1234
|
+
<JsonCodeBlock value={formatApiPanelJson(item.resultJson, item.result)} />
|
|
1186
1235
|
</div>
|
|
1187
1236
|
|
|
1188
1237
|
{item.errorMessage ? (
|
|
@@ -1209,7 +1258,9 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1209
1258
|
method: call.method,
|
|
1210
1259
|
path: call.path,
|
|
1211
1260
|
requestData: call.requestData,
|
|
1261
|
+
requestDataJson: call.requestDataJson,
|
|
1212
1262
|
result: call.result,
|
|
1263
|
+
resultJson: call.resultJson,
|
|
1213
1264
|
startedAt: call.startedAt,
|
|
1214
1265
|
statusCode: call.statusCode,
|
|
1215
1266
|
tags: [
|
|
@@ -1231,7 +1282,9 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1231
1282
|
method: trace.method,
|
|
1232
1283
|
path: trace.path,
|
|
1233
1284
|
requestData: getTraceRequestData(trace.trace),
|
|
1285
|
+
requestDataJson: trace.trace?.requestDataJson,
|
|
1234
1286
|
result: getTraceResultData(trace.trace),
|
|
1287
|
+
resultJson: trace.trace?.resultJson,
|
|
1235
1288
|
startedAt: trace.startedAt,
|
|
1236
1289
|
statusCode: trace.trace?.statusCode,
|
|
1237
1290
|
statusLabel: trace.status,
|
|
@@ -1245,57 +1298,67 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1245
1298
|
setSelectedRequestId(requestItems[0]?.id);
|
|
1246
1299
|
}, [requestItems, selectedRequestId]);
|
|
1247
1300
|
|
|
1301
|
+
const waterfallItems = buildApiWaterfallItems(requestItems);
|
|
1248
1302
|
const selectedItem = requestItems.find((item) => item.id === selectedRequestId) || requestItems[0];
|
|
1249
1303
|
|
|
1250
1304
|
return (
|
|
1251
1305
|
<div className="proteum-profiler__requestWorkspace">
|
|
1252
|
-
<div className="proteum-
|
|
1253
|
-
<
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
</div>
|
|
1306
|
+
<div className="proteum-profiler__splitColumn">
|
|
1307
|
+
<WaterfallChart
|
|
1308
|
+
emptyLabel="No API requests were captured for this session."
|
|
1309
|
+
itemLabel="request"
|
|
1310
|
+
items={waterfallItems}
|
|
1311
|
+
onSelect={setSelectedRequestId}
|
|
1312
|
+
/>
|
|
1260
1313
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
item={item}
|
|
1269
|
-
key={item.id}
|
|
1270
|
-
onSelect={() => setSelectedRequestId(item.id)}
|
|
1271
|
-
/>
|
|
1272
|
-
))}
|
|
1314
|
+
<div className="proteum-profiler__requestGroups">
|
|
1315
|
+
<div className="proteum-profiler__requestGroup">
|
|
1316
|
+
<div className="proteum-profiler__requestGroupHeader">
|
|
1317
|
+
<div className="proteum-profiler__sectionTitle">Synchronous calls</div>
|
|
1318
|
+
<div className="proteum-profiler__requestGroupCount">
|
|
1319
|
+
{syncItems.length} item{syncItems.length === 1 ? '' : 's'}
|
|
1320
|
+
</div>
|
|
1273
1321
|
</div>
|
|
1274
|
-
)}
|
|
1275
|
-
</div>
|
|
1276
1322
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1323
|
+
{syncItems.length === 0 ? (
|
|
1324
|
+
<div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
|
|
1325
|
+
) : (
|
|
1326
|
+
<div className="proteum-profiler__list">
|
|
1327
|
+
{syncItems.map((item) => (
|
|
1328
|
+
<ApiRequestListEntry
|
|
1329
|
+
isSelected={item.id === selectedItem?.id}
|
|
1330
|
+
item={item}
|
|
1331
|
+
key={item.id}
|
|
1332
|
+
onSelect={() => setSelectedRequestId(item.id)}
|
|
1333
|
+
/>
|
|
1334
|
+
))}
|
|
1335
|
+
</div>
|
|
1336
|
+
)}
|
|
1283
1337
|
</div>
|
|
1284
1338
|
|
|
1285
|
-
|
|
1286
|
-
<div className="proteum-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
isSelected={item.id === selectedItem?.id}
|
|
1292
|
-
item={item}
|
|
1293
|
-
key={item.id}
|
|
1294
|
-
onSelect={() => setSelectedRequestId(item.id)}
|
|
1295
|
-
/>
|
|
1296
|
-
))}
|
|
1339
|
+
<div className="proteum-profiler__requestGroup">
|
|
1340
|
+
<div className="proteum-profiler__requestGroupHeader">
|
|
1341
|
+
<div className="proteum-profiler__sectionTitle">Async requests</div>
|
|
1342
|
+
<div className="proteum-profiler__requestGroupCount">
|
|
1343
|
+
{asyncItems.length} item{asyncItems.length === 1 ? '' : 's'}
|
|
1344
|
+
</div>
|
|
1297
1345
|
</div>
|
|
1298
|
-
|
|
1346
|
+
|
|
1347
|
+
{asyncItems.length === 0 ? (
|
|
1348
|
+
<div className="proteum-profiler__empty">No async API calls captured.</div>
|
|
1349
|
+
) : (
|
|
1350
|
+
<div className="proteum-profiler__list">
|
|
1351
|
+
{asyncItems.map((item) => (
|
|
1352
|
+
<ApiRequestListEntry
|
|
1353
|
+
isSelected={item.id === selectedItem?.id}
|
|
1354
|
+
item={item}
|
|
1355
|
+
key={item.id}
|
|
1356
|
+
onSelect={() => setSelectedRequestId(item.id)}
|
|
1357
|
+
/>
|
|
1358
|
+
))}
|
|
1359
|
+
</div>
|
|
1360
|
+
)}
|
|
1361
|
+
</div>
|
|
1299
1362
|
</div>
|
|
1300
1363
|
</div>
|
|
1301
1364
|
|
|
@@ -1315,6 +1378,8 @@ const TraceEventSidebar = ({
|
|
|
1315
1378
|
label: string;
|
|
1316
1379
|
trace?: TRequestTrace;
|
|
1317
1380
|
}) => {
|
|
1381
|
+
const detailEntries = Object.entries(event?.details || {});
|
|
1382
|
+
|
|
1318
1383
|
if (!event) {
|
|
1319
1384
|
return (
|
|
1320
1385
|
<aside className="proteum-profiler__sidebar">
|
|
@@ -1353,19 +1418,23 @@ const TraceEventSidebar = ({
|
|
|
1353
1418
|
<SummaryRow label="Trace" value={trace?.id || 'n/a'} />
|
|
1354
1419
|
</div>
|
|
1355
1420
|
|
|
1356
|
-
|
|
1357
|
-
<div className="proteum-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1421
|
+
{detailEntries.length > 0 ? (
|
|
1422
|
+
<div className="proteum-profiler__sidebarSection">
|
|
1423
|
+
<div className="proteum-profiler__sidebarSectionTitle">Summary</div>
|
|
1424
|
+
<div>
|
|
1425
|
+
{detailEntries.map(([key, value]) => (
|
|
1426
|
+
<SummaryRow
|
|
1427
|
+
key={`${trace?.id || 'trace'}:${event.index}:detail:${key}`}
|
|
1428
|
+
label={key}
|
|
1429
|
+
value={<span className="proteum-profiler__mono">{truncate(renderSummaryValue(value), 120)}</span>}
|
|
1430
|
+
/>
|
|
1431
|
+
))}
|
|
1432
|
+
</div>
|
|
1364
1433
|
</div>
|
|
1365
|
-
|
|
1434
|
+
) : null}
|
|
1366
1435
|
|
|
1367
1436
|
<div className="proteum-profiler__sidebarSection">
|
|
1368
|
-
<div className="proteum-profiler__sidebarSectionTitle">
|
|
1437
|
+
<div className="proteum-profiler__sidebarSectionTitle">Raw JSON</div>
|
|
1369
1438
|
<JsonCodeBlock value={formatTraceEventDetailsJson(event.details)} />
|
|
1370
1439
|
</div>
|
|
1371
1440
|
</div>
|
|
@@ -1565,26 +1634,22 @@ const escapeHtml = (value: string) =>
|
|
|
1565
1634
|
.replace(/'/g, ''');
|
|
1566
1635
|
|
|
1567
1636
|
const timelineWaterfallMinDurationMs = 6;
|
|
1568
|
-
const
|
|
1569
|
-
const
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
1573
|
-
const [ApexChartComponent, setApexChartComponent] = React.useState<unknown>(null);
|
|
1574
|
-
|
|
1575
|
-
React.useEffect(() => {
|
|
1576
|
-
let isDisposed = false;
|
|
1637
|
+
const waterfallBarHeight = 15;
|
|
1638
|
+
const waterfallRowGap = 1;
|
|
1639
|
+
const waterfallRowHeight = waterfallBarHeight + waterfallRowGap;
|
|
1577
1640
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1641
|
+
const buildWaterfallEndMs = ({ durationMs, fallbackEndMs, finishedAt, startMs }: {
|
|
1642
|
+
durationMs?: number;
|
|
1643
|
+
fallbackEndMs?: number;
|
|
1644
|
+
finishedAt?: string;
|
|
1645
|
+
startMs: number;
|
|
1646
|
+
}) => {
|
|
1647
|
+
const finishedMs = readDateMs(finishedAt);
|
|
1648
|
+
const durationEndMs = durationMs !== undefined ? startMs + Math.max(durationMs, 1) : undefined;
|
|
1649
|
+
return Math.max(startMs + 1, fallbackEndMs ?? finishedMs ?? durationEndMs ?? startMs + 1);
|
|
1650
|
+
};
|
|
1587
1651
|
|
|
1652
|
+
const buildTimelineWaterfallItems = (session: TProfilerNavigationSession): TWaterfallChartItem[] => {
|
|
1588
1653
|
const sessionStartMs = readDateMs(session.startedAt) ?? 0;
|
|
1589
1654
|
const rawItems = session.traces.flatMap((traceItem) => {
|
|
1590
1655
|
const trace = traceItem.trace;
|
|
@@ -1594,17 +1659,21 @@ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1594
1659
|
const traceFinishedMs = readDateMs(trace.finishedAt) ?? (trace.durationMs !== undefined ? traceStartMs + trace.durationMs : undefined);
|
|
1595
1660
|
const traceLabel = formatSessionTraceDisplay(traceItem);
|
|
1596
1661
|
|
|
1597
|
-
return trace.events.map((event, index)
|
|
1662
|
+
return trace.events.map((event, index) => {
|
|
1598
1663
|
const nextEvent = trace.events[index + 1];
|
|
1599
1664
|
const startMs = readDateMs(event.at) ?? traceStartMs + event.elapsedMs;
|
|
1600
1665
|
const nextStartMs = nextEvent ? readDateMs(nextEvent.at) ?? traceStartMs + nextEvent.elapsedMs : undefined;
|
|
1601
|
-
const endMs =
|
|
1666
|
+
const endMs = buildWaterfallEndMs({
|
|
1667
|
+
fallbackEndMs: nextStartMs ?? traceFinishedMs,
|
|
1668
|
+
startMs,
|
|
1669
|
+
});
|
|
1602
1670
|
|
|
1603
1671
|
return {
|
|
1604
1672
|
durationMs: Math.max(1, endMs - startMs),
|
|
1605
1673
|
endMs,
|
|
1606
1674
|
event,
|
|
1607
1675
|
startMs,
|
|
1676
|
+
trace,
|
|
1608
1677
|
traceLabel,
|
|
1609
1678
|
};
|
|
1610
1679
|
});
|
|
@@ -1612,28 +1681,113 @@ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1612
1681
|
|
|
1613
1682
|
const sortedItems = [...rawItems].sort((left, right) => left.startMs - right.startMs || left.event.index - right.event.index);
|
|
1614
1683
|
const chartStartMs = sortedItems.length > 0 ? Math.min(...sortedItems.map((item) => item.startMs)) : 0;
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const items: TTimelineWaterfallEventItem[] = sortedItems
|
|
1684
|
+
|
|
1685
|
+
return sortedItems
|
|
1618
1686
|
.filter((item) => item.durationMs >= timelineWaterfallMinDurationMs)
|
|
1619
|
-
.map((item) =>
|
|
1620
|
-
|
|
1621
|
-
|
|
1687
|
+
.map((item) => {
|
|
1688
|
+
const startOffsetMs = item.startMs - chartStartMs;
|
|
1689
|
+
const endOffsetMs = item.endMs - chartStartMs;
|
|
1690
|
+
|
|
1691
|
+
return {
|
|
1692
|
+
barLabel: truncate(`${item.event.type} | ${item.traceLabel}`, 84),
|
|
1693
|
+
color: getTimelineDurationColor(item.durationMs),
|
|
1694
|
+
detailLines: [
|
|
1695
|
+
`Start: +${Math.round(startOffsetMs)} ms`,
|
|
1696
|
+
`End: +${Math.round(endOffsetMs)} ms`,
|
|
1697
|
+
`Span: ${formatDuration(item.durationMs)}`,
|
|
1698
|
+
],
|
|
1699
|
+
endOffsetMs,
|
|
1700
|
+
id: getTraceEventKey(item.trace.id, item.event),
|
|
1701
|
+
startOffsetMs,
|
|
1702
|
+
subtitle: item.traceLabel,
|
|
1703
|
+
title: item.event.type,
|
|
1704
|
+
};
|
|
1705
|
+
});
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
const buildApiWaterfallItems = (requestItems: TApiRequestItem[]): TWaterfallChartItem[] => {
|
|
1709
|
+
const rawItems = requestItems.map((item) => {
|
|
1710
|
+
const startMs = readDateMs(item.startedAt) ?? 0;
|
|
1711
|
+
const endMs = buildWaterfallEndMs({
|
|
1712
|
+
durationMs: item.durationMs,
|
|
1713
|
+
finishedAt: item.finishedAt,
|
|
1714
|
+
startMs,
|
|
1715
|
+
});
|
|
1716
|
+
const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
|
|
1717
|
+
const reference = formatApiReference(item.method, item.path, item.requestData, item.label);
|
|
1718
|
+
|
|
1719
|
+
return {
|
|
1720
|
+
endMs,
|
|
1721
|
+
item,
|
|
1722
|
+
reference,
|
|
1723
|
+
startMs,
|
|
1724
|
+
statusText,
|
|
1725
|
+
};
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
const sortedItems = [...rawItems].sort((left, right) => left.startMs - right.startMs || left.reference.localeCompare(right.reference));
|
|
1729
|
+
const chartStartMs = sortedItems.length > 0 ? Math.min(...sortedItems.map((item) => item.startMs)) : 0;
|
|
1730
|
+
|
|
1731
|
+
return sortedItems.map(({ endMs, item, reference, startMs, statusText }) => {
|
|
1732
|
+
const startOffsetMs = startMs - chartStartMs;
|
|
1733
|
+
const endOffsetMs = endMs - chartStartMs;
|
|
1734
|
+
|
|
1735
|
+
return {
|
|
1736
|
+
barLabel: truncate(reference, 84),
|
|
1622
1737
|
color: getTimelineDurationColor(item.durationMs),
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1738
|
+
detailLines: [
|
|
1739
|
+
`Status: ${statusText}`,
|
|
1740
|
+
`Duration: ${formatDuration(item.durationMs)}`,
|
|
1741
|
+
`Start: +${Math.round(startOffsetMs)} ms`,
|
|
1742
|
+
`End: +${Math.round(endOffsetMs)} ms`,
|
|
1743
|
+
],
|
|
1744
|
+
endOffsetMs,
|
|
1745
|
+
id: item.id,
|
|
1746
|
+
startOffsetMs,
|
|
1747
|
+
subtitle: item.groupLabel,
|
|
1748
|
+
title: reference,
|
|
1749
|
+
};
|
|
1750
|
+
});
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
const WaterfallChart = ({
|
|
1754
|
+
emptyLabel,
|
|
1755
|
+
itemLabel,
|
|
1756
|
+
items,
|
|
1757
|
+
onSelect,
|
|
1758
|
+
}: {
|
|
1759
|
+
emptyLabel: string;
|
|
1760
|
+
itemLabel: string;
|
|
1761
|
+
items: TWaterfallChartItem[];
|
|
1762
|
+
onSelect?: (itemId: string) => void;
|
|
1763
|
+
}) => {
|
|
1764
|
+
const [ApexChartComponent, setApexChartComponent] = React.useState<unknown>(null);
|
|
1765
|
+
|
|
1766
|
+
React.useEffect(() => {
|
|
1767
|
+
let isDisposed = false;
|
|
1768
|
+
|
|
1769
|
+
void import('react-apexcharts').then((module) => {
|
|
1770
|
+
if (isDisposed) return;
|
|
1771
|
+
setApexChartComponent(() => module.default);
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
return () => {
|
|
1775
|
+
isDisposed = true;
|
|
1776
|
+
};
|
|
1777
|
+
}, []);
|
|
1778
|
+
|
|
1779
|
+
const totalDurationMs = Math.max(items.length > 0 ? Math.max(...items.map((item) => item.endOffsetMs)) : 1, 1);
|
|
1780
|
+
const chartHeight = Math.max(260, items.length * waterfallRowHeight + 24);
|
|
1627
1781
|
const ChartComponent = ApexChartComponent as any;
|
|
1628
1782
|
|
|
1629
1783
|
const series = [
|
|
1630
1784
|
{
|
|
1631
1785
|
data: items.map((item) => ({
|
|
1632
1786
|
fillColor: item.color,
|
|
1633
|
-
x: item.
|
|
1787
|
+
x: item.barLabel,
|
|
1634
1788
|
y: [item.startOffsetMs, item.endOffsetMs],
|
|
1635
1789
|
})),
|
|
1636
|
-
name:
|
|
1790
|
+
name: itemLabel,
|
|
1637
1791
|
},
|
|
1638
1792
|
];
|
|
1639
1793
|
|
|
@@ -1641,6 +1795,18 @@ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1641
1795
|
chart: {
|
|
1642
1796
|
animations: { enabled: false },
|
|
1643
1797
|
background: 'transparent',
|
|
1798
|
+
events: onSelect
|
|
1799
|
+
? {
|
|
1800
|
+
dataPointSelection: (
|
|
1801
|
+
_event: unknown,
|
|
1802
|
+
_chartContext: unknown,
|
|
1803
|
+
config: { dataPointIndex: number },
|
|
1804
|
+
) => {
|
|
1805
|
+
const item = items[config.dataPointIndex];
|
|
1806
|
+
if (item) onSelect(item.id);
|
|
1807
|
+
},
|
|
1808
|
+
}
|
|
1809
|
+
: undefined,
|
|
1644
1810
|
foreColor: '#627186',
|
|
1645
1811
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
1646
1812
|
toolbar: { show: false },
|
|
@@ -1668,11 +1834,11 @@ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1668
1834
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
1669
1835
|
fontSize: '11px',
|
|
1670
1836
|
},
|
|
1671
|
-
text:
|
|
1837
|
+
text: emptyLabel,
|
|
1672
1838
|
},
|
|
1673
1839
|
plotOptions: {
|
|
1674
1840
|
bar: {
|
|
1675
|
-
barHeight:
|
|
1841
|
+
barHeight: waterfallBarHeight,
|
|
1676
1842
|
borderRadius: 2,
|
|
1677
1843
|
horizontal: true,
|
|
1678
1844
|
rangeBarGroupRows: false,
|
|
@@ -1689,11 +1855,14 @@ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1689
1855
|
|
|
1690
1856
|
return `
|
|
1691
1857
|
<div style="padding:8px 10px; color:#132033; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace; font-size:11px; line-height:1.5;">
|
|
1692
|
-
<div style="font-weight:700;">${escapeHtml(item.
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1858
|
+
<div style="font-weight:700;">${escapeHtml(item.title)}</div>
|
|
1859
|
+
${item.subtitle ? `<div style="color:#627186;">${escapeHtml(item.subtitle)}</div>` : ''}
|
|
1860
|
+
${item.detailLines
|
|
1861
|
+
.map(
|
|
1862
|
+
(line, index) =>
|
|
1863
|
+
`<div style="${index === 0 ? 'margin-top:6px;' : ''} color:#627186;">${escapeHtml(line)}</div>`,
|
|
1864
|
+
)
|
|
1865
|
+
.join('')}
|
|
1697
1866
|
</div>
|
|
1698
1867
|
`;
|
|
1699
1868
|
},
|
|
@@ -1727,7 +1896,8 @@ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1727
1896
|
<div className="proteum-profiler__timelineChart">
|
|
1728
1897
|
<div className="proteum-profiler__timelineChartMeta">
|
|
1729
1898
|
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
1730
|
-
{items.length}
|
|
1899
|
+
{items.length} {itemLabel}
|
|
1900
|
+
{items.length === 1 ? '' : 's'}
|
|
1731
1901
|
</span>
|
|
1732
1902
|
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(totalDurationMs)}</span>
|
|
1733
1903
|
</div>
|
|
@@ -1738,7 +1908,7 @@ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1738
1908
|
) : items.length > 0 ? (
|
|
1739
1909
|
<div className="proteum-profiler__empty">Loading waterfall chart...</div>
|
|
1740
1910
|
) : (
|
|
1741
|
-
<div className="proteum-profiler__empty">
|
|
1911
|
+
<div className="proteum-profiler__empty">{emptyLabel}</div>
|
|
1742
1912
|
)}
|
|
1743
1913
|
</div>
|
|
1744
1914
|
</div>
|
|
@@ -1764,67 +1934,72 @@ const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1764
1934
|
setSelectedEventKey(selections[0]?.key);
|
|
1765
1935
|
}, [selectedEventKey, selections]);
|
|
1766
1936
|
|
|
1937
|
+
const waterfallItems = buildTimelineWaterfallItems(session);
|
|
1767
1938
|
const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
|
|
1768
1939
|
|
|
1769
1940
|
return (
|
|
1770
|
-
<div className="proteum-
|
|
1771
|
-
<
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
<div className="proteum-profiler__list">
|
|
1779
|
-
{session.steps.map((step) => (
|
|
1780
|
-
<div className="proteum-profiler__row" key={step.id}>
|
|
1781
|
-
<div className="proteum-profiler__rowHeader">
|
|
1782
|
-
<strong>{step.label}</strong>
|
|
1783
|
-
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
|
|
1784
|
-
</div>
|
|
1785
|
-
<div className="proteum-profiler__tags">
|
|
1786
|
-
<span className="proteum-profiler__tag">{step.status}</span>
|
|
1787
|
-
{Object.entries(step.details || {}).map(([key, value]) => (
|
|
1788
|
-
<span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
|
|
1789
|
-
{key}:{String(value)}
|
|
1790
|
-
</span>
|
|
1791
|
-
))}
|
|
1792
|
-
{step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
|
|
1793
|
-
</div>
|
|
1794
|
-
</div>
|
|
1795
|
-
))}
|
|
1796
|
-
</div>
|
|
1797
|
-
</div>
|
|
1941
|
+
<div className="proteum-profiler__splitView">
|
|
1942
|
+
<div className="proteum-profiler__splitColumn">
|
|
1943
|
+
<WaterfallChart
|
|
1944
|
+
emptyLabel="No timeline events were captured for this session."
|
|
1945
|
+
itemLabel="event"
|
|
1946
|
+
items={waterfallItems}
|
|
1947
|
+
onSelect={setSelectedEventKey}
|
|
1948
|
+
/>
|
|
1798
1949
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
<div className="proteum-profiler__rowHeader">
|
|
1810
|
-
<strong>{formatSessionTraceDisplay(traceItem)}</strong>
|
|
1811
|
-
<span className="proteum-profiler__mono proteum-profiler__muted">{traceItem.status}</span>
|
|
1950
|
+
<div className="proteum-profiler__section">
|
|
1951
|
+
<div className="proteum-profiler__titleRow">
|
|
1952
|
+
<div className="proteum-profiler__sectionTitle">Navigation steps</div>
|
|
1953
|
+
</div>
|
|
1954
|
+
<div className="proteum-profiler__list">
|
|
1955
|
+
{session.steps.map((step) => (
|
|
1956
|
+
<div className="proteum-profiler__row" key={step.id}>
|
|
1957
|
+
<div className="proteum-profiler__rowHeader">
|
|
1958
|
+
<strong>{step.label}</strong>
|
|
1959
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
|
|
1812
1960
|
</div>
|
|
1813
|
-
<div className="proteum-
|
|
1814
|
-
{
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1961
|
+
<div className="proteum-profiler__tags">
|
|
1962
|
+
<span className="proteum-profiler__tag">{step.status}</span>
|
|
1963
|
+
{Object.entries(step.details || {}).map(([key, value]) => (
|
|
1964
|
+
<span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
|
|
1965
|
+
{key}:{String(value)}
|
|
1966
|
+
</span>
|
|
1967
|
+
))}
|
|
1968
|
+
{step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
|
|
1820
1969
|
</div>
|
|
1821
1970
|
</div>
|
|
1822
|
-
)
|
|
1823
|
-
|
|
1971
|
+
))}
|
|
1972
|
+
</div>
|
|
1824
1973
|
</div>
|
|
1825
1974
|
|
|
1826
|
-
|
|
1975
|
+
{session.traces.map((traceItem) =>
|
|
1976
|
+
traceItem.trace ? (
|
|
1977
|
+
<TraceRows
|
|
1978
|
+
key={traceItem.id}
|
|
1979
|
+
onSelect={setSelectedEventKey}
|
|
1980
|
+
selectedEventKey={selectedEventKey}
|
|
1981
|
+
trace={traceItem.trace}
|
|
1982
|
+
/>
|
|
1983
|
+
) : (
|
|
1984
|
+
<div className="proteum-profiler__row" key={traceItem.id}>
|
|
1985
|
+
<div className="proteum-profiler__rowHeader">
|
|
1986
|
+
<strong>{formatSessionTraceDisplay(traceItem)}</strong>
|
|
1987
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{traceItem.status}</span>
|
|
1988
|
+
</div>
|
|
1989
|
+
<div className="proteum-profiler__mono">
|
|
1990
|
+
{formatProfilerRequestReference({
|
|
1991
|
+
fallbackLabel: traceItem.label,
|
|
1992
|
+
method: traceItem.method,
|
|
1993
|
+
path: traceItem.path,
|
|
1994
|
+
requestData: getTraceRequestData(traceItem.trace),
|
|
1995
|
+
})}
|
|
1996
|
+
</div>
|
|
1997
|
+
</div>
|
|
1998
|
+
),
|
|
1999
|
+
)}
|
|
1827
2000
|
</div>
|
|
2001
|
+
|
|
2002
|
+
<TraceEventSidebar event={selected?.event} label={selected?.label || 'Trace event'} trace={selected?.trace} />
|
|
1828
2003
|
</div>
|
|
1829
2004
|
);
|
|
1830
2005
|
};
|