proteum 2.1.1 → 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.
@@ -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;
@@ -357,6 +367,47 @@ const profilerStyles = `
357
367
  line-height: 1.45;
358
368
  }
359
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
+
360
411
  .proteum-profiler__mono {
361
412
  font-family: inherit;
362
413
  font-size: 11px;
@@ -371,15 +422,37 @@ const profilerStyles = `
371
422
  margin: 0;
372
423
  white-space: pre-wrap;
373
424
  word-break: break-word;
374
- padding-top: 8px;
425
+ padding: 10px 0 0;
426
+ border: none;
375
427
  border-top: 1px solid var(--profiler-line);
428
+ border-radius: 0;
429
+ background: transparent;
430
+ }
431
+
432
+ .proteum-profiler__jsonKey {
433
+ color: var(--profiler-brand);
434
+ }
435
+
436
+ .proteum-profiler__jsonString {
437
+ color: #0f766e;
438
+ }
439
+
440
+ .proteum-profiler__jsonNumber {
441
+ color: #b45309;
442
+ }
443
+
444
+ .proteum-profiler__jsonLiteral {
445
+ color: var(--profiler-error);
376
446
  }
377
447
 
378
448
  .proteum-profiler__detail {
379
449
  display: grid;
380
450
  gap: 10px;
381
- padding: 10px 0 14px;
382
- border-top: 1px dashed var(--profiler-line);
451
+ padding: 10px 0 0;
452
+ border: none;
453
+ border-top: 1px solid var(--profiler-line);
454
+ border-radius: 0;
455
+ background: transparent;
383
456
  }
384
457
 
385
458
  .proteum-profiler__detailLine {
@@ -423,11 +496,196 @@ const profilerStyles = `
423
496
  }
424
497
 
425
498
  .proteum-profiler__empty {
426
- padding: 12px 0;
427
- border-top: 1px dashed var(--profiler-line);
499
+ padding: 12px;
500
+ border-top: 1px solid var(--profiler-line);
501
+ color: var(--profiler-muted);
502
+ }
503
+
504
+ .proteum-profiler__requestWorkspace {
505
+ display: grid;
506
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
507
+ gap: 0;
508
+ align-items: stretch;
509
+ min-height: 100%;
510
+ height: 100%;
511
+ }
512
+
513
+ .proteum-profiler__splitView {
514
+ display: grid;
515
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
516
+ gap: 0;
517
+ align-items: stretch;
518
+ min-height: 100%;
519
+ height: 100%;
520
+ }
521
+
522
+ .proteum-profiler__splitView--stacked {
523
+ min-height: 0;
524
+ height: auto;
525
+ }
526
+
527
+ .proteum-profiler__splitColumn {
528
+ display: grid;
529
+ gap: 0;
530
+ min-width: 0;
531
+ align-content: start;
532
+ }
533
+
534
+ .proteum-profiler__requestGroups {
535
+ display: grid;
536
+ gap: 0;
537
+ min-width: 0;
538
+ }
539
+
540
+ .proteum-profiler__requestGroup {
541
+ display: grid;
542
+ gap: 0;
543
+ }
544
+
545
+ .proteum-profiler__requestGroupHeader {
546
+ display: flex;
547
+ align-items: center;
548
+ justify-content: space-between;
549
+ gap: 12px;
550
+ padding: 8px 10px;
551
+ background: var(--profiler-title-row-bg);
552
+ }
553
+
554
+ .proteum-profiler__requestGroupCount {
555
+ color: var(--profiler-muted);
556
+ font-size: 10px;
557
+ letter-spacing: 0.08em;
558
+ text-transform: uppercase;
559
+ }
560
+
561
+ .proteum-profiler__sidebar {
562
+ position: sticky;
563
+ top: 0;
564
+ display: flex;
565
+ align-self: stretch;
566
+ height: 100%;
567
+ min-height: 0;
568
+ padding: 0;
569
+ border: none;
570
+ border-left: 1px solid var(--profiler-line);
571
+ border-radius: 0;
572
+ background: transparent;
573
+ box-shadow: none;
574
+ }
575
+
576
+ .proteum-profiler__sidebarScroller {
577
+ display: grid;
578
+ flex: 1 1 auto;
579
+ gap: 0;
580
+ align-content: start;
581
+ height: 100%;
582
+ min-height: 0;
583
+ overflow: auto;
584
+ overscroll-behavior: contain;
585
+ scrollbar-width: thin;
586
+ }
587
+
588
+ .proteum-profiler__titleRow {
589
+ padding: 8px 10px;
590
+ background: var(--profiler-title-row-bg);
591
+ }
592
+
593
+ .proteum-profiler__sidebarHeader {
594
+ display: grid;
595
+ gap: 6px;
596
+ padding: 10px 12px 0;
597
+ }
598
+
599
+ .proteum-profiler__sidebarEyebrow,
600
+ .proteum-profiler__sidebarSectionTitle {
601
+ color: var(--profiler-muted);
602
+ font-size: 10px;
603
+ letter-spacing: 0.08em;
604
+ text-transform: uppercase;
605
+ }
606
+
607
+ .proteum-profiler__sidebarTitle {
608
+ font-size: 13px;
609
+ line-height: 1.5;
610
+ word-break: break-word;
611
+ }
612
+
613
+ .proteum-profiler__sidebarSection {
614
+ display: grid;
615
+ gap: 6px;
616
+ padding: 10px 12px 0;
617
+ border-top: 1px solid var(--profiler-line);
618
+ }
619
+
620
+ .proteum-profiler__sidebarScroller > .proteum-profiler__metrics {
621
+ border-top: 1px solid var(--profiler-line);
622
+ }
623
+
624
+ .proteum-profiler__sidebarEmpty {
625
+ font-size: 12px;
428
626
  color: var(--profiler-muted);
429
627
  }
430
628
 
629
+ .proteum-profiler__timelineChart {
630
+ display: grid;
631
+ gap: 0;
632
+ }
633
+
634
+ .proteum-profiler__timelineChartMeta {
635
+ display: flex;
636
+ align-items: center;
637
+ justify-content: space-between;
638
+ gap: 12px;
639
+ padding: 10px 12px 0;
640
+ }
641
+
642
+ .proteum-profiler__timelineChartCanvas {
643
+ position: relative;
644
+ padding: 8px 12px 12px;
645
+ border-top: 1px solid var(--profiler-line);
646
+ }
647
+
648
+ .proteum-profiler__timelineChartCanvas > * {
649
+ height: 100%;
650
+ }
651
+
652
+ .proteum-profiler__timelineChartCanvas canvas {
653
+ display: block;
654
+ width: 100%;
655
+ height: 100%;
656
+ }
657
+
658
+ .proteum-profiler__timelineChartCanvas .apexcharts-canvas,
659
+ .proteum-profiler__timelineChartCanvas .apexcharts-svg {
660
+ background: transparent !important;
661
+ }
662
+
663
+ .proteum-profiler__traceEventRow {
664
+ --profiler-trace-depth: 0;
665
+ --profiler-trace-guide-opacity: 0;
666
+ --profiler-trace-indent: calc(var(--profiler-trace-depth) * 18px);
667
+ }
668
+
669
+ .proteum-profiler__traceEventRow .proteum-profiler__rowHeader,
670
+ .proteum-profiler__traceEventRow .proteum-profiler__tags {
671
+ padding-inline-start: var(--profiler-trace-indent);
672
+ }
673
+
674
+ .proteum-profiler__traceEventRow .proteum-profiler__rowHeader {
675
+ position: relative;
676
+ }
677
+
678
+ .proteum-profiler__traceEventRow .proteum-profiler__rowHeader::before {
679
+ content: '';
680
+ position: absolute;
681
+ left: max(0px, calc(var(--profiler-trace-indent) - 8px));
682
+ top: 3px;
683
+ bottom: 3px;
684
+ width: 1px;
685
+ background: var(--profiler-line-strong);
686
+ opacity: var(--profiler-trace-guide-opacity);
687
+ }
688
+
431
689
  @media (max-width: 900px) {
432
690
  .proteum-profiler__panel {
433
691
  height: 50vh;
@@ -459,6 +717,33 @@ const profilerStyles = `
459
717
  .proteum-profiler__select {
460
718
  min-width: 132px;
461
719
  }
720
+
721
+ .proteum-profiler__requestWorkspace {
722
+ grid-template-columns: 1fr;
723
+ min-height: 0;
724
+ height: auto;
725
+ }
726
+
727
+ .proteum-profiler__splitView {
728
+ grid-template-columns: 1fr;
729
+ min-height: 0;
730
+ height: auto;
731
+ }
732
+
733
+ .proteum-profiler__sidebar {
734
+ position: static;
735
+ height: auto;
736
+ min-height: 0;
737
+ border-left: none;
738
+ border-top: 1px solid var(--profiler-line);
739
+ }
740
+
741
+ .proteum-profiler__sidebarScroller {
742
+ height: auto;
743
+ max-height: none;
744
+ min-height: 0;
745
+ }
746
+
462
747
  }
463
748
  `;
464
749
 
@@ -473,20 +758,49 @@ type TSessionSummary = {
473
758
  statusLabel: string;
474
759
  totalMs?: number;
475
760
  };
761
+ type TApiRequestItem = {
762
+ id: string;
763
+ groupLabel: string;
764
+ durationMs?: number;
765
+ errorMessage?: string;
766
+ finishedAt?: string;
767
+ label?: string;
768
+ method: string;
769
+ path: string;
770
+ requestData?: TTraceSummaryValue;
771
+ requestDataJson?: unknown;
772
+ result?: TTraceSummaryValue;
773
+ resultJson?: unknown;
774
+ startedAt: string;
775
+ statusCode?: number;
776
+ statusLabel?: string;
777
+ tags: string[];
778
+ };
779
+ type TWaterfallChartItem = {
780
+ barLabel: string;
781
+ color: string;
782
+ detailLines: string[];
783
+ endOffsetMs: number;
784
+ id: string;
785
+ startOffsetMs: number;
786
+ subtitle?: string;
787
+ title: string;
788
+ };
476
789
  type TProfilerState = ReturnType<typeof profilerRuntime.getState>;
477
790
 
478
791
  const panelLabels: Record<TProfilerPanel, string> = {
479
792
  summary: 'Summary',
480
793
  timeline: 'Timeline',
481
794
  routing: 'Routing',
795
+ auth: 'Auth',
482
796
  controller: 'Controller',
483
797
  ssr: 'SSR',
484
798
  api: 'API',
799
+ errors: 'Errors',
485
800
  explain: 'Explain',
486
801
  doctor: 'Doctor',
487
802
  commands: 'Commands',
488
803
  cron: 'Cron',
489
- errors: 'Errors',
490
804
  };
491
805
 
492
806
  const getSelectedSession = (sessions: TProfilerNavigationSession[], selectedSessionId?: string, currentSessionId?: string) =>
@@ -563,6 +877,52 @@ const formatSummaryJson = (value: TTraceSummaryValue | undefined) => {
563
877
  return JSON.stringify(toSummaryJsonValue(value), null, 2);
564
878
  };
565
879
 
880
+ const formatApiPanelJson = (jsonValue: unknown, summaryValue: TTraceSummaryValue | undefined) =>
881
+ jsonValue !== undefined ? formatStructuredValue(jsonValue) : formatSummaryJson(summaryValue);
882
+
883
+ const formatTraceEventDetailsJson = (details: Record<string, TTraceSummaryValue>) =>
884
+ JSON.stringify(
885
+ Object.fromEntries(Object.entries(details).map(([key, value]) => [key, toSummaryJsonValue(value)])),
886
+ null,
887
+ 2,
888
+ );
889
+
890
+ const renderHighlightedJson = (value: string) => {
891
+ const tokenPattern =
892
+ /"(?:\\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;
893
+ const parts: React.ReactNode[] = [];
894
+ let lastIndex = 0;
895
+ let match: RegExpExecArray | null;
896
+
897
+ while ((match = tokenPattern.exec(value))) {
898
+ const index = match.index;
899
+ const token = match[0];
900
+
901
+ if (index > lastIndex) parts.push(value.slice(lastIndex, index));
902
+
903
+ const trailing = value.slice(index + token.length);
904
+ const isKey = token.startsWith('"') && /^\s*:/.test(trailing);
905
+ const className = token.startsWith('"')
906
+ ? isKey
907
+ ? 'proteum-profiler__jsonKey'
908
+ : 'proteum-profiler__jsonString'
909
+ : token === 'true' || token === 'false' || token === 'null'
910
+ ? 'proteum-profiler__jsonLiteral'
911
+ : 'proteum-profiler__jsonNumber';
912
+
913
+ parts.push(
914
+ <span className={className} key={`json:${index}`}>
915
+ {token}
916
+ </span>,
917
+ );
918
+ lastIndex = index + token.length;
919
+ }
920
+
921
+ if (lastIndex < value.length) parts.push(value.slice(lastIndex));
922
+
923
+ return parts;
924
+ };
925
+
566
926
  const formatSummaryLiteral = (value: TTraceSummaryValue | undefined, depth = 1): string => {
567
927
  if (value === undefined) return '';
568
928
  if (value === null) return 'null';
@@ -605,6 +965,26 @@ const formatApiReference = (method: string, path: string, requestData?: TTraceSu
605
965
  return `${getApiReferenceName(method, path, fallbackLabel)}(${truncate(args, 112)})`;
606
966
  };
607
967
 
968
+ const formatProfilerRequestReference = ({
969
+ fallbackLabel,
970
+ method,
971
+ path,
972
+ requestData,
973
+ }: {
974
+ fallbackLabel?: string;
975
+ method?: string;
976
+ path?: string;
977
+ requestData?: TTraceSummaryValue;
978
+ }) => {
979
+ const safeMethod = method || '';
980
+ const safePath = path || '';
981
+
982
+ if (safePath.startsWith('/api/')) return formatApiReference(safeMethod, safePath, requestData, fallbackLabel);
983
+
984
+ const rawReference = `${safeMethod} ${safePath}`.trim();
985
+ return rawReference || fallbackLabel || 'request';
986
+ };
987
+
608
988
  const getTraceRequestData = (trace: TRequestTrace | undefined) =>
609
989
  trace?.events.find((event) => event.type === 'request.start')?.details.data;
610
990
 
@@ -613,9 +993,59 @@ const getTraceResultData = (trace: TRequestTrace | undefined) =>
613
993
  .reverse()
614
994
  .find((event) => event.details.kind === 'json' && event.details.data !== undefined)?.details.data;
615
995
 
996
+ const getRequestStatusText = (statusCode?: number, statusLabel?: string) =>
997
+ statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
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
+
616
1007
  const findTraceEvents = (trace: TRequestTrace | undefined, eventTypes: string[]) =>
617
1008
  trace?.events.filter((event) => eventTypes.includes(event.type)) || [];
618
1009
 
1010
+ const traceEventDepths: Record<TTraceEventType, number> = {
1011
+ 'request.start': 0,
1012
+ 'request.user': 1,
1013
+ 'auth.decode': 1,
1014
+ 'auth.route': 1,
1015
+ 'auth.check.start': 2,
1016
+ 'auth.check.rule': 3,
1017
+ 'auth.check.result': 2,
1018
+ 'auth.session': 1,
1019
+ 'resolve.start': 1,
1020
+ 'resolve.controller-route': 2,
1021
+ 'resolve.routes-evaluated': 1,
1022
+ 'resolve.route-skip': 2,
1023
+ 'resolve.route-match': 2,
1024
+ 'resolve.not-found': 1,
1025
+ 'controller.start': 2,
1026
+ 'controller.result': 2,
1027
+ 'setup.options': 3,
1028
+ 'context.create': 3,
1029
+ 'page.data': 3,
1030
+ 'ssr.payload': 3,
1031
+ 'render.start': 2,
1032
+ 'render.end': 2,
1033
+ 'response.send': 1,
1034
+ 'request.finish': 0,
1035
+ error: 0,
1036
+ };
1037
+
1038
+ const getTraceEventDepth = (event: TRequestTrace['events'][number]) => traceEventDepths[event.type] ?? 0;
1039
+
1040
+ const authEventTypes: TTraceEventType[] = [
1041
+ 'auth.decode',
1042
+ 'auth.route',
1043
+ 'auth.check.start',
1044
+ 'auth.check.rule',
1045
+ 'auth.check.result',
1046
+ 'auth.session',
1047
+ ];
1048
+
619
1049
  const getSummary = (session: TProfilerNavigationSession): TSessionSummary => {
620
1050
  const primaryTrace =
621
1051
  session.traces.find((trace) => trace.kind === 'initial-root' && trace.trace) ||
@@ -665,90 +1095,370 @@ const SummaryRow = ({ label, value }: { label: string; value: React.ReactNode })
665
1095
  </div>
666
1096
  );
667
1097
 
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,
681
- }: {
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[];
694
- }) => {
695
- const [isOpen, setOpen] = React.useState(false);
696
- const statusText = statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
1098
+ const JsonCodeBlock = ({ value }: { value: string }) => (
1099
+ <pre className="proteum-profiler__mono proteum-profiler__pre">{renderHighlightedJson(value)}</pre>
1100
+ );
697
1101
 
698
- 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}
705
- </span>
706
- </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}
715
- </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)}` : ''}
1102
+ const formatTraceCallDisplay = (call: TTraceCall) => {
1103
+ if (call.path.startsWith('/api/')) {
1104
+ return formatProfilerRequestReference({
1105
+ fallbackLabel: call.label,
1106
+ method: call.method,
1107
+ path: call.path,
1108
+ requestData: call.requestData,
1109
+ });
1110
+ }
1111
+
1112
+ const rawReference = `${call.method} ${call.path}`.trim();
1113
+ if (call.label && rawReference) return `${call.label} (${rawReference})`;
1114
+ return call.label || rawReference || 'request';
1115
+ };
1116
+
1117
+ const formatSessionTraceDisplay = (traceItem: TProfilerSessionTrace) => {
1118
+ if (traceItem.path.startsWith('/api/')) {
1119
+ return formatProfilerRequestReference({
1120
+ fallbackLabel: traceItem.label,
1121
+ method: traceItem.method,
1122
+ path: traceItem.path,
1123
+ requestData: getTraceRequestData(traceItem.trace),
1124
+ });
1125
+ }
1126
+
1127
+ return traceItem.label || formatProfilerRequestReference({ method: traceItem.method, path: traceItem.path });
1128
+ };
1129
+
1130
+ const ApiRequestListEntry = ({
1131
+ isSelected,
1132
+ item,
1133
+ onSelect,
1134
+ }: {
1135
+ isSelected: boolean;
1136
+ item: TApiRequestItem;
1137
+ onSelect: () => void;
1138
+ }) => {
1139
+ const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
1140
+ const statusTone = getRequestStatusTone(item.statusCode, item.statusLabel);
1141
+
1142
+ return (
1143
+ <button
1144
+ aria-pressed={isSelected}
1145
+ className={`proteum-profiler__row proteum-profiler__row--interactive ${isSelected ? 'proteum-profiler__row--selected' : ''}`}
1146
+ onClick={onSelect}
1147
+ type="button"
1148
+ >
1149
+ <div className="proteum-profiler__rowHeader">
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>
1156
+ </span>
1157
+ </div>
1158
+ </button>
1159
+ );
1160
+ };
1161
+
1162
+ const ApiRequestSidebar = ({ item }: { item?: TApiRequestItem }) => {
1163
+ if (!item) {
1164
+ return (
1165
+ <aside className="proteum-profiler__sidebar">
1166
+ <div className="proteum-profiler__sidebarScroller">
1167
+ <div className="proteum-profiler__sidebarHeader">
1168
+ <div className="proteum-profiler__sidebarEyebrow">Request details</div>
1169
+ <div className="proteum-profiler__sidebarEmpty">
1170
+ Select a request to inspect its payload, result, and timing.
1171
+ </div>
1172
+ </div>
1173
+ </div>
1174
+ </aside>
1175
+ );
1176
+ }
1177
+
1178
+ const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
1179
+
1180
+ return (
1181
+ <aside className="proteum-profiler__sidebar">
1182
+ <div className="proteum-profiler__sidebarScroller">
1183
+ <div className="proteum-profiler__sidebarHeader">
1184
+ <div className="proteum-profiler__sidebarEyebrow">{item.groupLabel}</div>
1185
+ <div className="proteum-profiler__sidebarTitle">
1186
+ <strong>{formatApiReference(item.method, item.path, item.requestData, item.label)}</strong>
1187
+ </div>
1188
+ <div className="proteum-profiler__mono proteum-profiler__muted">
1189
+ {formatProfilerRequestReference({
1190
+ fallbackLabel: item.label,
1191
+ method: item.method,
1192
+ path: item.path,
1193
+ requestData: item.requestData,
1194
+ })}
1195
+ </div>
1196
+ </div>
1197
+
1198
+ <div className="proteum-profiler__metrics">
1199
+ <SummaryRow label="Status" value={statusText} />
1200
+ <SummaryRow label="Duration" value={formatDuration(item.durationMs)} />
1201
+ <SummaryRow label="Started" value={formatTimestamp(item.startedAt)} />
1202
+ <SummaryRow label="Finished" value={item.finishedAt ? formatTimestamp(item.finishedAt) : 'pending'} />
1203
+ <SummaryRow
1204
+ label="Endpoint"
1205
+ value={formatProfilerRequestReference({
1206
+ fallbackLabel: item.label,
1207
+ method: item.method,
1208
+ path: item.path,
1209
+ requestData: item.requestData,
1210
+ })}
1211
+ />
1212
+ </div>
1213
+
1214
+ {item.tags.length > 0 ? (
1215
+ <div className="proteum-profiler__sidebarSection">
1216
+ <div className="proteum-profiler__sidebarSectionTitle">Tags</div>
1217
+ <div className="proteum-profiler__tags">
1218
+ {item.tags.map((tag) => (
1219
+ <span className="proteum-profiler__tag" key={`${item.id}:detail:${tag}`}>
1220
+ {tag}
1221
+ </span>
1222
+ ))}
1223
+ </div>
1224
+ </div>
1225
+ ) : null}
1226
+
1227
+ <div className="proteum-profiler__sidebarSection">
1228
+ <div className="proteum-profiler__sidebarSectionTitle">Arguments</div>
1229
+ <JsonCodeBlock value={formatApiPanelJson(item.requestDataJson, item.requestData)} />
1230
+ </div>
1231
+
1232
+ <div className="proteum-profiler__sidebarSection">
1233
+ <div className="proteum-profiler__sidebarSectionTitle">Result</div>
1234
+ <JsonCodeBlock value={formatApiPanelJson(item.resultJson, item.result)} />
1235
+ </div>
1236
+
1237
+ {item.errorMessage ? (
1238
+ <div className="proteum-profiler__sidebarSection">
1239
+ <div className="proteum-profiler__sidebarSectionTitle">Error</div>
1240
+ <div className="proteum-profiler__mono">{item.errorMessage}</div>
1241
+ </div>
1242
+ ) : null}
1243
+ </div>
1244
+ </aside>
1245
+ );
1246
+ };
1247
+
1248
+ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
1249
+ const syncItems: TApiRequestItem[] = session.traces
1250
+ .flatMap((trace) => trace.trace?.calls.filter((call) => call.origin !== 'client-async') || [])
1251
+ .map((call: TTraceCall) => ({
1252
+ id: call.id,
1253
+ groupLabel: 'Synchronous call',
1254
+ durationMs: call.durationMs,
1255
+ errorMessage: call.errorMessage,
1256
+ finishedAt: call.finishedAt,
1257
+ label: call.label,
1258
+ method: call.method,
1259
+ path: call.path,
1260
+ requestData: call.requestData,
1261
+ requestDataJson: call.requestDataJson,
1262
+ result: call.result,
1263
+ resultJson: call.resultJson,
1264
+ startedAt: call.startedAt,
1265
+ statusCode: call.statusCode,
1266
+ tags: [
1267
+ call.origin,
1268
+ ...(call.fetcherId ? [`fetcher:${call.fetcherId}`] : []),
1269
+ ...call.requestDataKeys.map((key) => `arg:${key}`),
1270
+ ...call.resultKeys.map((key) => `res:${key}`),
1271
+ ],
1272
+ }));
1273
+ const asyncItems: TApiRequestItem[] = session.traces
1274
+ .filter((trace) => trace.kind === 'async')
1275
+ .map((trace) => ({
1276
+ id: trace.id,
1277
+ groupLabel: 'Async request',
1278
+ durationMs: trace.durationMs,
1279
+ errorMessage: trace.errorMessage || trace.trace?.errorMessage,
1280
+ finishedAt: trace.finishedAt,
1281
+ label: trace.label,
1282
+ method: trace.method,
1283
+ path: trace.path,
1284
+ requestData: getTraceRequestData(trace.trace),
1285
+ requestDataJson: trace.trace?.requestDataJson,
1286
+ result: getTraceResultData(trace.trace),
1287
+ resultJson: trace.trace?.resultJson,
1288
+ startedAt: trace.startedAt,
1289
+ statusCode: trace.trace?.statusCode,
1290
+ statusLabel: trace.status,
1291
+ tags: [trace.status, ...(trace.requestId ? [`request:${trace.requestId}`] : [])],
1292
+ }));
1293
+ const requestItems = [...syncItems, ...asyncItems];
1294
+ const [selectedRequestId, setSelectedRequestId] = React.useState<string | undefined>(() => requestItems[0]?.id);
1295
+
1296
+ React.useEffect(() => {
1297
+ if (requestItems.some((item) => item.id === selectedRequestId)) return;
1298
+ setSelectedRequestId(requestItems[0]?.id);
1299
+ }, [requestItems, selectedRequestId]);
1300
+
1301
+ const waterfallItems = buildApiWaterfallItems(requestItems);
1302
+ const selectedItem = requestItems.find((item) => item.id === selectedRequestId) || requestItems[0];
1303
+
1304
+ return (
1305
+ <div className="proteum-profiler__requestWorkspace">
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
+ />
1313
+
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>
1321
+ </div>
1322
+
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
+ )}
1337
+ </div>
1338
+
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>
725
1345
  </div>
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
+ )}
726
1361
  </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>
1362
+ </div>
1363
+ </div>
1364
+
1365
+ <ApiRequestSidebar item={selectedItem} />
1366
+ </div>
1367
+ );
1368
+ };
1369
+
1370
+ const getTraceEventKey = (traceId: string, event: TRequestTrace['events'][number]) => `${traceId}:${event.index}`;
1371
+
1372
+ const TraceEventSidebar = ({
1373
+ event,
1374
+ label,
1375
+ trace,
1376
+ }: {
1377
+ event?: TRequestTrace['events'][number];
1378
+ label: string;
1379
+ trace?: TRequestTrace;
1380
+ }) => {
1381
+ const detailEntries = Object.entries(event?.details || {});
1382
+
1383
+ if (!event) {
1384
+ return (
1385
+ <aside className="proteum-profiler__sidebar">
1386
+ <div className="proteum-profiler__sidebarScroller">
1387
+ <div className="proteum-profiler__sidebarHeader">
1388
+ <div className="proteum-profiler__sidebarEyebrow">{label}</div>
1389
+ <div className="proteum-profiler__sidebarEmpty">Select an event to inspect its timing and payload.</div>
730
1390
  </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>
1391
+ </div>
1392
+ </aside>
1393
+ );
1394
+ }
1395
+
1396
+ return (
1397
+ <aside className="proteum-profiler__sidebar">
1398
+ <div className="proteum-profiler__sidebarScroller">
1399
+ <div className="proteum-profiler__sidebarHeader">
1400
+ <div className="proteum-profiler__sidebarEyebrow">{label}</div>
1401
+ <div className="proteum-profiler__sidebarTitle">
1402
+ <strong>{event.type}</strong>
734
1403
  </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>
1404
+ {trace ? (
1405
+ <div className="proteum-profiler__mono proteum-profiler__muted">
1406
+ {formatProfilerRequestReference({
1407
+ method: trace.method,
1408
+ path: trace.path,
1409
+ requestData: getTraceRequestData(trace),
1410
+ })}
739
1411
  </div>
740
1412
  ) : null}
741
1413
  </div>
742
- ) : null}
743
- </>
1414
+
1415
+ <div className="proteum-profiler__metrics">
1416
+ <SummaryRow label="Elapsed" value={formatDuration(event.elapsedMs)} />
1417
+ <SummaryRow label="Captured" value={formatTimestamp(event.at)} />
1418
+ <SummaryRow label="Trace" value={trace?.id || 'n/a'} />
1419
+ </div>
1420
+
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>
1433
+ </div>
1434
+ ) : null}
1435
+
1436
+ <div className="proteum-profiler__sidebarSection">
1437
+ <div className="proteum-profiler__sidebarSectionTitle">Raw JSON</div>
1438
+ <JsonCodeBlock value={formatTraceEventDetailsJson(event.details)} />
1439
+ </div>
1440
+ </div>
1441
+ </aside>
744
1442
  );
745
1443
  };
746
1444
 
747
- const TraceRows = ({ trace }: { trace: TRequestTrace }) => (
1445
+ const TraceRows = ({
1446
+ onSelect,
1447
+ selectedEventKey,
1448
+ trace,
1449
+ }: {
1450
+ onSelect: (selectionKey: string) => void;
1451
+ selectedEventKey?: string;
1452
+ trace: TRequestTrace;
1453
+ }) => (
748
1454
  <div className="proteum-profiler__section">
749
1455
  <div className="proteum-profiler__sectionHeader">
750
1456
  <div className="proteum-profiler__sectionTitle">
751
- {trace.method} {trace.path}
1457
+ {formatProfilerRequestReference({
1458
+ method: trace.method,
1459
+ path: trace.path,
1460
+ requestData: getTraceRequestData(trace),
1461
+ })}
752
1462
  </div>
753
1463
  <div className="proteum-profiler__mono proteum-profiler__muted">{trace.id}</div>
754
1464
  </div>
@@ -758,9 +1468,7 @@ const TraceRows = ({ trace }: { trace: TRequestTrace }) => (
758
1468
  {trace.calls.map((call) => (
759
1469
  <div className="proteum-profiler__row" key={call.id}>
760
1470
  <div className="proteum-profiler__rowHeader">
761
- <strong>
762
- {call.label} {call.method ? `(${call.method} ${call.path})` : ''}
763
- </strong>
1471
+ <strong>{formatTraceCallDisplay(call)}</strong>
764
1472
  <span className="proteum-profiler__mono proteum-profiler__muted">
765
1473
  {formatDuration(call.durationMs)}
766
1474
  {call.statusCode !== undefined ? ` | ${call.statusCode}` : ''}
@@ -787,28 +1495,578 @@ const TraceRows = ({ trace }: { trace: TRequestTrace }) => (
787
1495
  )}
788
1496
 
789
1497
  <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>
1498
+ {trace.events.map((event) => {
1499
+ const selectionKey = getTraceEventKey(trace.id, event);
1500
+
1501
+ return (
1502
+ <TraceEventEntry
1503
+ event={event}
1504
+ isSelected={selectionKey === selectedEventKey}
1505
+ key={selectionKey}
1506
+ onSelect={() => onSelect(selectionKey)}
1507
+ traceId={trace.id}
1508
+ />
1509
+ );
1510
+ })}
1511
+ </div>
1512
+ </div>
1513
+ );
1514
+
1515
+ const AuthTraceSection = ({
1516
+ authEvents,
1517
+ label,
1518
+ onSelect,
1519
+ selectedEventKey,
1520
+ trace,
1521
+ }: {
1522
+ authEvents: TRequestTrace['events'];
1523
+ label: string;
1524
+ onSelect: (selectionKey: string) => void;
1525
+ selectedEventKey?: string;
1526
+ trace: TRequestTrace;
1527
+ }) => (
1528
+ <div className="proteum-profiler__section">
1529
+ <div className="proteum-profiler__sectionHeader">
1530
+ <div>
1531
+ <div className="proteum-profiler__sectionTitle">{label}</div>
1532
+ <div className="proteum-profiler__mono proteum-profiler__muted">
1533
+ {formatProfilerRequestReference({
1534
+ method: trace.method,
1535
+ path: trace.path,
1536
+ requestData: getTraceRequestData(trace),
1537
+ })}
1538
+ </div>
1539
+ </div>
1540
+ <div className="proteum-profiler__actions">
1541
+ <span className="proteum-profiler__tag">capture:{trace.capture}</span>
1542
+ <span className="proteum-profiler__tag">events:{authEvents.length}</span>
1543
+ {trace.statusCode !== undefined ? <span className="proteum-profiler__tag">status:{trace.statusCode}</span> : null}
1544
+ </div>
1545
+ </div>
1546
+
1547
+ <div className="proteum-profiler__list">
1548
+ {authEvents.map((event) => {
1549
+ const selectionKey = getTraceEventKey(trace.id, event);
1550
+
1551
+ return (
1552
+ <TraceEventEntry
1553
+ event={event}
1554
+ isSelected={selectionKey === selectedEventKey}
1555
+ key={selectionKey}
1556
+ onSelect={() => onSelect(selectionKey)}
1557
+ traceId={trace.id}
1558
+ />
1559
+ );
1560
+ })}
1561
+ </div>
1562
+ </div>
1563
+ );
1564
+
1565
+ const TraceEventEntry = ({
1566
+ event,
1567
+ isSelected,
1568
+ onSelect,
1569
+ traceId,
1570
+ }: {
1571
+ event: TRequestTrace['events'][number];
1572
+ isSelected: boolean;
1573
+ onSelect: () => void;
1574
+ traceId: string;
1575
+ }) => {
1576
+ const depth = getTraceEventDepth(event);
1577
+
1578
+ return (
1579
+ <button
1580
+ aria-pressed={isSelected}
1581
+ className={`proteum-profiler__row proteum-profiler__row--interactive proteum-profiler__traceEventRow ${isSelected ? 'proteum-profiler__row--selected' : ''}`}
1582
+ onClick={onSelect}
1583
+ style={
1584
+ {
1585
+ '--profiler-trace-depth': depth,
1586
+ '--profiler-trace-guide-opacity': depth > 0 ? 1 : 0,
1587
+ } as React.CSSProperties
1588
+ }
1589
+ type="button"
1590
+ >
1591
+ <div className="proteum-profiler__rowHeader">
1592
+ <strong>{event.type}</strong>
1593
+ <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(event.elapsedMs)}</span>
1594
+ </div>
1595
+ <div className="proteum-profiler__tags">
1596
+ {Object.entries(event.details).map(([key, value]) => (
1597
+ <span className="proteum-profiler__tag" key={`${traceId}:${event.index}:${key}`}>
1598
+ {key}:{truncate(renderSummaryValue(value), 72)}
1599
+ </span>
1600
+ ))}
1601
+ </div>
1602
+ </button>
1603
+ );
1604
+ };
1605
+
1606
+ type TTraceEventInspectorSelection = {
1607
+ event: TRequestTrace['events'][number];
1608
+ key: string;
1609
+ label: string;
1610
+ trace: TRequestTrace;
1611
+ };
1612
+
1613
+ const readDateMs = (value?: string) => {
1614
+ if (!value) return undefined;
1615
+ const ms = new Date(value).valueOf();
1616
+ return Number.isFinite(ms) ? ms : undefined;
1617
+ };
1618
+
1619
+ const getTimelineDurationColor = (durationMs?: number) => {
1620
+ if (durationMs === undefined) return '#93c5fd';
1621
+ if (durationMs >= 800) return '#ef4444';
1622
+ if (durationMs >= 450) return '#f97316';
1623
+ if (durationMs >= 220) return '#f59e0b';
1624
+ if (durationMs >= 100) return '#3b82f6';
1625
+ return '#22c55e';
1626
+ };
1627
+
1628
+ const escapeHtml = (value: string) =>
1629
+ value
1630
+ .replace(/&/g, '&amp;')
1631
+ .replace(/</g, '&lt;')
1632
+ .replace(/>/g, '&gt;')
1633
+ .replace(/"/g, '&quot;')
1634
+ .replace(/'/g, '&#39;');
1635
+
1636
+ const timelineWaterfallMinDurationMs = 6;
1637
+ const waterfallBarHeight = 15;
1638
+ const waterfallRowGap = 1;
1639
+ const waterfallRowHeight = waterfallBarHeight + waterfallRowGap;
1640
+
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
+ };
1651
+
1652
+ const buildTimelineWaterfallItems = (session: TProfilerNavigationSession): TWaterfallChartItem[] => {
1653
+ const sessionStartMs = readDateMs(session.startedAt) ?? 0;
1654
+ const rawItems = session.traces.flatMap((traceItem) => {
1655
+ const trace = traceItem.trace;
1656
+ if (!trace) return [];
1657
+
1658
+ const traceStartMs = readDateMs(trace.startedAt) ?? sessionStartMs;
1659
+ const traceFinishedMs = readDateMs(trace.finishedAt) ?? (trace.durationMs !== undefined ? traceStartMs + trace.durationMs : undefined);
1660
+ const traceLabel = formatSessionTraceDisplay(traceItem);
1661
+
1662
+ return trace.events.map((event, index) => {
1663
+ const nextEvent = trace.events[index + 1];
1664
+ const startMs = readDateMs(event.at) ?? traceStartMs + event.elapsedMs;
1665
+ const nextStartMs = nextEvent ? readDateMs(nextEvent.at) ?? traceStartMs + nextEvent.elapsedMs : undefined;
1666
+ const endMs = buildWaterfallEndMs({
1667
+ fallbackEndMs: nextStartMs ?? traceFinishedMs,
1668
+ startMs,
1669
+ });
1670
+
1671
+ return {
1672
+ durationMs: Math.max(1, endMs - startMs),
1673
+ endMs,
1674
+ event,
1675
+ startMs,
1676
+ trace,
1677
+ traceLabel,
1678
+ };
1679
+ });
1680
+ });
1681
+
1682
+ const sortedItems = [...rawItems].sort((left, right) => left.startMs - right.startMs || left.event.index - right.event.index);
1683
+ const chartStartMs = sortedItems.length > 0 ? Math.min(...sortedItems.map((item) => item.startMs)) : 0;
1684
+
1685
+ return sortedItems
1686
+ .filter((item) => item.durationMs >= timelineWaterfallMinDurationMs)
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),
1737
+ color: getTimelineDurationColor(item.durationMs),
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);
1781
+ const ChartComponent = ApexChartComponent as any;
1782
+
1783
+ const series = [
1784
+ {
1785
+ data: items.map((item) => ({
1786
+ fillColor: item.color,
1787
+ x: item.barLabel,
1788
+ y: [item.startOffsetMs, item.endOffsetMs],
1789
+ })),
1790
+ name: itemLabel,
1791
+ },
1792
+ ];
1793
+
1794
+ const options = {
1795
+ chart: {
1796
+ animations: { enabled: false },
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,
1810
+ foreColor: '#627186',
1811
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
1812
+ toolbar: { show: false },
1813
+ type: 'rangeBar',
1814
+ zoom: { enabled: false },
1815
+ },
1816
+ dataLabels: {
1817
+ enabled: false,
1818
+ },
1819
+ fill: {
1820
+ opacity: 1,
1821
+ },
1822
+ grid: {
1823
+ borderColor: 'rgba(19, 32, 51, 0.08)',
1824
+ padding: { bottom: 0, left: 0, right: 0, top: 4 },
1825
+ xaxis: { lines: { show: true } },
1826
+ yaxis: { lines: { show: false } },
1827
+ },
1828
+ legend: {
1829
+ show: false,
1830
+ },
1831
+ noData: {
1832
+ style: {
1833
+ color: '#627186',
1834
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
1835
+ fontSize: '11px',
1836
+ },
1837
+ text: emptyLabel,
1838
+ },
1839
+ plotOptions: {
1840
+ bar: {
1841
+ barHeight: waterfallBarHeight,
1842
+ borderRadius: 2,
1843
+ horizontal: true,
1844
+ rangeBarGroupRows: false,
1845
+ },
1846
+ },
1847
+ stroke: {
1848
+ colors: ['#ffffff'],
1849
+ width: 1,
1850
+ },
1851
+ tooltip: {
1852
+ custom: ({ dataPointIndex }: { dataPointIndex: number }) => {
1853
+ const item = items[dataPointIndex];
1854
+ if (!item) return '';
1855
+
1856
+ return `
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;">
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('')}
795
1866
  </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>
1867
+ `;
1868
+ },
1869
+ },
1870
+ xaxis: {
1871
+ axisBorder: { show: false },
1872
+ axisTicks: { show: false },
1873
+ labels: {
1874
+ formatter: (value: string | number) => `${Math.round(Number(value))} ms`,
1875
+ style: {
1876
+ colors: '#627186',
1877
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
1878
+ fontSize: '10px',
1879
+ },
1880
+ },
1881
+ max: totalDurationMs,
1882
+ min: 0,
1883
+ tickAmount: Math.min(6, Math.max(2, items.length > 0 ? 6 : 2)),
1884
+ type: 'numeric',
1885
+ },
1886
+ yaxis: {
1887
+ show: false,
1888
+ labels: {
1889
+ show: false,
1890
+ },
1891
+ },
1892
+ };
1893
+
1894
+ return (
1895
+ <div className="proteum-profiler__section">
1896
+ <div className="proteum-profiler__timelineChart">
1897
+ <div className="proteum-profiler__timelineChartMeta">
1898
+ <span className="proteum-profiler__mono proteum-profiler__muted">
1899
+ {items.length} {itemLabel}
1900
+ {items.length === 1 ? '' : 's'}
1901
+ </span>
1902
+ <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(totalDurationMs)}</span>
1903
+ </div>
1904
+
1905
+ <div className="proteum-profiler__timelineChartCanvas" style={{ height: `${chartHeight}px` }}>
1906
+ {ChartComponent && items.length > 0 ? (
1907
+ <ChartComponent height={chartHeight} options={options} series={series} type="rangeBar" width="100%" />
1908
+ ) : items.length > 0 ? (
1909
+ <div className="proteum-profiler__empty">Loading waterfall chart...</div>
1910
+ ) : (
1911
+ <div className="proteum-profiler__empty">{emptyLabel}</div>
1912
+ )}
1913
+ </div>
1914
+ </div>
1915
+ </div>
1916
+ );
1917
+ };
1918
+
1919
+ const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) => {
1920
+ const selections: TTraceEventInspectorSelection[] = session.traces.flatMap((traceItem) =>
1921
+ traceItem.trace
1922
+ ? traceItem.trace.events.map((event) => ({
1923
+ event,
1924
+ key: getTraceEventKey(traceItem.trace!.id, event),
1925
+ label: formatSessionTraceDisplay(traceItem),
1926
+ trace: traceItem.trace!,
1927
+ }))
1928
+ : [],
1929
+ );
1930
+ const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(() => selections[0]?.key);
1931
+
1932
+ React.useEffect(() => {
1933
+ if (selections.some((selection) => selection.key === selectedEventKey)) return;
1934
+ setSelectedEventKey(selections[0]?.key);
1935
+ }, [selectedEventKey, selections]);
1936
+
1937
+ const waterfallItems = buildTimelineWaterfallItems(session);
1938
+ const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
1939
+
1940
+ return (
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
+ />
1949
+
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>
1960
+ </div>
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}
1969
+ </div>
1970
+ </div>
801
1971
  ))}
802
1972
  </div>
803
1973
  </div>
804
- ))}
1974
+
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
+ )}
2000
+ </div>
2001
+
2002
+ <TraceEventSidebar event={selected?.event} label={selected?.label || 'Trace event'} trace={selected?.trace} />
805
2003
  </div>
806
- </div>
807
- );
2004
+ );
2005
+ };
2006
+
2007
+ const AuthPanel = ({ session }: { session: TProfilerNavigationSession }) => {
2008
+ const authSections = session.traces.flatMap((traceItem) => {
2009
+ const authEvents = traceItem.trace ? findTraceEvents(traceItem.trace, authEventTypes) : [];
2010
+ return traceItem.trace && authEvents.length > 0
2011
+ ? [{ authEvents, id: traceItem.id, label: formatSessionTraceDisplay(traceItem), trace: traceItem.trace }]
2012
+ : [];
2013
+ });
2014
+ const selections: TTraceEventInspectorSelection[] = authSections.flatMap((section) =>
2015
+ section.authEvents.map((event) => ({
2016
+ event,
2017
+ key: getTraceEventKey(section.trace.id, event),
2018
+ label: `${section.label} event`,
2019
+ trace: section.trace,
2020
+ })),
2021
+ );
2022
+ const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(() => selections[0]?.key);
2023
+
2024
+ React.useEffect(() => {
2025
+ if (selections.some((selection) => selection.key === selectedEventKey)) return;
2026
+ setSelectedEventKey(selections[0]?.key);
2027
+ }, [selectedEventKey, selections]);
808
2028
 
809
- const SimpleSection = ({ empty, rows, title }: { empty: string; rows: Array<{ key: string; title: string; value: string }>; title: string }) => (
2029
+ if (authSections.length === 0) return <div className="proteum-profiler__empty">No auth activity was captured for this session.</div>;
2030
+
2031
+ const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
2032
+
2033
+ return (
2034
+ <div className="proteum-profiler__splitView">
2035
+ <div className="proteum-profiler__splitColumn">
2036
+ {authSections.map((section) => (
2037
+ <AuthTraceSection
2038
+ authEvents={section.authEvents}
2039
+ key={section.id}
2040
+ label={section.label}
2041
+ onSelect={setSelectedEventKey}
2042
+ selectedEventKey={selectedEventKey}
2043
+ trace={section.trace}
2044
+ />
2045
+ ))}
2046
+ </div>
2047
+
2048
+ <TraceEventSidebar event={selected?.event} label={selected?.label || 'Auth event'} trace={selected?.trace} />
2049
+ </div>
2050
+ );
2051
+ };
2052
+
2053
+ const SimpleSection = ({
2054
+ empty,
2055
+ rows,
2056
+ showTitle = true,
2057
+ title,
2058
+ }: {
2059
+ empty: string;
2060
+ rows: Array<{ key: string; title: string; value: string }>;
2061
+ showTitle?: boolean;
2062
+ title: string;
2063
+ }) => (
810
2064
  <div className="proteum-profiler__section">
811
- <div className="proteum-profiler__sectionTitle">{title}</div>
2065
+ {showTitle ? (
2066
+ <div className="proteum-profiler__titleRow">
2067
+ <div className="proteum-profiler__sectionTitle">{title}</div>
2068
+ </div>
2069
+ ) : null}
812
2070
  {rows.length === 0 ? (
813
2071
  <div className="proteum-profiler__empty">{empty}</div>
814
2072
  ) : (
@@ -830,7 +2088,9 @@ const TextBlocks = ({ blocks }: { blocks: THumanTextBlock[] }) => (
830
2088
  <>
831
2089
  {blocks.map((block) => (
832
2090
  <div className="proteum-profiler__section" key={block.title}>
833
- <div className="proteum-profiler__sectionTitle">{block.title}</div>
2091
+ <div className="proteum-profiler__titleRow">
2092
+ <div className="proteum-profiler__sectionTitle">{block.title}</div>
2093
+ </div>
834
2094
  {block.items.length === 0 ? (
835
2095
  <div className="proteum-profiler__empty">{block.empty || 'none'}</div>
836
2096
  ) : (
@@ -873,44 +2133,11 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
873
2133
  }
874
2134
 
875
2135
  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>
2136
+ return <TimelinePanel session={session} />;
2137
+ }
898
2138
 
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
- );
2139
+ if (panel === 'auth') {
2140
+ return <AuthPanel session={session} />;
914
2141
  }
915
2142
 
916
2143
  if (panel === 'routing') {
@@ -930,6 +2157,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
930
2157
  .map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
931
2158
  .join(' '),
932
2159
  }))}
2160
+ showTitle={false}
933
2161
  title="Routing"
934
2162
  />
935
2163
  );
@@ -948,6 +2176,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
948
2176
  .join(' '),
949
2177
  }),
950
2178
  )}
2179
+ showTitle={false}
951
2180
  title="Controller"
952
2181
  />
953
2182
  );
@@ -964,74 +2193,14 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
964
2193
  .map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
965
2194
  .join(' '),
966
2195
  }))}
2196
+ showTitle={false}
967
2197
  title="SSR"
968
2198
  />
969
2199
  );
970
2200
  }
971
2201
 
972
2202
  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
- );
2203
+ return <ApiPanel session={session} />;
1035
2204
  }
1036
2205
 
1037
2206
  if (panel === 'explain') {
@@ -1230,13 +2399,15 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
1230
2399
  {execution ? (
1231
2400
  <div className="proteum-profiler__section">
1232
2401
  <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>
2402
+ <JsonCodeBlock
2403
+ value={
2404
+ execution.result?.json !== undefined
2405
+ ? formatStructuredValue(execution.result.json)
2406
+ : execution.result
2407
+ ? formatStructuredValue(execution.result.summary)
2408
+ : execution.errorMessage || 'undefined'
2409
+ }
2410
+ />
1240
2411
  </div>
1241
2412
  ) : null}
1242
2413
  </div>
@@ -1361,7 +2532,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
1361
2532
  })),
1362
2533
  ];
1363
2534
 
1364
- return <SimpleSection empty="No errors captured." rows={errorRows} title="Errors" />;
2535
+ return <SimpleSection empty="No errors captured." rows={errorRows} showTitle={false} title="Errors" />;
1365
2536
  };
1366
2537
 
1367
2538
  export default function DevProfiler() {
@@ -1396,9 +2567,13 @@ export default function DevProfiler() {
1396
2567
  session.kind === 'client-navigation'
1397
2568
  ? session.label
1398
2569
  : primaryTrace
1399
- ? `${primaryTrace.statusCode || 'pending'} ${primaryTrace.method} ${primaryTrace.path}`
2570
+ ? `${primaryTrace.statusCode || 'pending'} ${formatProfilerRequestReference({
2571
+ method: primaryTrace.method,
2572
+ path: primaryTrace.path,
2573
+ requestData: getTraceRequestData(primaryTrace),
2574
+ })}`
1400
2575
  : session.label;
1401
- const recentSessions = state.sessions.slice(-6).reverse();
2576
+ const recentSessions: TProfilerNavigationSession[] = state.sessions.slice(-6).reverse();
1402
2577
 
1403
2578
  return (
1404
2579
  <div className="proteum-profiler">