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.
- package/README.md +28 -6
- package/agents/framework/AGENTS.md +14 -1
- package/agents/project/AGENTS.md +3 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/dev.ts +2 -1
- package/cli/commands/init.ts +2 -94
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +8 -0
- package/cli/index.ts +1 -4
- package/cli/presentation/commands.ts +72 -10
- package/cli/presentation/devSession.ts +17 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +89 -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 +1410 -235
- package/common/dev/profiler.ts +1 -0
- package/common/dev/requestTrace.ts +10 -0
- package/common/dev/session.ts +24 -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/container/trace/index.ts +48 -0
- 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 +108 -6
- package/server/services/router/response/index.ts +1 -0
|
@@ -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;
|
|
@@ -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
|
|
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
|
|
382
|
-
border
|
|
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
|
|
427
|
-
border-top: 1px
|
|
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
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
{
|
|
736
|
-
<div className="proteum-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
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 = ({
|
|
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
|
-
{
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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, '&')
|
|
1631
|
+
.replace(/</g, '<')
|
|
1632
|
+
.replace(/>/g, '>')
|
|
1633
|
+
.replace(/"/g, '"')
|
|
1634
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
);
|
|
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
|
-
|
|
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
|
-
<
|
|
1234
|
-
{
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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'} ${
|
|
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">
|