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.
- package/README.md +25 -6
- package/agents/framework/AGENTS.md +14 -1
- package/agents/project/AGENTS.md +3 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/dev.ts +2 -1
- package/cli/commands/init.ts +2 -94
- package/cli/index.ts +1 -4
- package/cli/presentation/commands.ts +45 -9
- package/cli/presentation/devSession.ts +17 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +61 -3
- package/cli/scaffold/index.ts +720 -0
- package/cli/scaffold/templates.ts +344 -0
- package/cli/scaffold/types.ts +26 -0
- package/client/dev/profiler/index.tsx +1230 -230
- package/common/dev/profiler.ts +1 -0
- package/common/dev/requestTrace.ts +6 -0
- package/docs/dev-commands.md +7 -0
- package/docs/diagnostics.md +88 -0
- package/docs/request-tracing.md +10 -0
- package/eslint.js +11 -6
- package/package.json +3 -2
- package/server/app/index.ts +2 -2
- package/server/index.ts +0 -1
- package/server/services/auth/index.ts +525 -61
- package/server/services/auth/router/index.ts +106 -7
- package/server/services/router/http/index.ts +22 -6
|
@@ -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: #
|
|
25
|
-
--profiler-bg-strong: #
|
|
26
|
-
--profiler-
|
|
27
|
-
--profiler-
|
|
28
|
-
--profiler-
|
|
29
|
-
--profiler-
|
|
30
|
-
--profiler-
|
|
31
|
-
--profiler-
|
|
32
|
-
--profiler-
|
|
33
|
-
--profiler-
|
|
34
|
-
--profiler-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
287
|
-
padding:
|
|
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:
|
|
331
|
-
padding:
|
|
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
|
|
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
|
|
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
|
|
382
|
-
border
|
|
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
|
|
427
|
-
border-top: 1px
|
|
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
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
|
696
|
-
const statusText = statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
|
|
1086
|
+
const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
|
|
697
1087
|
|
|
698
1088
|
return (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
<
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
{
|
|
736
|
-
<div className="proteum-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
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 = ({
|
|
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
|
-
{
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
{
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
|
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, '&')
|
|
1562
|
+
.replace(/</g, '<')
|
|
1563
|
+
.replace(/>/g, '>')
|
|
1564
|
+
.replace(/"/g, '"')
|
|
1565
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
900
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
1234
|
-
{
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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'} ${
|
|
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">
|