proteum 2.1.1 → 2.1.2

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.
@@ -15,23 +15,26 @@ import type {
15
15
  TProfilerPanel,
16
16
  TProfilerSessionTrace,
17
17
  } from '@common/dev/profiler';
18
- import type { TRequestTrace, TTraceCall, TTraceSummaryValue } from '@common/dev/requestTrace';
18
+ import type { TRequestTrace, TTraceCall, TTraceEventType, TTraceSummaryValue } from '@common/dev/requestTrace';
19
19
 
20
20
  import { profilerRuntime } from './runtime';
21
21
 
22
22
  const profilerStyles = `
23
23
  .proteum-profiler {
24
- --profiler-bg: #000000;
25
- --profiler-bg-strong: #000000;
26
- --profiler-surface-hover: rgba(22, 33, 48, 0.32);
27
- --profiler-line: rgba(155, 188, 214, 0.16);
28
- --profiler-line-strong: rgba(155, 188, 214, 0.28);
29
- --profiler-text: #e5f2ff;
30
- --profiler-muted: rgba(213, 228, 242, 0.64);
31
- --profiler-brand: #8fd9ff;
32
- --profiler-ok: #7af4b4;
33
- --profiler-warn: #ffd369;
34
- --profiler-error: #ff9797;
24
+ --profiler-bg: #f3f5f8;
25
+ --profiler-bg-strong: #ffffff;
26
+ --profiler-bg-soft: #eef3f8;
27
+ --profiler-surface-hover: #eef4ff;
28
+ --profiler-surface-selected: #e1ecff;
29
+ --profiler-line: rgba(19, 32, 51, 0.1);
30
+ --profiler-line-strong: rgba(19, 32, 51, 0.18);
31
+ --profiler-text: #132033;
32
+ --profiler-muted: #627186;
33
+ --profiler-brand: #175fe6;
34
+ --profiler-ok: #15803d;
35
+ --profiler-warn: #b45309;
36
+ --profiler-error: #b91c1c;
37
+ --profiler-title-row-bg: #f1f3f5;
35
38
  position: fixed;
36
39
  inset-inline: 0;
37
40
  bottom: 0;
@@ -61,7 +64,7 @@ const profilerStyles = `
61
64
  min-height: 32px;
62
65
  padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px));
63
66
  border-top: 1px solid var(--profiler-line-strong);
64
- background: #000000;
67
+ background: var(--profiler-bg-strong);
65
68
  backdrop-filter: none;
66
69
  box-shadow: none;
67
70
  overflow-x: auto;
@@ -135,7 +138,7 @@ const profilerStyles = `
135
138
  padding: 0 12px;
136
139
  border: 1px solid var(--profiler-line-strong);
137
140
  border-radius: 0;
138
- background: #000000;
141
+ background: var(--profiler-bg-strong);
139
142
  backdrop-filter: none;
140
143
  color: var(--profiler-brand);
141
144
  box-shadow: none;
@@ -159,7 +162,7 @@ const profilerStyles = `
159
162
  border-left: none;
160
163
  border-right: none;
161
164
  border-radius: 0;
162
- background: #000000;
165
+ background: var(--profiler-bg-strong);
163
166
  backdrop-filter: none;
164
167
  box-shadow: none;
165
168
  overflow: hidden;
@@ -173,6 +176,7 @@ const profilerStyles = `
173
176
  padding: 12px 14px 10px;
174
177
  border-bottom: 1px solid var(--profiler-line);
175
178
  min-width: 0;
179
+ background: var(--profiler-bg-strong);
176
180
  }
177
181
 
178
182
  .proteum-profiler__panelTabs {
@@ -225,7 +229,8 @@ const profilerStyles = `
225
229
  height: 28px;
226
230
  padding: 0 28px 0 10px;
227
231
  border: 1px solid var(--profiler-line);
228
- background-color: #000000;
232
+ border-radius: 0;
233
+ background-color: var(--profiler-bg-strong);
229
234
  background-image:
230
235
  linear-gradient(45deg, transparent 50%, var(--profiler-muted) 50%),
231
236
  linear-gradient(135deg, var(--profiler-muted) 50%, transparent 50%);
@@ -240,19 +245,22 @@ const profilerStyles = `
240
245
  }
241
246
 
242
247
  .proteum-profiler__select option {
243
- background: #000000;
248
+ background: var(--profiler-bg-strong);
244
249
  color: var(--profiler-text);
245
250
  }
246
251
 
247
252
  .proteum-profiler__panelBody {
248
253
  overflow: auto;
249
- padding: 0 14px 16px;
254
+ height: 100%;
255
+ min-height: 0;
256
+ padding: 0;
257
+ background: transparent;
250
258
  }
251
259
 
252
260
  .proteum-profiler__metrics {
253
261
  display: grid;
254
262
  gap: 0;
255
- padding-top: 10px;
263
+ padding: 10px 12px 0;
256
264
  }
257
265
 
258
266
  .proteum-profiler__metricRow {
@@ -283,8 +291,8 @@ const profilerStyles = `
283
291
 
284
292
  .proteum-profiler__section {
285
293
  display: grid;
286
- gap: 8px;
287
- padding: 12px 0 0;
294
+ gap: 0;
295
+ padding: 0;
288
296
  border-top: 1px solid var(--profiler-line);
289
297
  }
290
298
 
@@ -293,6 +301,8 @@ const profilerStyles = `
293
301
  align-items: center;
294
302
  justify-content: space-between;
295
303
  gap: 12px;
304
+ padding: 8px 10px;
305
+ background: var(--profiler-title-row-bg);
296
306
  }
297
307
 
298
308
  .proteum-profiler__actions {
@@ -320,25 +330,21 @@ const profilerStyles = `
320
330
  gap: 0;
321
331
  }
322
332
 
323
- .proteum-profiler__list > .proteum-profiler__row:first-child {
324
- border-top: none;
325
- padding-top: 2px;
326
- }
327
-
328
333
  .proteum-profiler__row {
329
334
  display: grid;
330
- gap: 4px;
331
- padding: 8px 0;
335
+ gap: 6px;
336
+ padding: 10px 12px;
332
337
  border-top: 1px solid var(--profiler-line);
338
+ border-radius: 0;
339
+ background: transparent;
340
+ box-shadow: none;
333
341
  }
334
342
 
335
343
  .proteum-profiler__row--interactive {
336
344
  width: 100%;
337
345
  appearance: none;
338
346
  background: transparent;
339
- border-inline: none;
340
- border-bottom: none;
341
- border-radius: 0;
347
+ border: none;
342
348
  text-align: left;
343
349
  color: inherit;
344
350
  cursor: pointer;
@@ -348,6 +354,10 @@ const profilerStyles = `
348
354
  background: var(--profiler-surface-hover);
349
355
  }
350
356
 
357
+ .proteum-profiler__row--selected {
358
+ background: var(--profiler-surface-selected);
359
+ }
360
+
351
361
  .proteum-profiler__rowHeader {
352
362
  display: flex;
353
363
  align-items: flex-start;
@@ -371,15 +381,37 @@ const profilerStyles = `
371
381
  margin: 0;
372
382
  white-space: pre-wrap;
373
383
  word-break: break-word;
374
- padding-top: 8px;
384
+ padding: 10px 0 0;
385
+ border: none;
375
386
  border-top: 1px solid var(--profiler-line);
387
+ border-radius: 0;
388
+ background: transparent;
389
+ }
390
+
391
+ .proteum-profiler__jsonKey {
392
+ color: var(--profiler-brand);
393
+ }
394
+
395
+ .proteum-profiler__jsonString {
396
+ color: #0f766e;
397
+ }
398
+
399
+ .proteum-profiler__jsonNumber {
400
+ color: #b45309;
401
+ }
402
+
403
+ .proteum-profiler__jsonLiteral {
404
+ color: var(--profiler-error);
376
405
  }
377
406
 
378
407
  .proteum-profiler__detail {
379
408
  display: grid;
380
409
  gap: 10px;
381
- padding: 10px 0 14px;
382
- border-top: 1px dashed var(--profiler-line);
410
+ padding: 10px 0 0;
411
+ border: none;
412
+ border-top: 1px solid var(--profiler-line);
413
+ border-radius: 0;
414
+ background: transparent;
383
415
  }
384
416
 
385
417
  .proteum-profiler__detailLine {
@@ -423,11 +455,196 @@ const profilerStyles = `
423
455
  }
424
456
 
425
457
  .proteum-profiler__empty {
426
- padding: 12px 0;
427
- border-top: 1px dashed var(--profiler-line);
458
+ padding: 12px;
459
+ border-top: 1px solid var(--profiler-line);
460
+ color: var(--profiler-muted);
461
+ }
462
+
463
+ .proteum-profiler__requestWorkspace {
464
+ display: grid;
465
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
466
+ gap: 0;
467
+ align-items: stretch;
468
+ min-height: 100%;
469
+ height: 100%;
470
+ }
471
+
472
+ .proteum-profiler__splitView {
473
+ display: grid;
474
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
475
+ gap: 0;
476
+ align-items: stretch;
477
+ min-height: 100%;
478
+ height: 100%;
479
+ }
480
+
481
+ .proteum-profiler__splitView--stacked {
482
+ min-height: 0;
483
+ height: auto;
484
+ }
485
+
486
+ .proteum-profiler__splitColumn {
487
+ display: grid;
488
+ gap: 0;
489
+ min-width: 0;
490
+ align-content: start;
491
+ }
492
+
493
+ .proteum-profiler__requestGroups {
494
+ display: grid;
495
+ gap: 0;
496
+ min-width: 0;
497
+ }
498
+
499
+ .proteum-profiler__requestGroup {
500
+ display: grid;
501
+ gap: 0;
502
+ }
503
+
504
+ .proteum-profiler__requestGroupHeader {
505
+ display: flex;
506
+ align-items: center;
507
+ justify-content: space-between;
508
+ gap: 12px;
509
+ padding: 8px 10px;
510
+ background: var(--profiler-title-row-bg);
511
+ }
512
+
513
+ .proteum-profiler__requestGroupCount {
514
+ color: var(--profiler-muted);
515
+ font-size: 10px;
516
+ letter-spacing: 0.08em;
517
+ text-transform: uppercase;
518
+ }
519
+
520
+ .proteum-profiler__sidebar {
521
+ position: sticky;
522
+ top: 0;
523
+ display: flex;
524
+ align-self: stretch;
525
+ height: 100%;
526
+ min-height: 0;
527
+ padding: 0;
528
+ border: none;
529
+ border-left: 1px solid var(--profiler-line);
530
+ border-radius: 0;
531
+ background: transparent;
532
+ box-shadow: none;
533
+ }
534
+
535
+ .proteum-profiler__sidebarScroller {
536
+ display: grid;
537
+ flex: 1 1 auto;
538
+ gap: 0;
539
+ align-content: start;
540
+ height: 100%;
541
+ min-height: 0;
542
+ overflow: auto;
543
+ overscroll-behavior: contain;
544
+ scrollbar-width: thin;
545
+ }
546
+
547
+ .proteum-profiler__titleRow {
548
+ padding: 8px 10px;
549
+ background: var(--profiler-title-row-bg);
550
+ }
551
+
552
+ .proteum-profiler__sidebarHeader {
553
+ display: grid;
554
+ gap: 6px;
555
+ padding: 10px 12px 0;
556
+ }
557
+
558
+ .proteum-profiler__sidebarEyebrow,
559
+ .proteum-profiler__sidebarSectionTitle {
560
+ color: var(--profiler-muted);
561
+ font-size: 10px;
562
+ letter-spacing: 0.08em;
563
+ text-transform: uppercase;
564
+ }
565
+
566
+ .proteum-profiler__sidebarTitle {
567
+ font-size: 13px;
568
+ line-height: 1.5;
569
+ word-break: break-word;
570
+ }
571
+
572
+ .proteum-profiler__sidebarSection {
573
+ display: grid;
574
+ gap: 6px;
575
+ padding: 10px 12px 0;
576
+ border-top: 1px solid var(--profiler-line);
577
+ }
578
+
579
+ .proteum-profiler__sidebarScroller > .proteum-profiler__metrics {
580
+ border-top: 1px solid var(--profiler-line);
581
+ }
582
+
583
+ .proteum-profiler__sidebarEmpty {
584
+ font-size: 12px;
428
585
  color: var(--profiler-muted);
429
586
  }
430
587
 
588
+ .proteum-profiler__timelineChart {
589
+ display: grid;
590
+ gap: 0;
591
+ }
592
+
593
+ .proteum-profiler__timelineChartMeta {
594
+ display: flex;
595
+ align-items: center;
596
+ justify-content: space-between;
597
+ gap: 12px;
598
+ padding: 10px 12px 0;
599
+ }
600
+
601
+ .proteum-profiler__timelineChartCanvas {
602
+ position: relative;
603
+ padding: 8px 12px 12px;
604
+ border-top: 1px solid var(--profiler-line);
605
+ }
606
+
607
+ .proteum-profiler__timelineChartCanvas > * {
608
+ height: 100%;
609
+ }
610
+
611
+ .proteum-profiler__timelineChartCanvas canvas {
612
+ display: block;
613
+ width: 100%;
614
+ height: 100%;
615
+ }
616
+
617
+ .proteum-profiler__timelineChartCanvas .apexcharts-canvas,
618
+ .proteum-profiler__timelineChartCanvas .apexcharts-svg {
619
+ background: transparent !important;
620
+ }
621
+
622
+ .proteum-profiler__traceEventRow {
623
+ --profiler-trace-depth: 0;
624
+ --profiler-trace-guide-opacity: 0;
625
+ --profiler-trace-indent: calc(var(--profiler-trace-depth) * 18px);
626
+ }
627
+
628
+ .proteum-profiler__traceEventRow .proteum-profiler__rowHeader,
629
+ .proteum-profiler__traceEventRow .proteum-profiler__tags {
630
+ padding-inline-start: var(--profiler-trace-indent);
631
+ }
632
+
633
+ .proteum-profiler__traceEventRow .proteum-profiler__rowHeader {
634
+ position: relative;
635
+ }
636
+
637
+ .proteum-profiler__traceEventRow .proteum-profiler__rowHeader::before {
638
+ content: '';
639
+ position: absolute;
640
+ left: max(0px, calc(var(--profiler-trace-indent) - 8px));
641
+ top: 3px;
642
+ bottom: 3px;
643
+ width: 1px;
644
+ background: var(--profiler-line-strong);
645
+ opacity: var(--profiler-trace-guide-opacity);
646
+ }
647
+
431
648
  @media (max-width: 900px) {
432
649
  .proteum-profiler__panel {
433
650
  height: 50vh;
@@ -459,6 +676,33 @@ const profilerStyles = `
459
676
  .proteum-profiler__select {
460
677
  min-width: 132px;
461
678
  }
679
+
680
+ .proteum-profiler__requestWorkspace {
681
+ grid-template-columns: 1fr;
682
+ min-height: 0;
683
+ height: auto;
684
+ }
685
+
686
+ .proteum-profiler__splitView {
687
+ grid-template-columns: 1fr;
688
+ min-height: 0;
689
+ height: auto;
690
+ }
691
+
692
+ .proteum-profiler__sidebar {
693
+ position: static;
694
+ height: auto;
695
+ min-height: 0;
696
+ border-left: none;
697
+ border-top: 1px solid var(--profiler-line);
698
+ }
699
+
700
+ .proteum-profiler__sidebarScroller {
701
+ height: auto;
702
+ max-height: none;
703
+ min-height: 0;
704
+ }
705
+
462
706
  }
463
707
  `;
464
708
 
@@ -473,20 +717,48 @@ type TSessionSummary = {
473
717
  statusLabel: string;
474
718
  totalMs?: number;
475
719
  };
720
+ type TApiRequestItem = {
721
+ id: string;
722
+ groupLabel: string;
723
+ durationMs?: number;
724
+ errorMessage?: string;
725
+ finishedAt?: string;
726
+ label?: string;
727
+ method: string;
728
+ path: string;
729
+ requestData?: TTraceSummaryValue;
730
+ result?: TTraceSummaryValue;
731
+ startedAt: string;
732
+ statusCode?: number;
733
+ statusLabel?: string;
734
+ tags: string[];
735
+ };
736
+ type TTimelineWaterfallEventItem = {
737
+ chartLabel: string;
738
+ color: string;
739
+ durationMs: number;
740
+ endMs: number;
741
+ endOffsetMs: number;
742
+ event: TRequestTrace['events'][number];
743
+ startMs: number;
744
+ startOffsetMs: number;
745
+ traceLabel: string;
746
+ };
476
747
  type TProfilerState = ReturnType<typeof profilerRuntime.getState>;
477
748
 
478
749
  const panelLabels: Record<TProfilerPanel, string> = {
479
750
  summary: 'Summary',
480
751
  timeline: 'Timeline',
481
752
  routing: 'Routing',
753
+ auth: 'Auth',
482
754
  controller: 'Controller',
483
755
  ssr: 'SSR',
484
756
  api: 'API',
757
+ errors: 'Errors',
485
758
  explain: 'Explain',
486
759
  doctor: 'Doctor',
487
760
  commands: 'Commands',
488
761
  cron: 'Cron',
489
- errors: 'Errors',
490
762
  };
491
763
 
492
764
  const getSelectedSession = (sessions: TProfilerNavigationSession[], selectedSessionId?: string, currentSessionId?: string) =>
@@ -563,6 +835,49 @@ const formatSummaryJson = (value: TTraceSummaryValue | undefined) => {
563
835
  return JSON.stringify(toSummaryJsonValue(value), null, 2);
564
836
  };
565
837
 
838
+ const formatTraceEventDetailsJson = (details: Record<string, TTraceSummaryValue>) =>
839
+ JSON.stringify(
840
+ Object.fromEntries(Object.entries(details).map(([key, value]) => [key, toSummaryJsonValue(value)])),
841
+ null,
842
+ 2,
843
+ );
844
+
845
+ const renderHighlightedJson = (value: string) => {
846
+ const tokenPattern =
847
+ /"(?:\\u[0-9a-fA-F]{4}|\\[^u]|[^\\"])*"(?=\s*:)|"(?:\\u[0-9a-fA-F]{4}|\\[^u]|[^\\"])*"|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
848
+ const parts: React.ReactNode[] = [];
849
+ let lastIndex = 0;
850
+ let match: RegExpExecArray | null;
851
+
852
+ while ((match = tokenPattern.exec(value))) {
853
+ const index = match.index;
854
+ const token = match[0];
855
+
856
+ if (index > lastIndex) parts.push(value.slice(lastIndex, index));
857
+
858
+ const trailing = value.slice(index + token.length);
859
+ const isKey = token.startsWith('"') && /^\s*:/.test(trailing);
860
+ const className = token.startsWith('"')
861
+ ? isKey
862
+ ? 'proteum-profiler__jsonKey'
863
+ : 'proteum-profiler__jsonString'
864
+ : token === 'true' || token === 'false' || token === 'null'
865
+ ? 'proteum-profiler__jsonLiteral'
866
+ : 'proteum-profiler__jsonNumber';
867
+
868
+ parts.push(
869
+ <span className={className} key={`json:${index}`}>
870
+ {token}
871
+ </span>,
872
+ );
873
+ lastIndex = index + token.length;
874
+ }
875
+
876
+ if (lastIndex < value.length) parts.push(value.slice(lastIndex));
877
+
878
+ return parts;
879
+ };
880
+
566
881
  const formatSummaryLiteral = (value: TTraceSummaryValue | undefined, depth = 1): string => {
567
882
  if (value === undefined) return '';
568
883
  if (value === null) return 'null';
@@ -605,6 +920,26 @@ const formatApiReference = (method: string, path: string, requestData?: TTraceSu
605
920
  return `${getApiReferenceName(method, path, fallbackLabel)}(${truncate(args, 112)})`;
606
921
  };
607
922
 
923
+ const formatProfilerRequestReference = ({
924
+ fallbackLabel,
925
+ method,
926
+ path,
927
+ requestData,
928
+ }: {
929
+ fallbackLabel?: string;
930
+ method?: string;
931
+ path?: string;
932
+ requestData?: TTraceSummaryValue;
933
+ }) => {
934
+ const safeMethod = method || '';
935
+ const safePath = path || '';
936
+
937
+ if (safePath.startsWith('/api/')) return formatApiReference(safeMethod, safePath, requestData, fallbackLabel);
938
+
939
+ const rawReference = `${safeMethod} ${safePath}`.trim();
940
+ return rawReference || fallbackLabel || 'request';
941
+ };
942
+
608
943
  const getTraceRequestData = (trace: TRequestTrace | undefined) =>
609
944
  trace?.events.find((event) => event.type === 'request.start')?.details.data;
610
945
 
@@ -613,9 +948,51 @@ const getTraceResultData = (trace: TRequestTrace | undefined) =>
613
948
  .reverse()
614
949
  .find((event) => event.details.kind === 'json' && event.details.data !== undefined)?.details.data;
615
950
 
951
+ const getRequestStatusText = (statusCode?: number, statusLabel?: string) =>
952
+ statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
953
+
616
954
  const findTraceEvents = (trace: TRequestTrace | undefined, eventTypes: string[]) =>
617
955
  trace?.events.filter((event) => eventTypes.includes(event.type)) || [];
618
956
 
957
+ const traceEventDepths: Record<TTraceEventType, number> = {
958
+ 'request.start': 0,
959
+ 'request.user': 1,
960
+ 'auth.decode': 1,
961
+ 'auth.route': 1,
962
+ 'auth.check.start': 2,
963
+ 'auth.check.rule': 3,
964
+ 'auth.check.result': 2,
965
+ 'auth.session': 1,
966
+ 'resolve.start': 1,
967
+ 'resolve.controller-route': 2,
968
+ 'resolve.routes-evaluated': 1,
969
+ 'resolve.route-skip': 2,
970
+ 'resolve.route-match': 2,
971
+ 'resolve.not-found': 1,
972
+ 'controller.start': 2,
973
+ 'controller.result': 2,
974
+ 'setup.options': 3,
975
+ 'context.create': 3,
976
+ 'page.data': 3,
977
+ 'ssr.payload': 3,
978
+ 'render.start': 2,
979
+ 'render.end': 2,
980
+ 'response.send': 1,
981
+ 'request.finish': 0,
982
+ error: 0,
983
+ };
984
+
985
+ const getTraceEventDepth = (event: TRequestTrace['events'][number]) => traceEventDepths[event.type] ?? 0;
986
+
987
+ const authEventTypes: TTraceEventType[] = [
988
+ 'auth.decode',
989
+ 'auth.route',
990
+ 'auth.check.start',
991
+ 'auth.check.rule',
992
+ 'auth.check.result',
993
+ 'auth.session',
994
+ ];
995
+
619
996
  const getSummary = (session: TProfilerNavigationSession): TSessionSummary => {
620
997
  const primaryTrace =
621
998
  session.traces.find((trace) => trace.kind === 'initial-root' && trace.trace) ||
@@ -665,90 +1042,354 @@ const SummaryRow = ({ label, value }: { label: string; value: React.ReactNode })
665
1042
  </div>
666
1043
  );
667
1044
 
668
- const ApiRequestEntry = ({
669
- durationMs,
670
- errorMessage,
671
- finishedAt,
672
- label,
673
- method,
674
- path,
675
- requestData,
676
- result,
677
- startedAt,
678
- statusCode,
679
- statusLabel,
680
- tags,
1045
+ const JsonCodeBlock = ({ value }: { value: string }) => (
1046
+ <pre className="proteum-profiler__mono proteum-profiler__pre">{renderHighlightedJson(value)}</pre>
1047
+ );
1048
+
1049
+ const formatTraceCallDisplay = (call: TTraceCall) => {
1050
+ if (call.path.startsWith('/api/')) {
1051
+ return formatProfilerRequestReference({
1052
+ fallbackLabel: call.label,
1053
+ method: call.method,
1054
+ path: call.path,
1055
+ requestData: call.requestData,
1056
+ });
1057
+ }
1058
+
1059
+ const rawReference = `${call.method} ${call.path}`.trim();
1060
+ if (call.label && rawReference) return `${call.label} (${rawReference})`;
1061
+ return call.label || rawReference || 'request';
1062
+ };
1063
+
1064
+ const formatSessionTraceDisplay = (traceItem: TProfilerSessionTrace) => {
1065
+ if (traceItem.path.startsWith('/api/')) {
1066
+ return formatProfilerRequestReference({
1067
+ fallbackLabel: traceItem.label,
1068
+ method: traceItem.method,
1069
+ path: traceItem.path,
1070
+ requestData: getTraceRequestData(traceItem.trace),
1071
+ });
1072
+ }
1073
+
1074
+ return traceItem.label || formatProfilerRequestReference({ method: traceItem.method, path: traceItem.path });
1075
+ };
1076
+
1077
+ const ApiRequestListEntry = ({
1078
+ isSelected,
1079
+ item,
1080
+ onSelect,
681
1081
  }: {
682
- durationMs?: number;
683
- errorMessage?: string;
684
- finishedAt?: string;
685
- label?: string;
686
- method: string;
687
- path: string;
688
- requestData?: TTraceSummaryValue;
689
- result?: TTraceSummaryValue;
690
- startedAt: string;
691
- statusCode?: number;
692
- statusLabel?: string;
693
- tags: string[];
1082
+ isSelected: boolean;
1083
+ item: TApiRequestItem;
1084
+ onSelect: () => void;
694
1085
  }) => {
695
- const [isOpen, setOpen] = React.useState(false);
696
- const statusText = statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
1086
+ const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
697
1087
 
698
1088
  return (
699
- <>
700
- <button className="proteum-profiler__row proteum-profiler__row--interactive" onClick={() => setOpen((current) => !current)} type="button">
701
- <div className="proteum-profiler__rowHeader">
702
- <strong>{formatApiReference(method, path, requestData, label)}</strong>
703
- <span className="proteum-profiler__mono proteum-profiler__muted">
704
- {formatDuration(durationMs)} | {statusText}
1089
+ <button
1090
+ aria-pressed={isSelected}
1091
+ className={`proteum-profiler__row proteum-profiler__row--interactive ${isSelected ? 'proteum-profiler__row--selected' : ''}`}
1092
+ onClick={onSelect}
1093
+ type="button"
1094
+ >
1095
+ <div className="proteum-profiler__rowHeader">
1096
+ <strong>{formatApiReference(item.method, item.path, item.requestData, item.label)}</strong>
1097
+ <span className="proteum-profiler__mono proteum-profiler__muted">
1098
+ {formatDuration(item.durationMs)} | {statusText}
1099
+ </span>
1100
+ </div>
1101
+ <div className="proteum-profiler__tags">
1102
+ {item.tags.map((tag) => (
1103
+ <span className="proteum-profiler__tag" key={`${item.id}:${tag}`}>
1104
+ {tag}
705
1105
  </span>
1106
+ ))}
1107
+ {item.errorMessage ? <span className="proteum-profiler__tag">{truncate(item.errorMessage, 72)}</span> : null}
1108
+ </div>
1109
+ </button>
1110
+ );
1111
+ };
1112
+
1113
+ const ApiRequestSidebar = ({ item }: { item?: TApiRequestItem }) => {
1114
+ if (!item) {
1115
+ return (
1116
+ <aside className="proteum-profiler__sidebar">
1117
+ <div className="proteum-profiler__sidebarScroller">
1118
+ <div className="proteum-profiler__sidebarHeader">
1119
+ <div className="proteum-profiler__sidebarEyebrow">Request details</div>
1120
+ <div className="proteum-profiler__sidebarEmpty">
1121
+ Select a request to inspect its payload, result, and timing.
1122
+ </div>
1123
+ </div>
706
1124
  </div>
707
- {method || path ? <div className="proteum-profiler__mono proteum-profiler__muted">{method} {path}</div> : null}
708
- <div className="proteum-profiler__tags">
709
- {tags.map((tag) => (
710
- <span className="proteum-profiler__tag" key={`${label || method}:${path}:${tag}`}>
711
- {tag}
712
- </span>
713
- ))}
714
- {errorMessage ? <span className="proteum-profiler__tag">{truncate(errorMessage, 72)}</span> : null}
1125
+ </aside>
1126
+ );
1127
+ }
1128
+
1129
+ const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
1130
+
1131
+ return (
1132
+ <aside className="proteum-profiler__sidebar">
1133
+ <div className="proteum-profiler__sidebarScroller">
1134
+ <div className="proteum-profiler__sidebarHeader">
1135
+ <div className="proteum-profiler__sidebarEyebrow">{item.groupLabel}</div>
1136
+ <div className="proteum-profiler__sidebarTitle">
1137
+ <strong>{formatApiReference(item.method, item.path, item.requestData, item.label)}</strong>
1138
+ </div>
1139
+ <div className="proteum-profiler__mono proteum-profiler__muted">
1140
+ {formatProfilerRequestReference({
1141
+ fallbackLabel: item.label,
1142
+ method: item.method,
1143
+ path: item.path,
1144
+ requestData: item.requestData,
1145
+ })}
1146
+ </div>
1147
+ </div>
1148
+
1149
+ <div className="proteum-profiler__metrics">
1150
+ <SummaryRow label="Status" value={statusText} />
1151
+ <SummaryRow label="Duration" value={formatDuration(item.durationMs)} />
1152
+ <SummaryRow label="Started" value={formatTimestamp(item.startedAt)} />
1153
+ <SummaryRow label="Finished" value={item.finishedAt ? formatTimestamp(item.finishedAt) : 'pending'} />
1154
+ <SummaryRow
1155
+ label="Endpoint"
1156
+ value={formatProfilerRequestReference({
1157
+ fallbackLabel: item.label,
1158
+ method: item.method,
1159
+ path: item.path,
1160
+ requestData: item.requestData,
1161
+ })}
1162
+ />
1163
+ </div>
1164
+
1165
+ {item.tags.length > 0 ? (
1166
+ <div className="proteum-profiler__sidebarSection">
1167
+ <div className="proteum-profiler__sidebarSectionTitle">Tags</div>
1168
+ <div className="proteum-profiler__tags">
1169
+ {item.tags.map((tag) => (
1170
+ <span className="proteum-profiler__tag" key={`${item.id}:detail:${tag}`}>
1171
+ {tag}
1172
+ </span>
1173
+ ))}
1174
+ </div>
1175
+ </div>
1176
+ ) : null}
1177
+
1178
+ <div className="proteum-profiler__sidebarSection">
1179
+ <div className="proteum-profiler__sidebarSectionTitle">Arguments</div>
1180
+ <JsonCodeBlock value={formatSummaryJson(item.requestData)} />
1181
+ </div>
1182
+
1183
+ <div className="proteum-profiler__sidebarSection">
1184
+ <div className="proteum-profiler__sidebarSectionTitle">Result</div>
1185
+ <JsonCodeBlock value={formatSummaryJson(item.result)} />
1186
+ </div>
1187
+
1188
+ {item.errorMessage ? (
1189
+ <div className="proteum-profiler__sidebarSection">
1190
+ <div className="proteum-profiler__sidebarSectionTitle">Error</div>
1191
+ <div className="proteum-profiler__mono">{item.errorMessage}</div>
1192
+ </div>
1193
+ ) : null}
1194
+ </div>
1195
+ </aside>
1196
+ );
1197
+ };
1198
+
1199
+ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
1200
+ const syncItems: TApiRequestItem[] = session.traces
1201
+ .flatMap((trace) => trace.trace?.calls.filter((call) => call.origin !== 'client-async') || [])
1202
+ .map((call: TTraceCall) => ({
1203
+ id: call.id,
1204
+ groupLabel: 'Synchronous call',
1205
+ durationMs: call.durationMs,
1206
+ errorMessage: call.errorMessage,
1207
+ finishedAt: call.finishedAt,
1208
+ label: call.label,
1209
+ method: call.method,
1210
+ path: call.path,
1211
+ requestData: call.requestData,
1212
+ result: call.result,
1213
+ startedAt: call.startedAt,
1214
+ statusCode: call.statusCode,
1215
+ tags: [
1216
+ call.origin,
1217
+ ...(call.fetcherId ? [`fetcher:${call.fetcherId}`] : []),
1218
+ ...call.requestDataKeys.map((key) => `arg:${key}`),
1219
+ ...call.resultKeys.map((key) => `res:${key}`),
1220
+ ],
1221
+ }));
1222
+ const asyncItems: TApiRequestItem[] = session.traces
1223
+ .filter((trace) => trace.kind === 'async')
1224
+ .map((trace) => ({
1225
+ id: trace.id,
1226
+ groupLabel: 'Async request',
1227
+ durationMs: trace.durationMs,
1228
+ errorMessage: trace.errorMessage || trace.trace?.errorMessage,
1229
+ finishedAt: trace.finishedAt,
1230
+ label: trace.label,
1231
+ method: trace.method,
1232
+ path: trace.path,
1233
+ requestData: getTraceRequestData(trace.trace),
1234
+ result: getTraceResultData(trace.trace),
1235
+ startedAt: trace.startedAt,
1236
+ statusCode: trace.trace?.statusCode,
1237
+ statusLabel: trace.status,
1238
+ tags: [trace.status, ...(trace.requestId ? [`request:${trace.requestId}`] : [])],
1239
+ }));
1240
+ const requestItems = [...syncItems, ...asyncItems];
1241
+ const [selectedRequestId, setSelectedRequestId] = React.useState<string | undefined>(() => requestItems[0]?.id);
1242
+
1243
+ React.useEffect(() => {
1244
+ if (requestItems.some((item) => item.id === selectedRequestId)) return;
1245
+ setSelectedRequestId(requestItems[0]?.id);
1246
+ }, [requestItems, selectedRequestId]);
1247
+
1248
+ const selectedItem = requestItems.find((item) => item.id === selectedRequestId) || requestItems[0];
1249
+
1250
+ return (
1251
+ <div className="proteum-profiler__requestWorkspace">
1252
+ <div className="proteum-profiler__requestGroups">
1253
+ <div className="proteum-profiler__requestGroup">
1254
+ <div className="proteum-profiler__requestGroupHeader">
1255
+ <div className="proteum-profiler__sectionTitle">Synchronous calls</div>
1256
+ <div className="proteum-profiler__requestGroupCount">
1257
+ {syncItems.length} item{syncItems.length === 1 ? '' : 's'}
1258
+ </div>
1259
+ </div>
1260
+
1261
+ {syncItems.length === 0 ? (
1262
+ <div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
1263
+ ) : (
1264
+ <div className="proteum-profiler__list">
1265
+ {syncItems.map((item) => (
1266
+ <ApiRequestListEntry
1267
+ isSelected={item.id === selectedItem?.id}
1268
+ item={item}
1269
+ key={item.id}
1270
+ onSelect={() => setSelectedRequestId(item.id)}
1271
+ />
1272
+ ))}
1273
+ </div>
1274
+ )}
715
1275
  </div>
716
- </button>
717
-
718
- {isOpen ? (
719
- <div className="proteum-profiler__detail">
720
- <div className="proteum-profiler__detailLine">
721
- <div className="proteum-profiler__detailLabel">Performance</div>
722
- <div className="proteum-profiler__mono">
723
- duration={formatDuration(durationMs)} | status={statusText} | started={formatTimestamp(startedAt)}
724
- {finishedAt ? ` | finished=${formatTimestamp(finishedAt)}` : ''}
1276
+
1277
+ <div className="proteum-profiler__requestGroup">
1278
+ <div className="proteum-profiler__requestGroupHeader">
1279
+ <div className="proteum-profiler__sectionTitle">Async requests</div>
1280
+ <div className="proteum-profiler__requestGroupCount">
1281
+ {asyncItems.length} item{asyncItems.length === 1 ? '' : 's'}
725
1282
  </div>
726
1283
  </div>
727
- <div className="proteum-profiler__detailLine">
728
- <div className="proteum-profiler__detailLabel">Arguments</div>
729
- <pre className="proteum-profiler__mono proteum-profiler__pre">{formatSummaryJson(requestData)}</pre>
1284
+
1285
+ {asyncItems.length === 0 ? (
1286
+ <div className="proteum-profiler__empty">No async API calls captured.</div>
1287
+ ) : (
1288
+ <div className="proteum-profiler__list">
1289
+ {asyncItems.map((item) => (
1290
+ <ApiRequestListEntry
1291
+ isSelected={item.id === selectedItem?.id}
1292
+ item={item}
1293
+ key={item.id}
1294
+ onSelect={() => setSelectedRequestId(item.id)}
1295
+ />
1296
+ ))}
1297
+ </div>
1298
+ )}
1299
+ </div>
1300
+ </div>
1301
+
1302
+ <ApiRequestSidebar item={selectedItem} />
1303
+ </div>
1304
+ );
1305
+ };
1306
+
1307
+ const getTraceEventKey = (traceId: string, event: TRequestTrace['events'][number]) => `${traceId}:${event.index}`;
1308
+
1309
+ const TraceEventSidebar = ({
1310
+ event,
1311
+ label,
1312
+ trace,
1313
+ }: {
1314
+ event?: TRequestTrace['events'][number];
1315
+ label: string;
1316
+ trace?: TRequestTrace;
1317
+ }) => {
1318
+ if (!event) {
1319
+ return (
1320
+ <aside className="proteum-profiler__sidebar">
1321
+ <div className="proteum-profiler__sidebarScroller">
1322
+ <div className="proteum-profiler__sidebarHeader">
1323
+ <div className="proteum-profiler__sidebarEyebrow">{label}</div>
1324
+ <div className="proteum-profiler__sidebarEmpty">Select an event to inspect its timing and payload.</div>
730
1325
  </div>
731
- <div className="proteum-profiler__detailLine">
732
- <div className="proteum-profiler__detailLabel">Result</div>
733
- <pre className="proteum-profiler__mono proteum-profiler__pre">{formatSummaryJson(result)}</pre>
1326
+ </div>
1327
+ </aside>
1328
+ );
1329
+ }
1330
+
1331
+ return (
1332
+ <aside className="proteum-profiler__sidebar">
1333
+ <div className="proteum-profiler__sidebarScroller">
1334
+ <div className="proteum-profiler__sidebarHeader">
1335
+ <div className="proteum-profiler__sidebarEyebrow">{label}</div>
1336
+ <div className="proteum-profiler__sidebarTitle">
1337
+ <strong>{event.type}</strong>
734
1338
  </div>
735
- {errorMessage ? (
736
- <div className="proteum-profiler__detailLine">
737
- <div className="proteum-profiler__detailLabel">Error</div>
738
- <div className="proteum-profiler__mono">{errorMessage}</div>
1339
+ {trace ? (
1340
+ <div className="proteum-profiler__mono proteum-profiler__muted">
1341
+ {formatProfilerRequestReference({
1342
+ method: trace.method,
1343
+ path: trace.path,
1344
+ requestData: getTraceRequestData(trace),
1345
+ })}
739
1346
  </div>
740
1347
  ) : null}
741
1348
  </div>
742
- ) : null}
743
- </>
1349
+
1350
+ <div className="proteum-profiler__metrics">
1351
+ <SummaryRow label="Elapsed" value={formatDuration(event.elapsedMs)} />
1352
+ <SummaryRow label="Captured" value={formatTimestamp(event.at)} />
1353
+ <SummaryRow label="Trace" value={trace?.id || 'n/a'} />
1354
+ </div>
1355
+
1356
+ <div className="proteum-profiler__sidebarSection">
1357
+ <div className="proteum-profiler__sidebarSectionTitle">Summary</div>
1358
+ <div className="proteum-profiler__tags">
1359
+ {Object.entries(event.details).map(([key, value]) => (
1360
+ <span className="proteum-profiler__tag" key={`${trace?.id || 'trace'}:${event.index}:detail:${key}`}>
1361
+ {key}:{truncate(renderSummaryValue(value), 72)}
1362
+ </span>
1363
+ ))}
1364
+ </div>
1365
+ </div>
1366
+
1367
+ <div className="proteum-profiler__sidebarSection">
1368
+ <div className="proteum-profiler__sidebarSectionTitle">Details</div>
1369
+ <JsonCodeBlock value={formatTraceEventDetailsJson(event.details)} />
1370
+ </div>
1371
+ </div>
1372
+ </aside>
744
1373
  );
745
1374
  };
746
1375
 
747
- const TraceRows = ({ trace }: { trace: TRequestTrace }) => (
1376
+ const TraceRows = ({
1377
+ onSelect,
1378
+ selectedEventKey,
1379
+ trace,
1380
+ }: {
1381
+ onSelect: (selectionKey: string) => void;
1382
+ selectedEventKey?: string;
1383
+ trace: TRequestTrace;
1384
+ }) => (
748
1385
  <div className="proteum-profiler__section">
749
1386
  <div className="proteum-profiler__sectionHeader">
750
1387
  <div className="proteum-profiler__sectionTitle">
751
- {trace.method} {trace.path}
1388
+ {formatProfilerRequestReference({
1389
+ method: trace.method,
1390
+ path: trace.path,
1391
+ requestData: getTraceRequestData(trace),
1392
+ })}
752
1393
  </div>
753
1394
  <div className="proteum-profiler__mono proteum-profiler__muted">{trace.id}</div>
754
1395
  </div>
@@ -758,9 +1399,7 @@ const TraceRows = ({ trace }: { trace: TRequestTrace }) => (
758
1399
  {trace.calls.map((call) => (
759
1400
  <div className="proteum-profiler__row" key={call.id}>
760
1401
  <div className="proteum-profiler__rowHeader">
761
- <strong>
762
- {call.label} {call.method ? `(${call.method} ${call.path})` : ''}
763
- </strong>
1402
+ <strong>{formatTraceCallDisplay(call)}</strong>
764
1403
  <span className="proteum-profiler__mono proteum-profiler__muted">
765
1404
  {formatDuration(call.durationMs)}
766
1405
  {call.statusCode !== undefined ? ` | ${call.statusCode}` : ''}
@@ -787,28 +1426,472 @@ const TraceRows = ({ trace }: { trace: TRequestTrace }) => (
787
1426
  )}
788
1427
 
789
1428
  <div className="proteum-profiler__list">
790
- {trace.events.map((event) => (
791
- <div className="proteum-profiler__row" key={`${trace.id}:${event.index}`}>
792
- <div className="proteum-profiler__rowHeader">
793
- <strong>{event.type}</strong>
794
- <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(event.elapsedMs)}</span>
795
- </div>
796
- <div className="proteum-profiler__tags">
797
- {Object.entries(event.details).map(([key, value]) => (
798
- <span className="proteum-profiler__tag" key={`${trace.id}:${event.index}:${key}`}>
799
- {key}:{truncate(renderSummaryValue(value), 72)}
800
- </span>
801
- ))}
802
- </div>
1429
+ {trace.events.map((event) => {
1430
+ const selectionKey = getTraceEventKey(trace.id, event);
1431
+
1432
+ return (
1433
+ <TraceEventEntry
1434
+ event={event}
1435
+ isSelected={selectionKey === selectedEventKey}
1436
+ key={selectionKey}
1437
+ onSelect={() => onSelect(selectionKey)}
1438
+ traceId={trace.id}
1439
+ />
1440
+ );
1441
+ })}
1442
+ </div>
1443
+ </div>
1444
+ );
1445
+
1446
+ const AuthTraceSection = ({
1447
+ authEvents,
1448
+ label,
1449
+ onSelect,
1450
+ selectedEventKey,
1451
+ trace,
1452
+ }: {
1453
+ authEvents: TRequestTrace['events'];
1454
+ label: string;
1455
+ onSelect: (selectionKey: string) => void;
1456
+ selectedEventKey?: string;
1457
+ trace: TRequestTrace;
1458
+ }) => (
1459
+ <div className="proteum-profiler__section">
1460
+ <div className="proteum-profiler__sectionHeader">
1461
+ <div>
1462
+ <div className="proteum-profiler__sectionTitle">{label}</div>
1463
+ <div className="proteum-profiler__mono proteum-profiler__muted">
1464
+ {formatProfilerRequestReference({
1465
+ method: trace.method,
1466
+ path: trace.path,
1467
+ requestData: getTraceRequestData(trace),
1468
+ })}
803
1469
  </div>
804
- ))}
1470
+ </div>
1471
+ <div className="proteum-profiler__actions">
1472
+ <span className="proteum-profiler__tag">capture:{trace.capture}</span>
1473
+ <span className="proteum-profiler__tag">events:{authEvents.length}</span>
1474
+ {trace.statusCode !== undefined ? <span className="proteum-profiler__tag">status:{trace.statusCode}</span> : null}
1475
+ </div>
1476
+ </div>
1477
+
1478
+ <div className="proteum-profiler__list">
1479
+ {authEvents.map((event) => {
1480
+ const selectionKey = getTraceEventKey(trace.id, event);
1481
+
1482
+ return (
1483
+ <TraceEventEntry
1484
+ event={event}
1485
+ isSelected={selectionKey === selectedEventKey}
1486
+ key={selectionKey}
1487
+ onSelect={() => onSelect(selectionKey)}
1488
+ traceId={trace.id}
1489
+ />
1490
+ );
1491
+ })}
805
1492
  </div>
806
1493
  </div>
807
1494
  );
808
1495
 
809
- const SimpleSection = ({ empty, rows, title }: { empty: string; rows: Array<{ key: string; title: string; value: string }>; title: string }) => (
1496
+ const TraceEventEntry = ({
1497
+ event,
1498
+ isSelected,
1499
+ onSelect,
1500
+ traceId,
1501
+ }: {
1502
+ event: TRequestTrace['events'][number];
1503
+ isSelected: boolean;
1504
+ onSelect: () => void;
1505
+ traceId: string;
1506
+ }) => {
1507
+ const depth = getTraceEventDepth(event);
1508
+
1509
+ return (
1510
+ <button
1511
+ aria-pressed={isSelected}
1512
+ className={`proteum-profiler__row proteum-profiler__row--interactive proteum-profiler__traceEventRow ${isSelected ? 'proteum-profiler__row--selected' : ''}`}
1513
+ onClick={onSelect}
1514
+ style={
1515
+ {
1516
+ '--profiler-trace-depth': depth,
1517
+ '--profiler-trace-guide-opacity': depth > 0 ? 1 : 0,
1518
+ } as React.CSSProperties
1519
+ }
1520
+ type="button"
1521
+ >
1522
+ <div className="proteum-profiler__rowHeader">
1523
+ <strong>{event.type}</strong>
1524
+ <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(event.elapsedMs)}</span>
1525
+ </div>
1526
+ <div className="proteum-profiler__tags">
1527
+ {Object.entries(event.details).map(([key, value]) => (
1528
+ <span className="proteum-profiler__tag" key={`${traceId}:${event.index}:${key}`}>
1529
+ {key}:{truncate(renderSummaryValue(value), 72)}
1530
+ </span>
1531
+ ))}
1532
+ </div>
1533
+ </button>
1534
+ );
1535
+ };
1536
+
1537
+ type TTraceEventInspectorSelection = {
1538
+ event: TRequestTrace['events'][number];
1539
+ key: string;
1540
+ label: string;
1541
+ trace: TRequestTrace;
1542
+ };
1543
+
1544
+ const readDateMs = (value?: string) => {
1545
+ if (!value) return undefined;
1546
+ const ms = new Date(value).valueOf();
1547
+ return Number.isFinite(ms) ? ms : undefined;
1548
+ };
1549
+
1550
+ const getTimelineDurationColor = (durationMs?: number) => {
1551
+ if (durationMs === undefined) return '#93c5fd';
1552
+ if (durationMs >= 800) return '#ef4444';
1553
+ if (durationMs >= 450) return '#f97316';
1554
+ if (durationMs >= 220) return '#f59e0b';
1555
+ if (durationMs >= 100) return '#3b82f6';
1556
+ return '#22c55e';
1557
+ };
1558
+
1559
+ const escapeHtml = (value: string) =>
1560
+ value
1561
+ .replace(/&/g, '&amp;')
1562
+ .replace(/</g, '&lt;')
1563
+ .replace(/>/g, '&gt;')
1564
+ .replace(/"/g, '&quot;')
1565
+ .replace(/'/g, '&#39;');
1566
+
1567
+ const timelineWaterfallMinDurationMs = 6;
1568
+ const timelineWaterfallBarHeight = 15;
1569
+ const timelineWaterfallRowGap = 1;
1570
+ const timelineWaterfallRowHeight = timelineWaterfallBarHeight + timelineWaterfallRowGap;
1571
+
1572
+ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) => {
1573
+ const [ApexChartComponent, setApexChartComponent] = React.useState<unknown>(null);
1574
+
1575
+ React.useEffect(() => {
1576
+ let isDisposed = false;
1577
+
1578
+ void import('react-apexcharts').then((module) => {
1579
+ if (isDisposed) return;
1580
+ setApexChartComponent(() => module.default);
1581
+ });
1582
+
1583
+ return () => {
1584
+ isDisposed = true;
1585
+ };
1586
+ }, []);
1587
+
1588
+ const sessionStartMs = readDateMs(session.startedAt) ?? 0;
1589
+ const rawItems = session.traces.flatMap((traceItem) => {
1590
+ const trace = traceItem.trace;
1591
+ if (!trace) return [];
1592
+
1593
+ const traceStartMs = readDateMs(trace.startedAt) ?? sessionStartMs;
1594
+ const traceFinishedMs = readDateMs(trace.finishedAt) ?? (trace.durationMs !== undefined ? traceStartMs + trace.durationMs : undefined);
1595
+ const traceLabel = formatSessionTraceDisplay(traceItem);
1596
+
1597
+ return trace.events.map((event, index): Omit<TTimelineWaterfallEventItem, 'chartLabel' | 'color' | 'endOffsetMs' | 'startOffsetMs'> => {
1598
+ const nextEvent = trace.events[index + 1];
1599
+ const startMs = readDateMs(event.at) ?? traceStartMs + event.elapsedMs;
1600
+ const nextStartMs = nextEvent ? readDateMs(nextEvent.at) ?? traceStartMs + nextEvent.elapsedMs : undefined;
1601
+ const endMs = Math.max(startMs + 1, nextStartMs ?? traceFinishedMs ?? startMs + 1);
1602
+
1603
+ return {
1604
+ durationMs: Math.max(1, endMs - startMs),
1605
+ endMs,
1606
+ event,
1607
+ startMs,
1608
+ traceLabel,
1609
+ };
1610
+ });
1611
+ });
1612
+
1613
+ const sortedItems = [...rawItems].sort((left, right) => left.startMs - right.startMs || left.event.index - right.event.index);
1614
+ const chartStartMs = sortedItems.length > 0 ? Math.min(...sortedItems.map((item) => item.startMs)) : 0;
1615
+ const chartEndMs = sortedItems.length > 0 ? Math.max(...sortedItems.map((item) => item.endMs)) : chartStartMs + 1;
1616
+ const totalDurationMs = Math.max(chartEndMs - chartStartMs, 1);
1617
+ const items: TTimelineWaterfallEventItem[] = sortedItems
1618
+ .filter((item) => item.durationMs >= timelineWaterfallMinDurationMs)
1619
+ .map((item) => ({
1620
+ ...item,
1621
+ chartLabel: truncate(`${item.event.type} | ${item.traceLabel}`, 84),
1622
+ color: getTimelineDurationColor(item.durationMs),
1623
+ endOffsetMs: item.endMs - chartStartMs,
1624
+ startOffsetMs: item.startMs - chartStartMs,
1625
+ }));
1626
+ const chartHeight = Math.max(260, items.length * timelineWaterfallRowHeight + 24);
1627
+ const ChartComponent = ApexChartComponent as any;
1628
+
1629
+ const series = [
1630
+ {
1631
+ data: items.map((item) => ({
1632
+ fillColor: item.color,
1633
+ x: item.chartLabel,
1634
+ y: [item.startOffsetMs, item.endOffsetMs],
1635
+ })),
1636
+ name: 'Timeline events',
1637
+ },
1638
+ ];
1639
+
1640
+ const options = {
1641
+ chart: {
1642
+ animations: { enabled: false },
1643
+ background: 'transparent',
1644
+ foreColor: '#627186',
1645
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
1646
+ toolbar: { show: false },
1647
+ type: 'rangeBar',
1648
+ zoom: { enabled: false },
1649
+ },
1650
+ dataLabels: {
1651
+ enabled: false,
1652
+ },
1653
+ fill: {
1654
+ opacity: 1,
1655
+ },
1656
+ grid: {
1657
+ borderColor: 'rgba(19, 32, 51, 0.08)',
1658
+ padding: { bottom: 0, left: 0, right: 0, top: 4 },
1659
+ xaxis: { lines: { show: true } },
1660
+ yaxis: { lines: { show: false } },
1661
+ },
1662
+ legend: {
1663
+ show: false,
1664
+ },
1665
+ noData: {
1666
+ style: {
1667
+ color: '#627186',
1668
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
1669
+ fontSize: '11px',
1670
+ },
1671
+ text: 'No timeline events captured.',
1672
+ },
1673
+ plotOptions: {
1674
+ bar: {
1675
+ barHeight: timelineWaterfallBarHeight,
1676
+ borderRadius: 2,
1677
+ horizontal: true,
1678
+ rangeBarGroupRows: false,
1679
+ },
1680
+ },
1681
+ stroke: {
1682
+ colors: ['#ffffff'],
1683
+ width: 1,
1684
+ },
1685
+ tooltip: {
1686
+ custom: ({ dataPointIndex }: { dataPointIndex: number }) => {
1687
+ const item = items[dataPointIndex];
1688
+ if (!item) return '';
1689
+
1690
+ return `
1691
+ <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.event.type)}</div>
1693
+ <div style="color:#627186;">${escapeHtml(item.traceLabel)}</div>
1694
+ <div style="margin-top:6px; color:#627186;">Start: +${Math.round(item.startOffsetMs)} ms</div>
1695
+ <div style="color:#627186;">End: +${Math.round(item.endOffsetMs)} ms</div>
1696
+ <div style="color:#627186;">Span: ${escapeHtml(formatDuration(item.durationMs))}</div>
1697
+ </div>
1698
+ `;
1699
+ },
1700
+ },
1701
+ xaxis: {
1702
+ axisBorder: { show: false },
1703
+ axisTicks: { show: false },
1704
+ labels: {
1705
+ formatter: (value: string | number) => `${Math.round(Number(value))} ms`,
1706
+ style: {
1707
+ colors: '#627186',
1708
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
1709
+ fontSize: '10px',
1710
+ },
1711
+ },
1712
+ max: totalDurationMs,
1713
+ min: 0,
1714
+ tickAmount: Math.min(6, Math.max(2, items.length > 0 ? 6 : 2)),
1715
+ type: 'numeric',
1716
+ },
1717
+ yaxis: {
1718
+ show: false,
1719
+ labels: {
1720
+ show: false,
1721
+ },
1722
+ },
1723
+ };
1724
+
1725
+ return (
1726
+ <div className="proteum-profiler__section">
1727
+ <div className="proteum-profiler__timelineChart">
1728
+ <div className="proteum-profiler__timelineChartMeta">
1729
+ <span className="proteum-profiler__mono proteum-profiler__muted">
1730
+ {items.length} event{items.length === 1 ? '' : 's'}
1731
+ </span>
1732
+ <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(totalDurationMs)}</span>
1733
+ </div>
1734
+
1735
+ <div className="proteum-profiler__timelineChartCanvas" style={{ height: `${chartHeight}px` }}>
1736
+ {ChartComponent && items.length > 0 ? (
1737
+ <ChartComponent height={chartHeight} options={options} series={series} type="rangeBar" width="100%" />
1738
+ ) : items.length > 0 ? (
1739
+ <div className="proteum-profiler__empty">Loading waterfall chart...</div>
1740
+ ) : (
1741
+ <div className="proteum-profiler__empty">No timeline events were captured for this session.</div>
1742
+ )}
1743
+ </div>
1744
+ </div>
1745
+ </div>
1746
+ );
1747
+ };
1748
+
1749
+ const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) => {
1750
+ const selections: TTraceEventInspectorSelection[] = session.traces.flatMap((traceItem) =>
1751
+ traceItem.trace
1752
+ ? traceItem.trace.events.map((event) => ({
1753
+ event,
1754
+ key: getTraceEventKey(traceItem.trace!.id, event),
1755
+ label: formatSessionTraceDisplay(traceItem),
1756
+ trace: traceItem.trace!,
1757
+ }))
1758
+ : [],
1759
+ );
1760
+ const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(() => selections[0]?.key);
1761
+
1762
+ React.useEffect(() => {
1763
+ if (selections.some((selection) => selection.key === selectedEventKey)) return;
1764
+ setSelectedEventKey(selections[0]?.key);
1765
+ }, [selectedEventKey, selections]);
1766
+
1767
+ const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
1768
+
1769
+ return (
1770
+ <div className="proteum-profiler__splitColumn">
1771
+ <TimelineChart session={session} />
1772
+ <div className="proteum-profiler__splitView proteum-profiler__splitView--stacked">
1773
+ <div className="proteum-profiler__splitColumn">
1774
+ <div className="proteum-profiler__section">
1775
+ <div className="proteum-profiler__titleRow">
1776
+ <div className="proteum-profiler__sectionTitle">Navigation steps</div>
1777
+ </div>
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>
1798
+
1799
+ {session.traces.map((traceItem) =>
1800
+ traceItem.trace ? (
1801
+ <TraceRows
1802
+ key={traceItem.id}
1803
+ onSelect={setSelectedEventKey}
1804
+ selectedEventKey={selectedEventKey}
1805
+ trace={traceItem.trace}
1806
+ />
1807
+ ) : (
1808
+ <div className="proteum-profiler__row" key={traceItem.id}>
1809
+ <div className="proteum-profiler__rowHeader">
1810
+ <strong>{formatSessionTraceDisplay(traceItem)}</strong>
1811
+ <span className="proteum-profiler__mono proteum-profiler__muted">{traceItem.status}</span>
1812
+ </div>
1813
+ <div className="proteum-profiler__mono">
1814
+ {formatProfilerRequestReference({
1815
+ fallbackLabel: traceItem.label,
1816
+ method: traceItem.method,
1817
+ path: traceItem.path,
1818
+ requestData: getTraceRequestData(traceItem.trace),
1819
+ })}
1820
+ </div>
1821
+ </div>
1822
+ ),
1823
+ )}
1824
+ </div>
1825
+
1826
+ <TraceEventSidebar event={selected?.event} label={selected?.label || 'Trace event'} trace={selected?.trace} />
1827
+ </div>
1828
+ </div>
1829
+ );
1830
+ };
1831
+
1832
+ const AuthPanel = ({ session }: { session: TProfilerNavigationSession }) => {
1833
+ const authSections = session.traces.flatMap((traceItem) => {
1834
+ const authEvents = traceItem.trace ? findTraceEvents(traceItem.trace, authEventTypes) : [];
1835
+ return traceItem.trace && authEvents.length > 0
1836
+ ? [{ authEvents, id: traceItem.id, label: formatSessionTraceDisplay(traceItem), trace: traceItem.trace }]
1837
+ : [];
1838
+ });
1839
+ const selections: TTraceEventInspectorSelection[] = authSections.flatMap((section) =>
1840
+ section.authEvents.map((event) => ({
1841
+ event,
1842
+ key: getTraceEventKey(section.trace.id, event),
1843
+ label: `${section.label} event`,
1844
+ trace: section.trace,
1845
+ })),
1846
+ );
1847
+ const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(() => selections[0]?.key);
1848
+
1849
+ React.useEffect(() => {
1850
+ if (selections.some((selection) => selection.key === selectedEventKey)) return;
1851
+ setSelectedEventKey(selections[0]?.key);
1852
+ }, [selectedEventKey, selections]);
1853
+
1854
+ if (authSections.length === 0) return <div className="proteum-profiler__empty">No auth activity was captured for this session.</div>;
1855
+
1856
+ const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
1857
+
1858
+ return (
1859
+ <div className="proteum-profiler__splitView">
1860
+ <div className="proteum-profiler__splitColumn">
1861
+ {authSections.map((section) => (
1862
+ <AuthTraceSection
1863
+ authEvents={section.authEvents}
1864
+ key={section.id}
1865
+ label={section.label}
1866
+ onSelect={setSelectedEventKey}
1867
+ selectedEventKey={selectedEventKey}
1868
+ trace={section.trace}
1869
+ />
1870
+ ))}
1871
+ </div>
1872
+
1873
+ <TraceEventSidebar event={selected?.event} label={selected?.label || 'Auth event'} trace={selected?.trace} />
1874
+ </div>
1875
+ );
1876
+ };
1877
+
1878
+ const SimpleSection = ({
1879
+ empty,
1880
+ rows,
1881
+ showTitle = true,
1882
+ title,
1883
+ }: {
1884
+ empty: string;
1885
+ rows: Array<{ key: string; title: string; value: string }>;
1886
+ showTitle?: boolean;
1887
+ title: string;
1888
+ }) => (
810
1889
  <div className="proteum-profiler__section">
811
- <div className="proteum-profiler__sectionTitle">{title}</div>
1890
+ {showTitle ? (
1891
+ <div className="proteum-profiler__titleRow">
1892
+ <div className="proteum-profiler__sectionTitle">{title}</div>
1893
+ </div>
1894
+ ) : null}
812
1895
  {rows.length === 0 ? (
813
1896
  <div className="proteum-profiler__empty">{empty}</div>
814
1897
  ) : (
@@ -830,7 +1913,9 @@ const TextBlocks = ({ blocks }: { blocks: THumanTextBlock[] }) => (
830
1913
  <>
831
1914
  {blocks.map((block) => (
832
1915
  <div className="proteum-profiler__section" key={block.title}>
833
- <div className="proteum-profiler__sectionTitle">{block.title}</div>
1916
+ <div className="proteum-profiler__titleRow">
1917
+ <div className="proteum-profiler__sectionTitle">{block.title}</div>
1918
+ </div>
834
1919
  {block.items.length === 0 ? (
835
1920
  <div className="proteum-profiler__empty">{block.empty || 'none'}</div>
836
1921
  ) : (
@@ -873,44 +1958,11 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
873
1958
  }
874
1959
 
875
1960
  if (panel === 'timeline') {
876
- return (
877
- <div className="proteum-profiler__section">
878
- <div className="proteum-profiler__sectionTitle">Navigation steps</div>
879
- <div className="proteum-profiler__list">
880
- {session.steps.map((step) => (
881
- <div className="proteum-profiler__row" key={step.id}>
882
- <div className="proteum-profiler__rowHeader">
883
- <strong>{step.label}</strong>
884
- <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
885
- </div>
886
- <div className="proteum-profiler__tags">
887
- <span className="proteum-profiler__tag">{step.status}</span>
888
- {Object.entries(step.details || {}).map(([key, value]) => (
889
- <span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
890
- {key}:{String(value)}
891
- </span>
892
- ))}
893
- {step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
894
- </div>
895
- </div>
896
- ))}
897
- </div>
1961
+ return <TimelinePanel session={session} />;
1962
+ }
898
1963
 
899
- {session.traces.map((trace) =>
900
- trace.trace ? (
901
- <TraceRows key={trace.id} trace={trace.trace} />
902
- ) : (
903
- <div className="proteum-profiler__row" key={trace.id}>
904
- <div className="proteum-profiler__rowHeader">
905
- <strong>{trace.label}</strong>
906
- <span className="proteum-profiler__mono proteum-profiler__muted">{trace.status}</span>
907
- </div>
908
- <div className="proteum-profiler__mono">{trace.method} {trace.path}</div>
909
- </div>
910
- ),
911
- )}
912
- </div>
913
- );
1964
+ if (panel === 'auth') {
1965
+ return <AuthPanel session={session} />;
914
1966
  }
915
1967
 
916
1968
  if (panel === 'routing') {
@@ -930,6 +1982,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
930
1982
  .map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
931
1983
  .join(' '),
932
1984
  }))}
1985
+ showTitle={false}
933
1986
  title="Routing"
934
1987
  />
935
1988
  );
@@ -948,6 +2001,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
948
2001
  .join(' '),
949
2002
  }),
950
2003
  )}
2004
+ showTitle={false}
951
2005
  title="Controller"
952
2006
  />
953
2007
  );
@@ -964,74 +2018,14 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
964
2018
  .map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
965
2019
  .join(' '),
966
2020
  }))}
2021
+ showTitle={false}
967
2022
  title="SSR"
968
2023
  />
969
2024
  );
970
2025
  }
971
2026
 
972
2027
  if (panel === 'api') {
973
- const syncCalls = session.traces.flatMap((trace) =>
974
- trace.trace?.calls.filter((call) => call.origin !== 'client-async') || [],
975
- );
976
- const asyncTraces = session.traces.filter((trace) => trace.kind === 'async');
977
-
978
- return (
979
- <div className="proteum-profiler__section">
980
- <div className="proteum-profiler__sectionTitle">Synchronous calls</div>
981
- {syncCalls.length === 0 ? (
982
- <div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
983
- ) : (
984
- <div className="proteum-profiler__list">
985
- {syncCalls.map((call: TTraceCall) => (
986
- <ApiRequestEntry
987
- durationMs={call.durationMs}
988
- errorMessage={call.errorMessage}
989
- finishedAt={call.finishedAt}
990
- key={call.id}
991
- label={call.label}
992
- method={call.method}
993
- path={call.path}
994
- requestData={call.requestData}
995
- result={call.result}
996
- startedAt={call.startedAt}
997
- statusCode={call.statusCode}
998
- tags={[
999
- call.origin,
1000
- ...(call.fetcherId ? [`fetcher:${call.fetcherId}`] : []),
1001
- ...call.requestDataKeys.map((key) => `arg:${key}`),
1002
- ...call.resultKeys.map((key) => `res:${key}`),
1003
- ]}
1004
- />
1005
- ))}
1006
- </div>
1007
- )}
1008
-
1009
- <div className="proteum-profiler__sectionTitle">Async requests</div>
1010
- {asyncTraces.length === 0 ? (
1011
- <div className="proteum-profiler__empty">No async API calls captured.</div>
1012
- ) : (
1013
- <div className="proteum-profiler__list">
1014
- {asyncTraces.map((trace) => (
1015
- <ApiRequestEntry
1016
- durationMs={trace.durationMs}
1017
- errorMessage={trace.errorMessage || trace.trace?.errorMessage}
1018
- finishedAt={trace.finishedAt}
1019
- key={trace.id}
1020
- label={trace.label}
1021
- method={trace.method}
1022
- path={trace.path}
1023
- requestData={getTraceRequestData(trace.trace)}
1024
- result={getTraceResultData(trace.trace)}
1025
- startedAt={trace.startedAt}
1026
- statusCode={trace.trace?.statusCode}
1027
- statusLabel={trace.status}
1028
- tags={[trace.status, ...(trace.requestId ? [`request:${trace.requestId}`] : [])]}
1029
- />
1030
- ))}
1031
- </div>
1032
- )}
1033
- </div>
1034
- );
2028
+ return <ApiPanel session={session} />;
1035
2029
  }
1036
2030
 
1037
2031
  if (panel === 'explain') {
@@ -1230,13 +2224,15 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
1230
2224
  {execution ? (
1231
2225
  <div className="proteum-profiler__section">
1232
2226
  <div className="proteum-profiler__sectionTitle">Last result</div>
1233
- <pre className="proteum-profiler__mono proteum-profiler__pre">
1234
- {execution.result?.json !== undefined
1235
- ? formatStructuredValue(execution.result.json)
1236
- : execution.result
1237
- ? formatStructuredValue(execution.result.summary)
1238
- : execution.errorMessage || 'undefined'}
1239
- </pre>
2227
+ <JsonCodeBlock
2228
+ value={
2229
+ execution.result?.json !== undefined
2230
+ ? formatStructuredValue(execution.result.json)
2231
+ : execution.result
2232
+ ? formatStructuredValue(execution.result.summary)
2233
+ : execution.errorMessage || 'undefined'
2234
+ }
2235
+ />
1240
2236
  </div>
1241
2237
  ) : null}
1242
2238
  </div>
@@ -1361,7 +2357,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
1361
2357
  })),
1362
2358
  ];
1363
2359
 
1364
- return <SimpleSection empty="No errors captured." rows={errorRows} title="Errors" />;
2360
+ return <SimpleSection empty="No errors captured." rows={errorRows} showTitle={false} title="Errors" />;
1365
2361
  };
1366
2362
 
1367
2363
  export default function DevProfiler() {
@@ -1396,9 +2392,13 @@ export default function DevProfiler() {
1396
2392
  session.kind === 'client-navigation'
1397
2393
  ? session.label
1398
2394
  : primaryTrace
1399
- ? `${primaryTrace.statusCode || 'pending'} ${primaryTrace.method} ${primaryTrace.path}`
2395
+ ? `${primaryTrace.statusCode || 'pending'} ${formatProfilerRequestReference({
2396
+ method: primaryTrace.method,
2397
+ path: primaryTrace.path,
2398
+ requestData: getTraceRequestData(primaryTrace),
2399
+ })}`
1400
2400
  : session.label;
1401
- const recentSessions = state.sessions.slice(-6).reverse();
2401
+ const recentSessions: TProfilerNavigationSession[] = state.sessions.slice(-6).reverse();
1402
2402
 
1403
2403
  return (
1404
2404
  <div className="proteum-profiler">