proteum 2.1.0 → 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/AGENTS.md +44 -98
- package/README.md +143 -10
- package/agents/framework/AGENTS.md +146 -886
- package/agents/project/AGENTS.md +73 -127
- package/agents/project/client/AGENTS.md +22 -93
- package/agents/project/client/pages/AGENTS.md +24 -26
- package/agents/project/server/routes/AGENTS.md +10 -8
- package/agents/project/server/services/AGENTS.md +22 -159
- package/agents/project/tests/AGENTS.md +11 -8
- package/cli/app/config.ts +7 -20
- package/cli/bin.js +8 -0
- package/cli/commands/command.ts +243 -0
- package/cli/commands/commandLocalRunner.js +198 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/deploy/web.ts +1 -2
- package/cli/commands/dev.ts +98 -2
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -186
- package/cli/commands/init.ts +2 -94
- package/cli/commands/trace.ts +228 -0
- package/cli/compiler/artifacts/commands.ts +217 -0
- package/cli/compiler/artifacts/manifest.ts +35 -21
- package/cli/compiler/artifacts/services.ts +300 -1
- package/cli/compiler/client/index.ts +43 -8
- package/cli/compiler/common/commands.ts +175 -0
- package/cli/compiler/common/index.ts +1 -1
- package/cli/compiler/common/proteumManifest.ts +15 -114
- package/cli/compiler/index.ts +25 -2
- package/cli/compiler/server/index.ts +31 -6
- package/cli/index.ts +1 -4
- package/cli/paths.ts +16 -1
- package/cli/presentation/commands.ts +104 -14
- package/cli/presentation/devSession.ts +22 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +121 -4
- package/cli/scaffold/index.ts +720 -0
- package/cli/scaffold/templates.ts +344 -0
- package/cli/scaffold/types.ts +26 -0
- package/cli/tsconfig.json +4 -1
- package/cli/utils/check.ts +1 -1
- package/client/app/component.tsx +13 -9
- package/client/dev/profiler/index.tsx +2511 -0
- package/client/dev/profiler/noop.tsx +5 -0
- package/client/dev/profiler/runtime.noop.ts +116 -0
- package/client/dev/profiler/runtime.ts +840 -0
- package/client/services/router/components/router.tsx +30 -2
- package/client/services/router/index.tsx +27 -3
- package/client/services/router/request/api.ts +133 -17
- package/commands/proteum/diagnostics.ts +11 -0
- package/common/dev/commands.ts +50 -0
- package/common/dev/diagnostics.ts +298 -0
- package/common/dev/profiler.ts +92 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +115 -0
- package/common/env/proteumEnv.ts +284 -0
- package/common/router/index.ts +4 -22
- package/docs/dev-commands.md +93 -0
- package/docs/diagnostics.md +88 -0
- package/docs/request-tracing.md +132 -0
- package/eslint.js +11 -6
- package/package.json +3 -3
- package/server/app/commands.ts +35 -370
- package/server/app/commandsManager.ts +393 -0
- package/server/app/container/config.ts +11 -49
- package/server/app/container/console/index.ts +2 -3
- package/server/app/container/index.ts +5 -2
- package/server/app/container/trace/index.ts +364 -0
- package/server/app/devCommands.ts +192 -0
- package/server/app/devDiagnostics.ts +53 -0
- package/server/app/index.ts +29 -6
- 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/cron/CronTask.ts +73 -5
- package/server/services/cron/index.ts +34 -11
- package/server/services/fetch/index.ts +3 -10
- package/server/services/prisma/index.ts +66 -4
- package/server/services/router/http/index.ts +173 -6
- package/server/services/router/index.ts +200 -12
- package/server/services/router/request/api.ts +30 -1
- package/server/services/router/response/index.ts +83 -10
- package/server/services/router/response/page/document.tsx +16 -0
- package/server/services/router/response/page/index.tsx +27 -1
- package/skills/clean-project-code/SKILL.md +7 -2
- package/test-results/.last-run.json +4 -0
- package/types/aliases.d.ts +6 -0
- package/types/global/utils.d.ts +7 -14
- package/Rte.zip +0 -0
- package/agents/project/agents.md.zip +0 -0
- package/doc/TODO.md +0 -71
- package/doc/front/router.md +0 -27
- package/doc/workspace/workspace.png +0 -0
- package/doc/workspace/workspace2.png +0 -0
- package/doc/workspace/workspace_26.01.22.png +0 -0
- package/server/services/router/http/session.ts.old +0 -40
|
@@ -0,0 +1,2511 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildDoctorBlocks,
|
|
5
|
+
buildExplainBlocks,
|
|
6
|
+
buildExplainSummaryItems,
|
|
7
|
+
explainSectionNames,
|
|
8
|
+
formatManifestLocation,
|
|
9
|
+
type THumanTextBlock,
|
|
10
|
+
} from '@common/dev/diagnostics';
|
|
11
|
+
import type { TDevCommandDefinition, TDevCommandExecution } from '@common/dev/commands';
|
|
12
|
+
import type {
|
|
13
|
+
TProfilerCronTask,
|
|
14
|
+
TProfilerNavigationSession,
|
|
15
|
+
TProfilerPanel,
|
|
16
|
+
TProfilerSessionTrace,
|
|
17
|
+
} from '@common/dev/profiler';
|
|
18
|
+
import type { TRequestTrace, TTraceCall, TTraceEventType, TTraceSummaryValue } from '@common/dev/requestTrace';
|
|
19
|
+
|
|
20
|
+
import { profilerRuntime } from './runtime';
|
|
21
|
+
|
|
22
|
+
const profilerStyles = `
|
|
23
|
+
.proteum-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;
|
|
38
|
+
position: fixed;
|
|
39
|
+
inset-inline: 0;
|
|
40
|
+
bottom: 0;
|
|
41
|
+
z-index: 2147483000;
|
|
42
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
43
|
+
color: var(--profiler-text);
|
|
44
|
+
letter-spacing: 0.01em;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.proteum-profiler__bar,
|
|
48
|
+
.proteum-profiler__panel,
|
|
49
|
+
.proteum-profiler__handle {
|
|
50
|
+
position: relative;
|
|
51
|
+
box-sizing: border-box;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.proteum-profiler__bar::before,
|
|
55
|
+
.proteum-profiler__panel::before,
|
|
56
|
+
.proteum-profiler__handle::before {
|
|
57
|
+
display: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.proteum-profiler__bar {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 0;
|
|
64
|
+
min-height: 32px;
|
|
65
|
+
padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px));
|
|
66
|
+
border-top: 1px solid var(--profiler-line-strong);
|
|
67
|
+
background: var(--profiler-bg-strong);
|
|
68
|
+
backdrop-filter: none;
|
|
69
|
+
box-shadow: none;
|
|
70
|
+
overflow-x: auto;
|
|
71
|
+
scrollbar-width: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.proteum-profiler__bar::-webkit-scrollbar,
|
|
75
|
+
.proteum-profiler__panelTabs::-webkit-scrollbar {
|
|
76
|
+
display: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.proteum-profiler__token {
|
|
80
|
+
flex: 0 0 auto;
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 6px;
|
|
84
|
+
min-height: 20px;
|
|
85
|
+
padding: 0 10px;
|
|
86
|
+
border: none;
|
|
87
|
+
border-inline-start: 1px solid var(--profiler-line);
|
|
88
|
+
background: transparent;
|
|
89
|
+
color: var(--profiler-muted);
|
|
90
|
+
font-size: 11px;
|
|
91
|
+
line-height: 1;
|
|
92
|
+
letter-spacing: 0.06em;
|
|
93
|
+
text-transform: uppercase;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.proteum-profiler__token:first-child {
|
|
99
|
+
padding-inline-start: 0;
|
|
100
|
+
border-inline-start: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.proteum-profiler__token:hover {
|
|
104
|
+
color: var(--profiler-text);
|
|
105
|
+
background: var(--profiler-surface-hover);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.proteum-profiler__token--brand {
|
|
109
|
+
color: var(--profiler-brand);
|
|
110
|
+
font-weight: 700;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.proteum-profiler__token--ok {
|
|
114
|
+
color: var(--profiler-ok);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.proteum-profiler__token--warn {
|
|
118
|
+
color: var(--profiler-warn);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.proteum-profiler__token--error {
|
|
122
|
+
color: var(--profiler-error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.proteum-profiler__spacer {
|
|
126
|
+
flex: 1 1 auto;
|
|
127
|
+
min-width: 16px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.proteum-profiler__handle {
|
|
131
|
+
position: fixed;
|
|
132
|
+
right: 10px;
|
|
133
|
+
bottom: calc(10px + env(safe-area-inset-bottom, 0px));
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 10px;
|
|
137
|
+
min-height: 30px;
|
|
138
|
+
padding: 0 12px;
|
|
139
|
+
border: 1px solid var(--profiler-line-strong);
|
|
140
|
+
border-radius: 0;
|
|
141
|
+
background: var(--profiler-bg-strong);
|
|
142
|
+
backdrop-filter: none;
|
|
143
|
+
color: var(--profiler-brand);
|
|
144
|
+
box-shadow: none;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
font-size: 11px;
|
|
147
|
+
letter-spacing: 0.08em;
|
|
148
|
+
text-transform: uppercase;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.proteum-profiler__panel {
|
|
152
|
+
position: fixed;
|
|
153
|
+
inset-inline: 0;
|
|
154
|
+
bottom: calc(32px + env(safe-area-inset-bottom, 0px));
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-rows: auto 1fr;
|
|
157
|
+
height: 50vh;
|
|
158
|
+
max-height: 50vh;
|
|
159
|
+
margin: 0;
|
|
160
|
+
border: 1px solid var(--profiler-line-strong);
|
|
161
|
+
border-bottom: none;
|
|
162
|
+
border-left: none;
|
|
163
|
+
border-right: none;
|
|
164
|
+
border-radius: 0;
|
|
165
|
+
background: var(--profiler-bg-strong);
|
|
166
|
+
backdrop-filter: none;
|
|
167
|
+
box-shadow: none;
|
|
168
|
+
overflow: hidden;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.proteum-profiler__panelHeader {
|
|
172
|
+
display: flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
justify-content: flex-start;
|
|
175
|
+
gap: 12px;
|
|
176
|
+
padding: 12px 14px 10px;
|
|
177
|
+
border-bottom: 1px solid var(--profiler-line);
|
|
178
|
+
min-width: 0;
|
|
179
|
+
background: var(--profiler-bg-strong);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.proteum-profiler__panelTabs {
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 14px;
|
|
186
|
+
overflow: auto;
|
|
187
|
+
padding: 0;
|
|
188
|
+
border-bottom: none;
|
|
189
|
+
scrollbar-width: none;
|
|
190
|
+
flex: 1 1 auto;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.proteum-profiler__pill {
|
|
195
|
+
display: inline-flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
gap: 6px;
|
|
198
|
+
min-height: 20px;
|
|
199
|
+
padding: 0;
|
|
200
|
+
border: none;
|
|
201
|
+
border-bottom: 1px solid transparent;
|
|
202
|
+
background: transparent;
|
|
203
|
+
font-size: 11px;
|
|
204
|
+
color: var(--profiler-muted);
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
white-space: nowrap;
|
|
207
|
+
letter-spacing: 0.05em;
|
|
208
|
+
text-transform: uppercase;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.proteum-profiler__pill:hover {
|
|
212
|
+
color: var(--profiler-text);
|
|
213
|
+
border-bottom-color: var(--profiler-line-strong);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.proteum-profiler__pill--active {
|
|
217
|
+
color: var(--profiler-brand);
|
|
218
|
+
border-bottom-color: var(--profiler-brand);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.proteum-profiler__pill:disabled {
|
|
222
|
+
opacity: 0.44;
|
|
223
|
+
cursor: default;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.proteum-profiler__select {
|
|
227
|
+
flex: 0 1 280px;
|
|
228
|
+
min-width: 160px;
|
|
229
|
+
height: 28px;
|
|
230
|
+
padding: 0 28px 0 10px;
|
|
231
|
+
border: 1px solid var(--profiler-line);
|
|
232
|
+
border-radius: 0;
|
|
233
|
+
background-color: var(--profiler-bg-strong);
|
|
234
|
+
background-image:
|
|
235
|
+
linear-gradient(45deg, transparent 50%, var(--profiler-muted) 50%),
|
|
236
|
+
linear-gradient(135deg, var(--profiler-muted) 50%, transparent 50%);
|
|
237
|
+
background-position: calc(100% - 14px) 11px, calc(100% - 9px) 11px;
|
|
238
|
+
background-repeat: no-repeat;
|
|
239
|
+
background-size: 5px 5px;
|
|
240
|
+
color: var(--profiler-text);
|
|
241
|
+
font: inherit;
|
|
242
|
+
font-size: 11px;
|
|
243
|
+
outline: none;
|
|
244
|
+
appearance: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.proteum-profiler__select option {
|
|
248
|
+
background: var(--profiler-bg-strong);
|
|
249
|
+
color: var(--profiler-text);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.proteum-profiler__panelBody {
|
|
253
|
+
overflow: auto;
|
|
254
|
+
height: 100%;
|
|
255
|
+
min-height: 0;
|
|
256
|
+
padding: 0;
|
|
257
|
+
background: transparent;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.proteum-profiler__metrics {
|
|
261
|
+
display: grid;
|
|
262
|
+
gap: 0;
|
|
263
|
+
padding: 10px 12px 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.proteum-profiler__metricRow {
|
|
267
|
+
display: grid;
|
|
268
|
+
grid-template-columns: minmax(104px, 140px) 1fr;
|
|
269
|
+
gap: 12px;
|
|
270
|
+
padding: 8px 0;
|
|
271
|
+
border-top: 1px solid var(--profiler-line);
|
|
272
|
+
align-items: start;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.proteum-profiler__metricRow:first-child {
|
|
276
|
+
border-top: none;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.proteum-profiler__metricLabel {
|
|
280
|
+
color: var(--profiler-muted);
|
|
281
|
+
font-size: 10px;
|
|
282
|
+
letter-spacing: 0.08em;
|
|
283
|
+
text-transform: uppercase;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.proteum-profiler__metricValue {
|
|
287
|
+
font-size: 12px;
|
|
288
|
+
line-height: 1.45;
|
|
289
|
+
word-break: break-word;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.proteum-profiler__section {
|
|
293
|
+
display: grid;
|
|
294
|
+
gap: 0;
|
|
295
|
+
padding: 0;
|
|
296
|
+
border-top: 1px solid var(--profiler-line);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.proteum-profiler__sectionHeader {
|
|
300
|
+
display: flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
justify-content: space-between;
|
|
303
|
+
gap: 12px;
|
|
304
|
+
padding: 8px 10px;
|
|
305
|
+
background: var(--profiler-title-row-bg);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.proteum-profiler__actions {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 10px;
|
|
312
|
+
flex-wrap: wrap;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.proteum-profiler__panelHeader .proteum-profiler__actions {
|
|
316
|
+
flex: 0 0 auto;
|
|
317
|
+
margin-left: auto;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.proteum-profiler__sectionTitle {
|
|
321
|
+
font-size: 11px;
|
|
322
|
+
font-weight: 700;
|
|
323
|
+
color: var(--profiler-brand);
|
|
324
|
+
letter-spacing: 0.08em;
|
|
325
|
+
text-transform: uppercase;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.proteum-profiler__list {
|
|
329
|
+
display: grid;
|
|
330
|
+
gap: 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.proteum-profiler__row {
|
|
334
|
+
display: grid;
|
|
335
|
+
gap: 6px;
|
|
336
|
+
padding: 10px 12px;
|
|
337
|
+
border-top: 1px solid var(--profiler-line);
|
|
338
|
+
border-radius: 0;
|
|
339
|
+
background: transparent;
|
|
340
|
+
box-shadow: none;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.proteum-profiler__row--interactive {
|
|
344
|
+
width: 100%;
|
|
345
|
+
appearance: none;
|
|
346
|
+
background: transparent;
|
|
347
|
+
border: none;
|
|
348
|
+
text-align: left;
|
|
349
|
+
color: inherit;
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.proteum-profiler__row--interactive:hover {
|
|
354
|
+
background: var(--profiler-surface-hover);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.proteum-profiler__row--selected {
|
|
358
|
+
background: var(--profiler-surface-selected);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.proteum-profiler__rowHeader {
|
|
362
|
+
display: flex;
|
|
363
|
+
align-items: flex-start;
|
|
364
|
+
justify-content: space-between;
|
|
365
|
+
gap: 10px;
|
|
366
|
+
font-size: 11px;
|
|
367
|
+
line-height: 1.45;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.proteum-profiler__mono {
|
|
371
|
+
font-family: inherit;
|
|
372
|
+
font-size: 11px;
|
|
373
|
+
line-height: 1.5;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.proteum-profiler__muted {
|
|
377
|
+
color: var(--profiler-muted);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.proteum-profiler__pre {
|
|
381
|
+
margin: 0;
|
|
382
|
+
white-space: pre-wrap;
|
|
383
|
+
word-break: break-word;
|
|
384
|
+
padding: 10px 0 0;
|
|
385
|
+
border: none;
|
|
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);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.proteum-profiler__detail {
|
|
408
|
+
display: grid;
|
|
409
|
+
gap: 10px;
|
|
410
|
+
padding: 10px 0 0;
|
|
411
|
+
border: none;
|
|
412
|
+
border-top: 1px solid var(--profiler-line);
|
|
413
|
+
border-radius: 0;
|
|
414
|
+
background: transparent;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.proteum-profiler__detailLine {
|
|
418
|
+
display: grid;
|
|
419
|
+
gap: 4px;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.proteum-profiler__detailLabel {
|
|
423
|
+
color: var(--profiler-muted);
|
|
424
|
+
font-size: 10px;
|
|
425
|
+
letter-spacing: 0.08em;
|
|
426
|
+
text-transform: uppercase;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.proteum-profiler__tags {
|
|
430
|
+
display: flex;
|
|
431
|
+
flex-wrap: wrap;
|
|
432
|
+
gap: 8px;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.proteum-profiler__tag {
|
|
436
|
+
display: inline-flex;
|
|
437
|
+
align-items: center;
|
|
438
|
+
min-height: 0;
|
|
439
|
+
padding: 0;
|
|
440
|
+
font-size: 11px;
|
|
441
|
+
color: var(--profiler-muted);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.proteum-profiler__tag::before,
|
|
445
|
+
.proteum-profiler__tag::after {
|
|
446
|
+
color: var(--profiler-line-strong);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.proteum-profiler__tag::before {
|
|
450
|
+
content: '[';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.proteum-profiler__tag::after {
|
|
454
|
+
content: ']';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.proteum-profiler__empty {
|
|
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;
|
|
585
|
+
color: var(--profiler-muted);
|
|
586
|
+
}
|
|
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
|
+
|
|
648
|
+
@media (max-width: 900px) {
|
|
649
|
+
.proteum-profiler__panel {
|
|
650
|
+
height: 50vh;
|
|
651
|
+
max-height: 50vh;
|
|
652
|
+
margin: 0;
|
|
653
|
+
border-left: 0;
|
|
654
|
+
border-right: 0;
|
|
655
|
+
border-radius: 0;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.proteum-profiler__bar {
|
|
659
|
+
padding-inline: 8px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.proteum-profiler__panelHeader,
|
|
663
|
+
.proteum-profiler__panelTabs,
|
|
664
|
+
.proteum-profiler__panelBody {
|
|
665
|
+
padding-inline: 10px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.proteum-profiler__panelTabs {
|
|
669
|
+
gap: 10px;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.proteum-profiler__metricRow {
|
|
673
|
+
grid-template-columns: minmax(90px, 110px) 1fr;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.proteum-profiler__select {
|
|
677
|
+
min-width: 132px;
|
|
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
|
+
|
|
706
|
+
}
|
|
707
|
+
`;
|
|
708
|
+
|
|
709
|
+
type TSessionSummary = {
|
|
710
|
+
apiAsyncCount: number;
|
|
711
|
+
apiSyncCount: number;
|
|
712
|
+
errorCount: number;
|
|
713
|
+
primaryTrace?: TProfilerSessionTrace;
|
|
714
|
+
renderMs?: number;
|
|
715
|
+
routeLabel: string;
|
|
716
|
+
ssrPayloadBytes?: number;
|
|
717
|
+
statusLabel: string;
|
|
718
|
+
totalMs?: number;
|
|
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
|
+
};
|
|
747
|
+
type TProfilerState = ReturnType<typeof profilerRuntime.getState>;
|
|
748
|
+
|
|
749
|
+
const panelLabels: Record<TProfilerPanel, string> = {
|
|
750
|
+
summary: 'Summary',
|
|
751
|
+
timeline: 'Timeline',
|
|
752
|
+
routing: 'Routing',
|
|
753
|
+
auth: 'Auth',
|
|
754
|
+
controller: 'Controller',
|
|
755
|
+
ssr: 'SSR',
|
|
756
|
+
api: 'API',
|
|
757
|
+
errors: 'Errors',
|
|
758
|
+
explain: 'Explain',
|
|
759
|
+
doctor: 'Doctor',
|
|
760
|
+
commands: 'Commands',
|
|
761
|
+
cron: 'Cron',
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const getSelectedSession = (sessions: TProfilerNavigationSession[], selectedSessionId?: string, currentSessionId?: string) =>
|
|
765
|
+
sessions.find((session) => session.id === selectedSessionId) ||
|
|
766
|
+
sessions.find((session) => session.id === currentSessionId) ||
|
|
767
|
+
sessions[sessions.length - 1];
|
|
768
|
+
|
|
769
|
+
const getSessionSelectorLabel = (session: TProfilerNavigationSession) => truncate(session.path || session.url || session.label, 56);
|
|
770
|
+
const truncate = (value: string, max = 96) => (value.length <= max ? value : `${value.slice(0, max)}...`);
|
|
771
|
+
const readNumber = (value: TTraceSummaryValue | undefined) => (typeof value === 'number' ? value : undefined);
|
|
772
|
+
const readString = (value: TTraceSummaryValue | undefined) => (typeof value === 'string' ? value : undefined);
|
|
773
|
+
const formatDuration = (value?: number) => (value === undefined ? 'pending' : `${Math.round(value)} ms`);
|
|
774
|
+
const formatBytes = (value?: number) => (value === undefined ? 'n/a' : `${(value / 1024).toFixed(value >= 1024 ? 1 : 2)} KB`);
|
|
775
|
+
const formatTimestamp = (value?: string) => {
|
|
776
|
+
if (!value) return 'never';
|
|
777
|
+
const date = new Date(value);
|
|
778
|
+
return Number.isNaN(date.valueOf()) ? value : date.toLocaleString();
|
|
779
|
+
};
|
|
780
|
+
const formatCronFrequency = (task: TProfilerCronTask) =>
|
|
781
|
+
task.frequency.kind === 'cron' ? task.frequency.value : `once at ${formatTimestamp(task.frequency.value)}`;
|
|
782
|
+
const formatStructuredValue = (value: unknown) => {
|
|
783
|
+
try {
|
|
784
|
+
return JSON.stringify(value, null, 2);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
return String(value);
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const renderSummaryValue = (value: TTraceSummaryValue | undefined): string => {
|
|
791
|
+
if (value === undefined) return '';
|
|
792
|
+
if (value === null) return 'null';
|
|
793
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
794
|
+
if (value.kind === 'undefined') return 'undefined';
|
|
795
|
+
if (value.kind === 'redacted') return `[redacted: ${value.reason}]`;
|
|
796
|
+
if (value.kind === 'error') return `${value.name}: ${value.message}`;
|
|
797
|
+
if (value.kind === 'array') return `Array(${value.length})`;
|
|
798
|
+
if (value.kind === 'object') return `${value.constructorName} { ${Object.keys(value.entries).join(', ')} }`;
|
|
799
|
+
if (value.kind === 'buffer') return `Buffer(${value.byteLength})`;
|
|
800
|
+
if ('value' in value) return String(value.value);
|
|
801
|
+
if ('size' in value) return String(value.size);
|
|
802
|
+
if ('name' in value) return value.name;
|
|
803
|
+
return JSON.stringify(value);
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const toSummaryJsonValue = (value: TTraceSummaryValue | undefined): unknown => {
|
|
807
|
+
if (value === undefined) return 'undefined';
|
|
808
|
+
if (value === null) return null;
|
|
809
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
810
|
+
if (value.kind === 'undefined') return 'undefined';
|
|
811
|
+
if (value.kind === 'redacted') return `[redacted: ${value.reason}]`;
|
|
812
|
+
if (value.kind === 'bigint') return `${value.value}n`;
|
|
813
|
+
if (value.kind === 'symbol') return value.value;
|
|
814
|
+
if (value.kind === 'function') return `[Function ${value.name}]`;
|
|
815
|
+
if (value.kind === 'date') return value.value;
|
|
816
|
+
if (value.kind === 'error') return { name: value.name, message: value.message, stack: value.stack };
|
|
817
|
+
if (value.kind === 'buffer') return `[Buffer ${value.byteLength} bytes]`;
|
|
818
|
+
if (value.kind === 'map') return `[Map(${value.size})]`;
|
|
819
|
+
if (value.kind === 'set') return `[Set(${value.size})]`;
|
|
820
|
+
if (value.kind === 'array') {
|
|
821
|
+
const items = value.items.map((item) => toSummaryJsonValue(item));
|
|
822
|
+
if (value.truncated) items.push(`... ${Math.max(0, value.length - value.items.length)} more item(s)`);
|
|
823
|
+
return items;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const objectValue: Record<string, unknown> = {};
|
|
827
|
+
for (const [key, entry] of Object.entries(value.entries)) objectValue[key] = toSummaryJsonValue(entry);
|
|
828
|
+
if (value.truncated) objectValue.__truncated = `${Math.max(0, value.keys.length - Object.keys(value.entries).length)} more key(s)`;
|
|
829
|
+
return objectValue;
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
const formatSummaryJson = (value: TTraceSummaryValue | undefined) => {
|
|
833
|
+
if (value === undefined) return 'undefined';
|
|
834
|
+
if (typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'undefined') return 'undefined';
|
|
835
|
+
return JSON.stringify(toSummaryJsonValue(value), null, 2);
|
|
836
|
+
};
|
|
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
|
+
|
|
881
|
+
const formatSummaryLiteral = (value: TTraceSummaryValue | undefined, depth = 1): string => {
|
|
882
|
+
if (value === undefined) return '';
|
|
883
|
+
if (value === null) return 'null';
|
|
884
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
885
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
886
|
+
if (depth <= 0) return renderSummaryValue(value);
|
|
887
|
+
if (value.kind === 'undefined') return 'undefined';
|
|
888
|
+
if (value.kind === 'redacted') return `[redacted: ${value.reason}]`;
|
|
889
|
+
if (value.kind === 'bigint') return `${value.value}n`;
|
|
890
|
+
if (value.kind === 'symbol') return value.value;
|
|
891
|
+
if (value.kind === 'function') return `[Function ${value.name}]`;
|
|
892
|
+
if (value.kind === 'date') return JSON.stringify(value.value);
|
|
893
|
+
if (value.kind === 'error') return `${value.name}(${JSON.stringify(value.message)})`;
|
|
894
|
+
if (value.kind === 'buffer') return `Buffer(${value.byteLength})`;
|
|
895
|
+
if (value.kind === 'map') return `Map(${value.size})`;
|
|
896
|
+
if (value.kind === 'set') return `Set(${value.size})`;
|
|
897
|
+
if (value.kind === 'array') {
|
|
898
|
+
const items = value.items.slice(0, 4).map((item) => formatSummaryLiteral(item, depth - 1));
|
|
899
|
+
if (value.truncated || value.length > items.length) items.push('...');
|
|
900
|
+
return `[${items.join(', ')}]`;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const entries = Object.entries(value.entries)
|
|
904
|
+
.slice(0, 4)
|
|
905
|
+
.map(([key, entry]) => `${key}: ${formatSummaryLiteral(entry, depth - 1)}`);
|
|
906
|
+
if (value.truncated || value.keys.length > entries.length) entries.push('...');
|
|
907
|
+
return `{ ${entries.join(', ')} }`;
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
const getApiReferenceName = (method: string, path: string, fallbackLabel?: string) => {
|
|
911
|
+
if (path.startsWith('/api/')) return path.slice('/api/'.length).split('/').filter(Boolean).join('.');
|
|
912
|
+
|
|
913
|
+
const rawName = `${method} ${path}`.trim();
|
|
914
|
+
if (rawName) return rawName;
|
|
915
|
+
return fallbackLabel || 'request';
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
const formatApiReference = (method: string, path: string, requestData?: TTraceSummaryValue, fallbackLabel?: string) => {
|
|
919
|
+
const args = formatSummaryLiteral(requestData, 1);
|
|
920
|
+
return `${getApiReferenceName(method, path, fallbackLabel)}(${truncate(args, 112)})`;
|
|
921
|
+
};
|
|
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
|
+
|
|
943
|
+
const getTraceRequestData = (trace: TRequestTrace | undefined) =>
|
|
944
|
+
trace?.events.find((event) => event.type === 'request.start')?.details.data;
|
|
945
|
+
|
|
946
|
+
const getTraceResultData = (trace: TRequestTrace | undefined) =>
|
|
947
|
+
[...findTraceEvents(trace, ['controller.result'])]
|
|
948
|
+
.reverse()
|
|
949
|
+
.find((event) => event.details.kind === 'json' && event.details.data !== undefined)?.details.data;
|
|
950
|
+
|
|
951
|
+
const getRequestStatusText = (statusCode?: number, statusLabel?: string) =>
|
|
952
|
+
statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
|
|
953
|
+
|
|
954
|
+
const findTraceEvents = (trace: TRequestTrace | undefined, eventTypes: string[]) =>
|
|
955
|
+
trace?.events.filter((event) => eventTypes.includes(event.type)) || [];
|
|
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
|
+
|
|
996
|
+
const getSummary = (session: TProfilerNavigationSession): TSessionSummary => {
|
|
997
|
+
const primaryTrace =
|
|
998
|
+
session.traces.find((trace) => trace.kind === 'initial-root' && trace.trace) ||
|
|
999
|
+
session.traces.find((trace) => trace.kind === 'navigation-data' && trace.trace) ||
|
|
1000
|
+
session.traces.find((trace) => trace.trace);
|
|
1001
|
+
const trace = primaryTrace?.trace;
|
|
1002
|
+
const syncCalls = session.traces.flatMap((traceItem) =>
|
|
1003
|
+
traceItem.trace?.calls.filter((call) => call.origin === 'ssr-fetcher' || call.origin === 'api-batch-fetcher') || [],
|
|
1004
|
+
);
|
|
1005
|
+
const asyncCount = session.traces.filter((traceItem) => traceItem.kind === 'async').length;
|
|
1006
|
+
const errorCount =
|
|
1007
|
+
session.steps.filter((step) => step.status === 'error').length +
|
|
1008
|
+
session.traces.filter((traceItem) => traceItem.status === 'error').length +
|
|
1009
|
+
syncCalls.filter((call) => call.errorMessage || (call.statusCode !== undefined && call.statusCode >= 400)).length;
|
|
1010
|
+
const renderStart = trace?.events.find((event) => event.type === 'render.start');
|
|
1011
|
+
const renderEnd = trace?.events.find((event) => event.type === 'render.end');
|
|
1012
|
+
const localRender = [...session.steps].reverse().find((step) => step.label === 'Render' && step.durationMs !== undefined);
|
|
1013
|
+
const ssrPayload = trace?.events.find((event) => event.type === 'ssr.payload');
|
|
1014
|
+
const routeLabel = session.routeLabel || readString(renderStart?.details.routeId) || session.path;
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
apiAsyncCount: asyncCount,
|
|
1018
|
+
apiSyncCount: syncCalls.length,
|
|
1019
|
+
errorCount,
|
|
1020
|
+
primaryTrace,
|
|
1021
|
+
renderMs:
|
|
1022
|
+
renderStart && renderEnd
|
|
1023
|
+
? Math.max(0, renderEnd.elapsedMs - renderStart.elapsedMs)
|
|
1024
|
+
: localRender?.durationMs,
|
|
1025
|
+
routeLabel,
|
|
1026
|
+
ssrPayloadBytes: readNumber(ssrPayload?.details.serializedBytes),
|
|
1027
|
+
statusLabel: session.kind === 'client-navigation' ? 'NAV' : trace ? `${trace.statusCode || 'pending'} ${trace.method}` : 'SSR',
|
|
1028
|
+
totalMs: session.kind === 'client-navigation' ? session.durationMs : trace?.durationMs ?? session.durationMs,
|
|
1029
|
+
};
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
const StatusToken = ({ label, onClick, tone = 'ok' }: { label: string; onClick: () => void; tone?: 'ok' | 'warn' | 'error' }) => (
|
|
1033
|
+
<button className={`proteum-profiler__token proteum-profiler__token--${tone}`} onClick={onClick} type="button">
|
|
1034
|
+
{label}
|
|
1035
|
+
</button>
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
const SummaryRow = ({ label, value }: { label: string; value: React.ReactNode }) => (
|
|
1039
|
+
<div className="proteum-profiler__metricRow">
|
|
1040
|
+
<div className="proteum-profiler__metricLabel">{label}</div>
|
|
1041
|
+
<div className="proteum-profiler__metricValue">{value}</div>
|
|
1042
|
+
</div>
|
|
1043
|
+
);
|
|
1044
|
+
|
|
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,
|
|
1081
|
+
}: {
|
|
1082
|
+
isSelected: boolean;
|
|
1083
|
+
item: TApiRequestItem;
|
|
1084
|
+
onSelect: () => void;
|
|
1085
|
+
}) => {
|
|
1086
|
+
const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
|
|
1087
|
+
|
|
1088
|
+
return (
|
|
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}
|
|
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>
|
|
1124
|
+
</div>
|
|
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
|
+
)}
|
|
1275
|
+
</div>
|
|
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'}
|
|
1282
|
+
</div>
|
|
1283
|
+
</div>
|
|
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>
|
|
1325
|
+
</div>
|
|
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>
|
|
1338
|
+
</div>
|
|
1339
|
+
{trace ? (
|
|
1340
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">
|
|
1341
|
+
{formatProfilerRequestReference({
|
|
1342
|
+
method: trace.method,
|
|
1343
|
+
path: trace.path,
|
|
1344
|
+
requestData: getTraceRequestData(trace),
|
|
1345
|
+
})}
|
|
1346
|
+
</div>
|
|
1347
|
+
) : null}
|
|
1348
|
+
</div>
|
|
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>
|
|
1373
|
+
);
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
const TraceRows = ({
|
|
1377
|
+
onSelect,
|
|
1378
|
+
selectedEventKey,
|
|
1379
|
+
trace,
|
|
1380
|
+
}: {
|
|
1381
|
+
onSelect: (selectionKey: string) => void;
|
|
1382
|
+
selectedEventKey?: string;
|
|
1383
|
+
trace: TRequestTrace;
|
|
1384
|
+
}) => (
|
|
1385
|
+
<div className="proteum-profiler__section">
|
|
1386
|
+
<div className="proteum-profiler__sectionHeader">
|
|
1387
|
+
<div className="proteum-profiler__sectionTitle">
|
|
1388
|
+
{formatProfilerRequestReference({
|
|
1389
|
+
method: trace.method,
|
|
1390
|
+
path: trace.path,
|
|
1391
|
+
requestData: getTraceRequestData(trace),
|
|
1392
|
+
})}
|
|
1393
|
+
</div>
|
|
1394
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">{trace.id}</div>
|
|
1395
|
+
</div>
|
|
1396
|
+
|
|
1397
|
+
{trace.calls.length > 0 && (
|
|
1398
|
+
<div className="proteum-profiler__list">
|
|
1399
|
+
{trace.calls.map((call) => (
|
|
1400
|
+
<div className="proteum-profiler__row" key={call.id}>
|
|
1401
|
+
<div className="proteum-profiler__rowHeader">
|
|
1402
|
+
<strong>{formatTraceCallDisplay(call)}</strong>
|
|
1403
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
1404
|
+
{formatDuration(call.durationMs)}
|
|
1405
|
+
{call.statusCode !== undefined ? ` | ${call.statusCode}` : ''}
|
|
1406
|
+
</span>
|
|
1407
|
+
</div>
|
|
1408
|
+
<div className="proteum-profiler__tags">
|
|
1409
|
+
<span className="proteum-profiler__tag">{call.origin}</span>
|
|
1410
|
+
{call.fetcherId ? <span className="proteum-profiler__tag">fetcher:{call.fetcherId}</span> : null}
|
|
1411
|
+
{call.requestDataKeys.map((key) => (
|
|
1412
|
+
<span className="proteum-profiler__tag" key={`${call.id}:req:${key}`}>
|
|
1413
|
+
req:{key}
|
|
1414
|
+
</span>
|
|
1415
|
+
))}
|
|
1416
|
+
{call.resultKeys.map((key) => (
|
|
1417
|
+
<span className="proteum-profiler__tag" key={`${call.id}:res:${key}`}>
|
|
1418
|
+
res:{key}
|
|
1419
|
+
</span>
|
|
1420
|
+
))}
|
|
1421
|
+
{call.errorMessage ? <span className="proteum-profiler__tag">{truncate(call.errorMessage, 72)}</span> : null}
|
|
1422
|
+
</div>
|
|
1423
|
+
</div>
|
|
1424
|
+
))}
|
|
1425
|
+
</div>
|
|
1426
|
+
)}
|
|
1427
|
+
|
|
1428
|
+
<div className="proteum-profiler__list">
|
|
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
|
+
})}
|
|
1469
|
+
</div>
|
|
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
|
+
})}
|
|
1492
|
+
</div>
|
|
1493
|
+
</div>
|
|
1494
|
+
);
|
|
1495
|
+
|
|
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
|
+
}) => (
|
|
1889
|
+
<div className="proteum-profiler__section">
|
|
1890
|
+
{showTitle ? (
|
|
1891
|
+
<div className="proteum-profiler__titleRow">
|
|
1892
|
+
<div className="proteum-profiler__sectionTitle">{title}</div>
|
|
1893
|
+
</div>
|
|
1894
|
+
) : null}
|
|
1895
|
+
{rows.length === 0 ? (
|
|
1896
|
+
<div className="proteum-profiler__empty">{empty}</div>
|
|
1897
|
+
) : (
|
|
1898
|
+
<div className="proteum-profiler__list">
|
|
1899
|
+
{rows.map((row) => (
|
|
1900
|
+
<div className="proteum-profiler__row" key={row.key}>
|
|
1901
|
+
<div className="proteum-profiler__rowHeader">
|
|
1902
|
+
<strong>{row.title}</strong>
|
|
1903
|
+
</div>
|
|
1904
|
+
<div className="proteum-profiler__mono">{row.value}</div>
|
|
1905
|
+
</div>
|
|
1906
|
+
))}
|
|
1907
|
+
</div>
|
|
1908
|
+
)}
|
|
1909
|
+
</div>
|
|
1910
|
+
);
|
|
1911
|
+
|
|
1912
|
+
const TextBlocks = ({ blocks }: { blocks: THumanTextBlock[] }) => (
|
|
1913
|
+
<>
|
|
1914
|
+
{blocks.map((block) => (
|
|
1915
|
+
<div className="proteum-profiler__section" key={block.title}>
|
|
1916
|
+
<div className="proteum-profiler__titleRow">
|
|
1917
|
+
<div className="proteum-profiler__sectionTitle">{block.title}</div>
|
|
1918
|
+
</div>
|
|
1919
|
+
{block.items.length === 0 ? (
|
|
1920
|
+
<div className="proteum-profiler__empty">{block.empty || 'none'}</div>
|
|
1921
|
+
) : (
|
|
1922
|
+
<div className="proteum-profiler__list">
|
|
1923
|
+
{block.items.map((item, index) => (
|
|
1924
|
+
<div className="proteum-profiler__row" key={`${block.title}:${index}`}>
|
|
1925
|
+
<div className="proteum-profiler__mono">{item}</div>
|
|
1926
|
+
</div>
|
|
1927
|
+
))}
|
|
1928
|
+
</div>
|
|
1929
|
+
)}
|
|
1930
|
+
</div>
|
|
1931
|
+
))}
|
|
1932
|
+
</>
|
|
1933
|
+
);
|
|
1934
|
+
|
|
1935
|
+
const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession, summary: TSessionSummary, state: TProfilerState) => {
|
|
1936
|
+
const primaryTrace = summary.primaryTrace?.trace;
|
|
1937
|
+
|
|
1938
|
+
if (panel === 'summary') {
|
|
1939
|
+
return (
|
|
1940
|
+
<div className="proteum-profiler__metrics">
|
|
1941
|
+
<SummaryRow label="Session" value={session.label} />
|
|
1942
|
+
<SummaryRow label="Status" value={summary.statusLabel} />
|
|
1943
|
+
<SummaryRow label="Duration" value={formatDuration(summary.totalMs)} />
|
|
1944
|
+
<SummaryRow label="Route" value={summary.routeLabel} />
|
|
1945
|
+
<SummaryRow
|
|
1946
|
+
label="SSR"
|
|
1947
|
+
value={
|
|
1948
|
+
summary.ssrPayloadBytes !== undefined
|
|
1949
|
+
? `${formatDuration(summary.renderMs)} | ${formatBytes(summary.ssrPayloadBytes)}`
|
|
1950
|
+
: formatDuration(summary.renderMs)
|
|
1951
|
+
}
|
|
1952
|
+
/>
|
|
1953
|
+
<SummaryRow label="API" value={`sync ${summary.apiSyncCount} / async ${summary.apiAsyncCount}`} />
|
|
1954
|
+
<SummaryRow label="Errors" value={String(summary.errorCount)} />
|
|
1955
|
+
<SummaryRow label="Request" value={session.requestId || 'client-only'} />
|
|
1956
|
+
</div>
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
if (panel === 'timeline') {
|
|
1961
|
+
return <TimelinePanel session={session} />;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (panel === 'auth') {
|
|
1965
|
+
return <AuthPanel session={session} />;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
if (panel === 'routing') {
|
|
1969
|
+
return (
|
|
1970
|
+
<SimpleSection
|
|
1971
|
+
empty="No routing data captured yet."
|
|
1972
|
+
rows={findTraceEvents(primaryTrace, [
|
|
1973
|
+
'resolve.start',
|
|
1974
|
+
'resolve.controller-route',
|
|
1975
|
+
'resolve.route-match',
|
|
1976
|
+
'resolve.routes-evaluated',
|
|
1977
|
+
'resolve.not-found',
|
|
1978
|
+
]).map((event) => ({
|
|
1979
|
+
key: `${event.index}:${event.type}`,
|
|
1980
|
+
title: event.type,
|
|
1981
|
+
value: Object.entries(event.details)
|
|
1982
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
1983
|
+
.join(' '),
|
|
1984
|
+
}))}
|
|
1985
|
+
showTitle={false}
|
|
1986
|
+
title="Routing"
|
|
1987
|
+
/>
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (panel === 'controller') {
|
|
1992
|
+
return (
|
|
1993
|
+
<SimpleSection
|
|
1994
|
+
empty="No controller data captured yet."
|
|
1995
|
+
rows={findTraceEvents(primaryTrace, ['controller.start', 'controller.result', 'setup.options', 'context.create']).map(
|
|
1996
|
+
(event) => ({
|
|
1997
|
+
key: `${event.index}:${event.type}`,
|
|
1998
|
+
title: event.type,
|
|
1999
|
+
value: Object.entries(event.details)
|
|
2000
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
2001
|
+
.join(' '),
|
|
2002
|
+
}),
|
|
2003
|
+
)}
|
|
2004
|
+
showTitle={false}
|
|
2005
|
+
title="Controller"
|
|
2006
|
+
/>
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
if (panel === 'ssr') {
|
|
2011
|
+
return (
|
|
2012
|
+
<SimpleSection
|
|
2013
|
+
empty="No SSR data captured for this session."
|
|
2014
|
+
rows={findTraceEvents(primaryTrace, ['page.data', 'ssr.payload', 'render.start', 'render.end']).map((event) => ({
|
|
2015
|
+
key: `${event.index}:${event.type}`,
|
|
2016
|
+
title: event.type,
|
|
2017
|
+
value: Object.entries(event.details)
|
|
2018
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
2019
|
+
.join(' '),
|
|
2020
|
+
}))}
|
|
2021
|
+
showTitle={false}
|
|
2022
|
+
title="SSR"
|
|
2023
|
+
/>
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
if (panel === 'api') {
|
|
2028
|
+
return <ApiPanel session={session} />;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
if (panel === 'explain') {
|
|
2032
|
+
const explain = state.explain;
|
|
2033
|
+
const blocks = explain.manifest
|
|
2034
|
+
? [
|
|
2035
|
+
{ title: 'Overview', items: buildExplainSummaryItems(explain.manifest) },
|
|
2036
|
+
...buildExplainBlocks(explain.manifest, [...explainSectionNames]),
|
|
2037
|
+
]
|
|
2038
|
+
: [];
|
|
2039
|
+
|
|
2040
|
+
return (
|
|
2041
|
+
<div className="proteum-profiler__section">
|
|
2042
|
+
<div className="proteum-profiler__sectionHeader">
|
|
2043
|
+
<div className="proteum-profiler__sectionTitle">Explain</div>
|
|
2044
|
+
<div className="proteum-profiler__actions">
|
|
2045
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshExplain()} type="button">
|
|
2046
|
+
Refresh
|
|
2047
|
+
</button>
|
|
2048
|
+
</div>
|
|
2049
|
+
</div>
|
|
2050
|
+
|
|
2051
|
+
{explain.errorMessage ? (
|
|
2052
|
+
<div className="proteum-profiler__row">
|
|
2053
|
+
<div className="proteum-profiler__rowHeader">
|
|
2054
|
+
<strong>Last explain panel error</strong>
|
|
2055
|
+
</div>
|
|
2056
|
+
<div className="proteum-profiler__mono">{explain.errorMessage}</div>
|
|
2057
|
+
</div>
|
|
2058
|
+
) : null}
|
|
2059
|
+
|
|
2060
|
+
{explain.status === 'loading' && !explain.manifest ? (
|
|
2061
|
+
<div className="proteum-profiler__empty">Loading explain data...</div>
|
|
2062
|
+
) : !explain.manifest ? (
|
|
2063
|
+
<div className="proteum-profiler__empty">No explain manifest is available.</div>
|
|
2064
|
+
) : (
|
|
2065
|
+
<>
|
|
2066
|
+
<div className="proteum-profiler__row">
|
|
2067
|
+
<div className="proteum-profiler__rowHeader">
|
|
2068
|
+
<strong>Manifest snapshot</strong>
|
|
2069
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
2070
|
+
{explain.lastLoadedAt ? formatTimestamp(explain.lastLoadedAt) : 'Not loaded'}
|
|
2071
|
+
</span>
|
|
2072
|
+
</div>
|
|
2073
|
+
<div className="proteum-profiler__mono">
|
|
2074
|
+
Same manifest-backed sections as `proteum explain`, rendered from the shared diagnostics contract.
|
|
2075
|
+
</div>
|
|
2076
|
+
</div>
|
|
2077
|
+
<TextBlocks blocks={blocks} />
|
|
2078
|
+
</>
|
|
2079
|
+
)}
|
|
2080
|
+
</div>
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
if (panel === 'doctor') {
|
|
2085
|
+
const doctor = state.doctor;
|
|
2086
|
+
const doctorRows =
|
|
2087
|
+
doctor.response?.diagnostics.map((diagnostic, index) => ({
|
|
2088
|
+
key: `${diagnostic.code}:${index}`,
|
|
2089
|
+
title: `[${diagnostic.level}] ${diagnostic.code}`,
|
|
2090
|
+
value: `${diagnostic.message} source=${diagnostic.filepath}${formatManifestLocation(
|
|
2091
|
+
diagnostic.sourceLocation?.line,
|
|
2092
|
+
diagnostic.sourceLocation?.column,
|
|
2093
|
+
)}${diagnostic.relatedFilepaths?.length ? ` related=${diagnostic.relatedFilepaths.join(',')}` : ''}`,
|
|
2094
|
+
})) || [];
|
|
2095
|
+
const doctorBlocks = state.explain.manifest ? buildDoctorBlocks(state.explain.manifest) : [];
|
|
2096
|
+
|
|
2097
|
+
return (
|
|
2098
|
+
<div className="proteum-profiler__section">
|
|
2099
|
+
<div className="proteum-profiler__sectionHeader">
|
|
2100
|
+
<div className="proteum-profiler__sectionTitle">Doctor</div>
|
|
2101
|
+
<div className="proteum-profiler__actions">
|
|
2102
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshDoctor()} type="button">
|
|
2103
|
+
Refresh
|
|
2104
|
+
</button>
|
|
2105
|
+
</div>
|
|
2106
|
+
</div>
|
|
2107
|
+
|
|
2108
|
+
{doctor.errorMessage ? (
|
|
2109
|
+
<div className="proteum-profiler__row">
|
|
2110
|
+
<div className="proteum-profiler__rowHeader">
|
|
2111
|
+
<strong>Last doctor panel error</strong>
|
|
2112
|
+
</div>
|
|
2113
|
+
<div className="proteum-profiler__mono">{doctor.errorMessage}</div>
|
|
2114
|
+
</div>
|
|
2115
|
+
) : null}
|
|
2116
|
+
|
|
2117
|
+
{doctor.status === 'loading' && !doctor.response ? (
|
|
2118
|
+
<div className="proteum-profiler__empty">Loading doctor diagnostics...</div>
|
|
2119
|
+
) : !doctor.response ? (
|
|
2120
|
+
<div className="proteum-profiler__empty">No doctor diagnostics are available.</div>
|
|
2121
|
+
) : (
|
|
2122
|
+
<>
|
|
2123
|
+
<div className="proteum-profiler__metrics">
|
|
2124
|
+
<SummaryRow label="Errors" value={String(doctor.response.summary.errors)} />
|
|
2125
|
+
<SummaryRow label="Warnings" value={String(doctor.response.summary.warnings)} />
|
|
2126
|
+
<SummaryRow label="Strict" value={doctor.response.summary.strictFailed ? 'failed' : 'ok'} />
|
|
2127
|
+
<SummaryRow
|
|
2128
|
+
label="Refreshed"
|
|
2129
|
+
value={doctor.lastLoadedAt ? formatTimestamp(doctor.lastLoadedAt) : 'Not loaded'}
|
|
2130
|
+
/>
|
|
2131
|
+
</div>
|
|
2132
|
+
{doctorBlocks.length > 0 ? (
|
|
2133
|
+
<TextBlocks blocks={doctorBlocks} />
|
|
2134
|
+
) : (
|
|
2135
|
+
<SimpleSection empty="No manifest diagnostics were found." rows={doctorRows} title="Diagnostics" />
|
|
2136
|
+
)}
|
|
2137
|
+
</>
|
|
2138
|
+
)}
|
|
2139
|
+
</div>
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
if (panel === 'commands') {
|
|
2144
|
+
const commandsState = state.commands;
|
|
2145
|
+
|
|
2146
|
+
return (
|
|
2147
|
+
<div className="proteum-profiler__section">
|
|
2148
|
+
<div className="proteum-profiler__sectionHeader">
|
|
2149
|
+
<div className="proteum-profiler__sectionTitle">Available commands</div>
|
|
2150
|
+
<div className="proteum-profiler__actions">
|
|
2151
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshCommands()} type="button">
|
|
2152
|
+
Refresh
|
|
2153
|
+
</button>
|
|
2154
|
+
</div>
|
|
2155
|
+
</div>
|
|
2156
|
+
|
|
2157
|
+
<div className="proteum-profiler__row">
|
|
2158
|
+
<div className="proteum-profiler__rowHeader">
|
|
2159
|
+
<strong>Dev commands</strong>
|
|
2160
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
2161
|
+
{commandsState.commands.length} command{commandsState.commands.length === 1 ? '' : 's'}
|
|
2162
|
+
</span>
|
|
2163
|
+
</div>
|
|
2164
|
+
<div className="proteum-profiler__mono">
|
|
2165
|
+
Commands live under /commands, extend the Proteum Commands class, and run only in a dev context.
|
|
2166
|
+
</div>
|
|
2167
|
+
</div>
|
|
2168
|
+
|
|
2169
|
+
{commandsState.errorMessage ? (
|
|
2170
|
+
<div className="proteum-profiler__row">
|
|
2171
|
+
<div className="proteum-profiler__rowHeader">
|
|
2172
|
+
<strong>Last command panel error</strong>
|
|
2173
|
+
</div>
|
|
2174
|
+
<div className="proteum-profiler__mono">{commandsState.errorMessage}</div>
|
|
2175
|
+
</div>
|
|
2176
|
+
) : null}
|
|
2177
|
+
|
|
2178
|
+
{commandsState.status === 'loading' && commandsState.commands.length === 0 ? (
|
|
2179
|
+
<div className="proteum-profiler__empty">Loading commands...</div>
|
|
2180
|
+
) : commandsState.commands.length === 0 ? (
|
|
2181
|
+
<div className="proteum-profiler__empty">No commands are registered for this app.</div>
|
|
2182
|
+
) : (
|
|
2183
|
+
<div className="proteum-profiler__list">
|
|
2184
|
+
{commandsState.commands.map((command: TDevCommandDefinition) => {
|
|
2185
|
+
const execution = commandsState.executions[command.path] as TDevCommandExecution | undefined;
|
|
2186
|
+
return (
|
|
2187
|
+
<div className="proteum-profiler__row" key={command.path}>
|
|
2188
|
+
<div className="proteum-profiler__rowHeader">
|
|
2189
|
+
<strong>{command.path}</strong>
|
|
2190
|
+
<div className="proteum-profiler__actions">
|
|
2191
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
2192
|
+
{execution ? formatTimestamp(execution.finishedAt) : 'Never run'}
|
|
2193
|
+
</span>
|
|
2194
|
+
<button
|
|
2195
|
+
className="proteum-profiler__pill"
|
|
2196
|
+
onClick={() => void profilerRuntime.runCommand(command.path)}
|
|
2197
|
+
type="button"
|
|
2198
|
+
>
|
|
2199
|
+
Run now
|
|
2200
|
+
</button>
|
|
2201
|
+
</div>
|
|
2202
|
+
</div>
|
|
2203
|
+
|
|
2204
|
+
<div className="proteum-profiler__tags">
|
|
2205
|
+
<span className="proteum-profiler__tag">{command.className}</span>
|
|
2206
|
+
<span className="proteum-profiler__tag">{command.methodName}</span>
|
|
2207
|
+
<span className="proteum-profiler__tag">{command.scope}</span>
|
|
2208
|
+
{execution ? <span className="proteum-profiler__tag">{execution.status}</span> : null}
|
|
2209
|
+
{execution ? (
|
|
2210
|
+
<span className="proteum-profiler__tag">{formatDuration(execution.durationMs)}</span>
|
|
2211
|
+
) : null}
|
|
2212
|
+
{execution?.errorMessage ? (
|
|
2213
|
+
<span className="proteum-profiler__tag">{truncate(execution.errorMessage, 72)}</span>
|
|
2214
|
+
) : null}
|
|
2215
|
+
</div>
|
|
2216
|
+
|
|
2217
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">
|
|
2218
|
+
source {command.filepath}:{command.sourceLocation.line}:{command.sourceLocation.column}
|
|
2219
|
+
{commandsState.lastLoadedAt
|
|
2220
|
+
? ` | refreshed ${formatTimestamp(commandsState.lastLoadedAt)}`
|
|
2221
|
+
: ''}
|
|
2222
|
+
</div>
|
|
2223
|
+
|
|
2224
|
+
{execution ? (
|
|
2225
|
+
<div className="proteum-profiler__section">
|
|
2226
|
+
<div className="proteum-profiler__sectionTitle">Last result</div>
|
|
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
|
+
/>
|
|
2236
|
+
</div>
|
|
2237
|
+
) : null}
|
|
2238
|
+
</div>
|
|
2239
|
+
);
|
|
2240
|
+
})}
|
|
2241
|
+
</div>
|
|
2242
|
+
)}
|
|
2243
|
+
</div>
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
if (panel === 'cron') {
|
|
2248
|
+
const cron = state.cron;
|
|
2249
|
+
|
|
2250
|
+
return (
|
|
2251
|
+
<div className="proteum-profiler__section">
|
|
2252
|
+
<div className="proteum-profiler__sectionHeader">
|
|
2253
|
+
<div className="proteum-profiler__sectionTitle">Registered tasks</div>
|
|
2254
|
+
<div className="proteum-profiler__actions">
|
|
2255
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshCronTasks()} type="button">
|
|
2256
|
+
Refresh
|
|
2257
|
+
</button>
|
|
2258
|
+
</div>
|
|
2259
|
+
</div>
|
|
2260
|
+
|
|
2261
|
+
<div className="proteum-profiler__row">
|
|
2262
|
+
<div className="proteum-profiler__rowHeader">
|
|
2263
|
+
<strong>Dev mode</strong>
|
|
2264
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
2265
|
+
{cron.tasks.length} task{cron.tasks.length === 1 ? '' : 's'}
|
|
2266
|
+
</span>
|
|
2267
|
+
</div>
|
|
2268
|
+
<div className="proteum-profiler__mono">
|
|
2269
|
+
Automatic execution is disabled in dev. Registered cron tasks stay visible here and only run when
|
|
2270
|
+
triggered manually.
|
|
2271
|
+
</div>
|
|
2272
|
+
</div>
|
|
2273
|
+
|
|
2274
|
+
{cron.errorMessage ? (
|
|
2275
|
+
<div className="proteum-profiler__row">
|
|
2276
|
+
<div className="proteum-profiler__rowHeader">
|
|
2277
|
+
<strong>Last cron panel error</strong>
|
|
2278
|
+
</div>
|
|
2279
|
+
<div className="proteum-profiler__mono">{cron.errorMessage}</div>
|
|
2280
|
+
</div>
|
|
2281
|
+
) : null}
|
|
2282
|
+
|
|
2283
|
+
{cron.status === 'loading' && cron.tasks.length === 0 ? (
|
|
2284
|
+
<div className="proteum-profiler__empty">Loading cron tasks...</div>
|
|
2285
|
+
) : cron.tasks.length === 0 ? (
|
|
2286
|
+
<div className="proteum-profiler__empty">No cron tasks are registered for this app.</div>
|
|
2287
|
+
) : (
|
|
2288
|
+
<div className="proteum-profiler__list">
|
|
2289
|
+
{cron.tasks.map((task) => (
|
|
2290
|
+
<div className="proteum-profiler__row" key={task.name}>
|
|
2291
|
+
<div className="proteum-profiler__rowHeader">
|
|
2292
|
+
<strong>{task.name}</strong>
|
|
2293
|
+
<div className="proteum-profiler__actions">
|
|
2294
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
2295
|
+
{task.running
|
|
2296
|
+
? 'Running...'
|
|
2297
|
+
: task.lastRunFinishedAt
|
|
2298
|
+
? formatTimestamp(task.lastRunFinishedAt)
|
|
2299
|
+
: 'Never run'}
|
|
2300
|
+
</span>
|
|
2301
|
+
<button
|
|
2302
|
+
className="proteum-profiler__pill"
|
|
2303
|
+
disabled={task.running}
|
|
2304
|
+
onClick={() => void profilerRuntime.runCronTask(task.name)}
|
|
2305
|
+
type="button"
|
|
2306
|
+
>
|
|
2307
|
+
{task.running ? 'Running...' : 'Run now'}
|
|
2308
|
+
</button>
|
|
2309
|
+
</div>
|
|
2310
|
+
</div>
|
|
2311
|
+
|
|
2312
|
+
<div className="proteum-profiler__tags">
|
|
2313
|
+
<span className="proteum-profiler__tag">schedule:{truncate(formatCronFrequency(task), 64)}</span>
|
|
2314
|
+
<span className="proteum-profiler__tag">
|
|
2315
|
+
next:{task.nextInvocation ? formatTimestamp(task.nextInvocation) : 'none'}
|
|
2316
|
+
</span>
|
|
2317
|
+
<span className="proteum-profiler__tag">autoexec:{task.autoexec ? 'yes' : 'no'}</span>
|
|
2318
|
+
<span className="proteum-profiler__tag">
|
|
2319
|
+
automatic:{task.automaticExecution ? 'enabled' : 'disabled in dev'}
|
|
2320
|
+
</span>
|
|
2321
|
+
<span className="proteum-profiler__tag">runs:{task.runCount}</span>
|
|
2322
|
+
{task.lastTrigger ? <span className="proteum-profiler__tag">trigger:{task.lastTrigger}</span> : null}
|
|
2323
|
+
{task.lastRunStatus ? <span className="proteum-profiler__tag">{task.lastRunStatus}</span> : null}
|
|
2324
|
+
{task.lastRunDurationMs !== undefined ? (
|
|
2325
|
+
<span className="proteum-profiler__tag">{formatDuration(task.lastRunDurationMs)}</span>
|
|
2326
|
+
) : null}
|
|
2327
|
+
{task.lastErrorMessage ? (
|
|
2328
|
+
<span className="proteum-profiler__tag">{truncate(task.lastErrorMessage, 72)}</span>
|
|
2329
|
+
) : null}
|
|
2330
|
+
</div>
|
|
2331
|
+
|
|
2332
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">
|
|
2333
|
+
registered {formatTimestamp(task.registeredAt)}
|
|
2334
|
+
{cron.lastLoadedAt ? ` | refreshed ${formatTimestamp(cron.lastLoadedAt)}` : ''}
|
|
2335
|
+
</div>
|
|
2336
|
+
</div>
|
|
2337
|
+
))}
|
|
2338
|
+
</div>
|
|
2339
|
+
)}
|
|
2340
|
+
</div>
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
const errorRows = [
|
|
2345
|
+
...session.steps
|
|
2346
|
+
.filter((step) => step.status === 'error')
|
|
2347
|
+
.map((step) => ({ key: step.id, title: step.label, value: step.errorMessage || 'Step failed' })),
|
|
2348
|
+
...session.traces
|
|
2349
|
+
.filter((trace) => trace.status === 'error')
|
|
2350
|
+
.map((trace) => ({ key: trace.id, title: trace.label, value: trace.errorMessage || 'Request failed' })),
|
|
2351
|
+
...findTraceEvents(primaryTrace, ['error']).map((event) => ({
|
|
2352
|
+
key: `${event.index}:error`,
|
|
2353
|
+
title: event.type,
|
|
2354
|
+
value: Object.entries(event.details)
|
|
2355
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
2356
|
+
.join(' '),
|
|
2357
|
+
})),
|
|
2358
|
+
];
|
|
2359
|
+
|
|
2360
|
+
return <SimpleSection empty="No errors captured." rows={errorRows} showTitle={false} title="Errors" />;
|
|
2361
|
+
};
|
|
2362
|
+
|
|
2363
|
+
export default function DevProfiler() {
|
|
2364
|
+
const [state, setState] = React.useState(() => profilerRuntime.getState());
|
|
2365
|
+
|
|
2366
|
+
React.useEffect(() => profilerRuntime.subscribe(() => setState(profilerRuntime.getState())), []);
|
|
2367
|
+
React.useEffect(() => {
|
|
2368
|
+
void profilerRuntime.refreshCommands();
|
|
2369
|
+
void profilerRuntime.refreshCronTasks();
|
|
2370
|
+
}, []);
|
|
2371
|
+
|
|
2372
|
+
React.useEffect(() => {
|
|
2373
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
2374
|
+
if (event.key === 'Escape' && profilerRuntime.getState().uiState === 'expanded') {
|
|
2375
|
+
profilerRuntime.setUiState('minimized');
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2379
|
+
window.addEventListener('keydown', onKeyDown);
|
|
2380
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
2381
|
+
}, []);
|
|
2382
|
+
|
|
2383
|
+
if (!window.dev) return null;
|
|
2384
|
+
|
|
2385
|
+
const session = getSelectedSession(state.sessions, state.selectedSessionId, state.currentSessionId);
|
|
2386
|
+
if (!session) return null;
|
|
2387
|
+
|
|
2388
|
+
const summary = getSummary(session);
|
|
2389
|
+
const tone = summary.errorCount > 0 ? 'error' : (summary.totalMs || 0) > 500 ? 'warn' : 'ok';
|
|
2390
|
+
const primaryTrace = summary.primaryTrace?.trace;
|
|
2391
|
+
const minimizedLabel =
|
|
2392
|
+
session.kind === 'client-navigation'
|
|
2393
|
+
? session.label
|
|
2394
|
+
: primaryTrace
|
|
2395
|
+
? `${primaryTrace.statusCode || 'pending'} ${formatProfilerRequestReference({
|
|
2396
|
+
method: primaryTrace.method,
|
|
2397
|
+
path: primaryTrace.path,
|
|
2398
|
+
requestData: getTraceRequestData(primaryTrace),
|
|
2399
|
+
})}`
|
|
2400
|
+
: session.label;
|
|
2401
|
+
const recentSessions: TProfilerNavigationSession[] = state.sessions.slice(-6).reverse();
|
|
2402
|
+
|
|
2403
|
+
return (
|
|
2404
|
+
<div className="proteum-profiler">
|
|
2405
|
+
<style dangerouslySetInnerHTML={{ __html: profilerStyles }} />
|
|
2406
|
+
|
|
2407
|
+
{state.uiState === 'pinned-handle' ? (
|
|
2408
|
+
<button
|
|
2409
|
+
className="proteum-profiler__handle"
|
|
2410
|
+
onClick={() => profilerRuntime.setUiState('minimized')}
|
|
2411
|
+
type="button"
|
|
2412
|
+
>
|
|
2413
|
+
Proteum Profiler
|
|
2414
|
+
</button>
|
|
2415
|
+
) : (
|
|
2416
|
+
<>
|
|
2417
|
+
{state.uiState === 'expanded' ? (
|
|
2418
|
+
<div className="proteum-profiler__panel">
|
|
2419
|
+
<div className="proteum-profiler__panelHeader">
|
|
2420
|
+
<select
|
|
2421
|
+
aria-label="Profiler path selector"
|
|
2422
|
+
className="proteum-profiler__select"
|
|
2423
|
+
onChange={(event) => profilerRuntime.selectSession(event.currentTarget.value)}
|
|
2424
|
+
value={session.id}
|
|
2425
|
+
>
|
|
2426
|
+
{recentSessions.map((recentSession) => (
|
|
2427
|
+
<option key={recentSession.id} value={recentSession.id}>
|
|
2428
|
+
{getSessionSelectorLabel(recentSession)}
|
|
2429
|
+
</option>
|
|
2430
|
+
))}
|
|
2431
|
+
</select>
|
|
2432
|
+
|
|
2433
|
+
<div className="proteum-profiler__panelTabs">
|
|
2434
|
+
{(Object.keys(panelLabels) as TProfilerPanel[]).map((panel) => (
|
|
2435
|
+
<button
|
|
2436
|
+
className={`proteum-profiler__pill ${
|
|
2437
|
+
state.activePanel === panel ? 'proteum-profiler__pill--active' : ''
|
|
2438
|
+
}`}
|
|
2439
|
+
key={panel}
|
|
2440
|
+
onClick={() => profilerRuntime.openPanel(panel)}
|
|
2441
|
+
type="button"
|
|
2442
|
+
>
|
|
2443
|
+
{panelLabels[panel]}
|
|
2444
|
+
</button>
|
|
2445
|
+
))}
|
|
2446
|
+
</div>
|
|
2447
|
+
|
|
2448
|
+
<div className="proteum-profiler__actions">
|
|
2449
|
+
<button className="proteum-profiler__pill" onClick={() => profilerRuntime.setUiState('minimized')} type="button">
|
|
2450
|
+
Collapse
|
|
2451
|
+
</button>
|
|
2452
|
+
<button className="proteum-profiler__pill" onClick={() => profilerRuntime.setUiState('pinned-handle')} type="button">
|
|
2453
|
+
Hide
|
|
2454
|
+
</button>
|
|
2455
|
+
</div>
|
|
2456
|
+
</div>
|
|
2457
|
+
|
|
2458
|
+
<div className="proteum-profiler__panelBody">{renderPanel(state.activePanel, session, summary, state)}</div>
|
|
2459
|
+
</div>
|
|
2460
|
+
) : null}
|
|
2461
|
+
|
|
2462
|
+
<div className="proteum-profiler__bar">
|
|
2463
|
+
<button
|
|
2464
|
+
className="proteum-profiler__token proteum-profiler__token--brand"
|
|
2465
|
+
onClick={() => profilerRuntime.openPanel('summary')}
|
|
2466
|
+
type="button"
|
|
2467
|
+
>
|
|
2468
|
+
Proteum
|
|
2469
|
+
</button>
|
|
2470
|
+
<StatusToken label={truncate(minimizedLabel, 56)} onClick={() => profilerRuntime.openPanel('summary')} tone={tone} />
|
|
2471
|
+
<StatusToken
|
|
2472
|
+
label={formatDuration(summary.totalMs)}
|
|
2473
|
+
onClick={() => profilerRuntime.openPanel('summary')}
|
|
2474
|
+
tone={tone}
|
|
2475
|
+
/>
|
|
2476
|
+
<StatusToken
|
|
2477
|
+
label={truncate(summary.routeLabel, 28)}
|
|
2478
|
+
onClick={() => profilerRuntime.openPanel('routing')}
|
|
2479
|
+
tone="ok"
|
|
2480
|
+
/>
|
|
2481
|
+
<StatusToken
|
|
2482
|
+
label={
|
|
2483
|
+
summary.ssrPayloadBytes !== undefined
|
|
2484
|
+
? `${formatDuration(summary.renderMs)} ${formatBytes(summary.ssrPayloadBytes)}`
|
|
2485
|
+
: formatDuration(summary.renderMs)
|
|
2486
|
+
}
|
|
2487
|
+
onClick={() => profilerRuntime.openPanel('ssr')}
|
|
2488
|
+
tone="ok"
|
|
2489
|
+
/>
|
|
2490
|
+
<StatusToken
|
|
2491
|
+
label={`API ${summary.apiSyncCount} / ${summary.apiAsyncCount}`}
|
|
2492
|
+
onClick={() => profilerRuntime.openPanel('api')}
|
|
2493
|
+
tone={summary.apiAsyncCount > 0 || summary.apiSyncCount > 0 ? 'ok' : 'warn'}
|
|
2494
|
+
/>
|
|
2495
|
+
{summary.errorCount > 0 ? (
|
|
2496
|
+
<StatusToken
|
|
2497
|
+
label={`${summary.errorCount} error${summary.errorCount === 1 ? '' : 's'}`}
|
|
2498
|
+
onClick={() => profilerRuntime.openPanel('errors')}
|
|
2499
|
+
tone="error"
|
|
2500
|
+
/>
|
|
2501
|
+
) : null}
|
|
2502
|
+
<div className="proteum-profiler__spacer" />
|
|
2503
|
+
<button className="proteum-profiler__token" onClick={() => profilerRuntime.setUiState('pinned-handle')} type="button">
|
|
2504
|
+
Hide
|
|
2505
|
+
</button>
|
|
2506
|
+
</div>
|
|
2507
|
+
</>
|
|
2508
|
+
)}
|
|
2509
|
+
</div>
|
|
2510
|
+
);
|
|
2511
|
+
}
|