proteum 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +44 -98
- package/README.md +121 -7
- package/agents/framework/AGENTS.md +133 -886
- package/agents/project/AGENTS.md +70 -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/deploy/web.ts +1 -2
- package/cli/commands/dev.ts +96 -1
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -186
- 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/paths.ts +16 -1
- package/cli/presentation/commands.ts +59 -5
- package/cli/presentation/devSession.ts +5 -0
- package/cli/runtime/commands.ts +60 -1
- 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 +1511 -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 +91 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +109 -0
- package/common/env/proteumEnv.ts +284 -0
- package/common/router/index.ts +4 -22
- package/docs/dev-commands.md +86 -0
- package/docs/request-tracing.md +122 -0
- package/package.json +1 -2
- 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 +27 -4
- 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 +151 -0
- 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,1511 @@
|
|
|
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, TTraceSummaryValue } from '@common/dev/requestTrace';
|
|
19
|
+
|
|
20
|
+
import { profilerRuntime } from './runtime';
|
|
21
|
+
|
|
22
|
+
const profilerStyles = `
|
|
23
|
+
.proteum-profiler {
|
|
24
|
+
--profiler-bg: #000000;
|
|
25
|
+
--profiler-bg-strong: #000000;
|
|
26
|
+
--profiler-surface-hover: rgba(22, 33, 48, 0.32);
|
|
27
|
+
--profiler-line: rgba(155, 188, 214, 0.16);
|
|
28
|
+
--profiler-line-strong: rgba(155, 188, 214, 0.28);
|
|
29
|
+
--profiler-text: #e5f2ff;
|
|
30
|
+
--profiler-muted: rgba(213, 228, 242, 0.64);
|
|
31
|
+
--profiler-brand: #8fd9ff;
|
|
32
|
+
--profiler-ok: #7af4b4;
|
|
33
|
+
--profiler-warn: #ffd369;
|
|
34
|
+
--profiler-error: #ff9797;
|
|
35
|
+
position: fixed;
|
|
36
|
+
inset-inline: 0;
|
|
37
|
+
bottom: 0;
|
|
38
|
+
z-index: 2147483000;
|
|
39
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
40
|
+
color: var(--profiler-text);
|
|
41
|
+
letter-spacing: 0.01em;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.proteum-profiler__bar,
|
|
45
|
+
.proteum-profiler__panel,
|
|
46
|
+
.proteum-profiler__handle {
|
|
47
|
+
position: relative;
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.proteum-profiler__bar::before,
|
|
52
|
+
.proteum-profiler__panel::before,
|
|
53
|
+
.proteum-profiler__handle::before {
|
|
54
|
+
display: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.proteum-profiler__bar {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 0;
|
|
61
|
+
min-height: 32px;
|
|
62
|
+
padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px));
|
|
63
|
+
border-top: 1px solid var(--profiler-line-strong);
|
|
64
|
+
background: #000000;
|
|
65
|
+
backdrop-filter: none;
|
|
66
|
+
box-shadow: none;
|
|
67
|
+
overflow-x: auto;
|
|
68
|
+
scrollbar-width: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.proteum-profiler__bar::-webkit-scrollbar,
|
|
72
|
+
.proteum-profiler__panelTabs::-webkit-scrollbar {
|
|
73
|
+
display: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.proteum-profiler__token {
|
|
77
|
+
flex: 0 0 auto;
|
|
78
|
+
display: inline-flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 6px;
|
|
81
|
+
min-height: 20px;
|
|
82
|
+
padding: 0 10px;
|
|
83
|
+
border: none;
|
|
84
|
+
border-inline-start: 1px solid var(--profiler-line);
|
|
85
|
+
background: transparent;
|
|
86
|
+
color: var(--profiler-muted);
|
|
87
|
+
font-size: 11px;
|
|
88
|
+
line-height: 1;
|
|
89
|
+
letter-spacing: 0.06em;
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.proteum-profiler__token:first-child {
|
|
96
|
+
padding-inline-start: 0;
|
|
97
|
+
border-inline-start: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.proteum-profiler__token:hover {
|
|
101
|
+
color: var(--profiler-text);
|
|
102
|
+
background: var(--profiler-surface-hover);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.proteum-profiler__token--brand {
|
|
106
|
+
color: var(--profiler-brand);
|
|
107
|
+
font-weight: 700;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.proteum-profiler__token--ok {
|
|
111
|
+
color: var(--profiler-ok);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.proteum-profiler__token--warn {
|
|
115
|
+
color: var(--profiler-warn);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.proteum-profiler__token--error {
|
|
119
|
+
color: var(--profiler-error);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.proteum-profiler__spacer {
|
|
123
|
+
flex: 1 1 auto;
|
|
124
|
+
min-width: 16px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.proteum-profiler__handle {
|
|
128
|
+
position: fixed;
|
|
129
|
+
right: 10px;
|
|
130
|
+
bottom: calc(10px + env(safe-area-inset-bottom, 0px));
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 10px;
|
|
134
|
+
min-height: 30px;
|
|
135
|
+
padding: 0 12px;
|
|
136
|
+
border: 1px solid var(--profiler-line-strong);
|
|
137
|
+
border-radius: 0;
|
|
138
|
+
background: #000000;
|
|
139
|
+
backdrop-filter: none;
|
|
140
|
+
color: var(--profiler-brand);
|
|
141
|
+
box-shadow: none;
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
font-size: 11px;
|
|
144
|
+
letter-spacing: 0.08em;
|
|
145
|
+
text-transform: uppercase;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.proteum-profiler__panel {
|
|
149
|
+
position: fixed;
|
|
150
|
+
inset-inline: 0;
|
|
151
|
+
bottom: calc(32px + env(safe-area-inset-bottom, 0px));
|
|
152
|
+
display: grid;
|
|
153
|
+
grid-template-rows: auto 1fr;
|
|
154
|
+
height: 50vh;
|
|
155
|
+
max-height: 50vh;
|
|
156
|
+
margin: 0;
|
|
157
|
+
border: 1px solid var(--profiler-line-strong);
|
|
158
|
+
border-bottom: none;
|
|
159
|
+
border-left: none;
|
|
160
|
+
border-right: none;
|
|
161
|
+
border-radius: 0;
|
|
162
|
+
background: #000000;
|
|
163
|
+
backdrop-filter: none;
|
|
164
|
+
box-shadow: none;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.proteum-profiler__panelHeader {
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: flex-start;
|
|
172
|
+
gap: 12px;
|
|
173
|
+
padding: 12px 14px 10px;
|
|
174
|
+
border-bottom: 1px solid var(--profiler-line);
|
|
175
|
+
min-width: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.proteum-profiler__panelTabs {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 14px;
|
|
182
|
+
overflow: auto;
|
|
183
|
+
padding: 0;
|
|
184
|
+
border-bottom: none;
|
|
185
|
+
scrollbar-width: none;
|
|
186
|
+
flex: 1 1 auto;
|
|
187
|
+
min-width: 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.proteum-profiler__pill {
|
|
191
|
+
display: inline-flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: 6px;
|
|
194
|
+
min-height: 20px;
|
|
195
|
+
padding: 0;
|
|
196
|
+
border: none;
|
|
197
|
+
border-bottom: 1px solid transparent;
|
|
198
|
+
background: transparent;
|
|
199
|
+
font-size: 11px;
|
|
200
|
+
color: var(--profiler-muted);
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
white-space: nowrap;
|
|
203
|
+
letter-spacing: 0.05em;
|
|
204
|
+
text-transform: uppercase;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.proteum-profiler__pill:hover {
|
|
208
|
+
color: var(--profiler-text);
|
|
209
|
+
border-bottom-color: var(--profiler-line-strong);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.proteum-profiler__pill--active {
|
|
213
|
+
color: var(--profiler-brand);
|
|
214
|
+
border-bottom-color: var(--profiler-brand);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.proteum-profiler__pill:disabled {
|
|
218
|
+
opacity: 0.44;
|
|
219
|
+
cursor: default;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.proteum-profiler__select {
|
|
223
|
+
flex: 0 1 280px;
|
|
224
|
+
min-width: 160px;
|
|
225
|
+
height: 28px;
|
|
226
|
+
padding: 0 28px 0 10px;
|
|
227
|
+
border: 1px solid var(--profiler-line);
|
|
228
|
+
background-color: #000000;
|
|
229
|
+
background-image:
|
|
230
|
+
linear-gradient(45deg, transparent 50%, var(--profiler-muted) 50%),
|
|
231
|
+
linear-gradient(135deg, var(--profiler-muted) 50%, transparent 50%);
|
|
232
|
+
background-position: calc(100% - 14px) 11px, calc(100% - 9px) 11px;
|
|
233
|
+
background-repeat: no-repeat;
|
|
234
|
+
background-size: 5px 5px;
|
|
235
|
+
color: var(--profiler-text);
|
|
236
|
+
font: inherit;
|
|
237
|
+
font-size: 11px;
|
|
238
|
+
outline: none;
|
|
239
|
+
appearance: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.proteum-profiler__select option {
|
|
243
|
+
background: #000000;
|
|
244
|
+
color: var(--profiler-text);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.proteum-profiler__panelBody {
|
|
248
|
+
overflow: auto;
|
|
249
|
+
padding: 0 14px 16px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.proteum-profiler__metrics {
|
|
253
|
+
display: grid;
|
|
254
|
+
gap: 0;
|
|
255
|
+
padding-top: 10px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.proteum-profiler__metricRow {
|
|
259
|
+
display: grid;
|
|
260
|
+
grid-template-columns: minmax(104px, 140px) 1fr;
|
|
261
|
+
gap: 12px;
|
|
262
|
+
padding: 8px 0;
|
|
263
|
+
border-top: 1px solid var(--profiler-line);
|
|
264
|
+
align-items: start;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.proteum-profiler__metricRow:first-child {
|
|
268
|
+
border-top: none;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.proteum-profiler__metricLabel {
|
|
272
|
+
color: var(--profiler-muted);
|
|
273
|
+
font-size: 10px;
|
|
274
|
+
letter-spacing: 0.08em;
|
|
275
|
+
text-transform: uppercase;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.proteum-profiler__metricValue {
|
|
279
|
+
font-size: 12px;
|
|
280
|
+
line-height: 1.45;
|
|
281
|
+
word-break: break-word;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.proteum-profiler__section {
|
|
285
|
+
display: grid;
|
|
286
|
+
gap: 8px;
|
|
287
|
+
padding: 12px 0 0;
|
|
288
|
+
border-top: 1px solid var(--profiler-line);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.proteum-profiler__sectionHeader {
|
|
292
|
+
display: flex;
|
|
293
|
+
align-items: center;
|
|
294
|
+
justify-content: space-between;
|
|
295
|
+
gap: 12px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.proteum-profiler__actions {
|
|
299
|
+
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
gap: 10px;
|
|
302
|
+
flex-wrap: wrap;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.proteum-profiler__panelHeader .proteum-profiler__actions {
|
|
306
|
+
flex: 0 0 auto;
|
|
307
|
+
margin-left: auto;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.proteum-profiler__sectionTitle {
|
|
311
|
+
font-size: 11px;
|
|
312
|
+
font-weight: 700;
|
|
313
|
+
color: var(--profiler-brand);
|
|
314
|
+
letter-spacing: 0.08em;
|
|
315
|
+
text-transform: uppercase;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.proteum-profiler__list {
|
|
319
|
+
display: grid;
|
|
320
|
+
gap: 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.proteum-profiler__list > .proteum-profiler__row:first-child {
|
|
324
|
+
border-top: none;
|
|
325
|
+
padding-top: 2px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.proteum-profiler__row {
|
|
329
|
+
display: grid;
|
|
330
|
+
gap: 4px;
|
|
331
|
+
padding: 8px 0;
|
|
332
|
+
border-top: 1px solid var(--profiler-line);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.proteum-profiler__row--interactive {
|
|
336
|
+
width: 100%;
|
|
337
|
+
appearance: none;
|
|
338
|
+
background: transparent;
|
|
339
|
+
border-inline: none;
|
|
340
|
+
border-bottom: none;
|
|
341
|
+
border-radius: 0;
|
|
342
|
+
text-align: left;
|
|
343
|
+
color: inherit;
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.proteum-profiler__row--interactive:hover {
|
|
348
|
+
background: var(--profiler-surface-hover);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.proteum-profiler__rowHeader {
|
|
352
|
+
display: flex;
|
|
353
|
+
align-items: flex-start;
|
|
354
|
+
justify-content: space-between;
|
|
355
|
+
gap: 10px;
|
|
356
|
+
font-size: 11px;
|
|
357
|
+
line-height: 1.45;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.proteum-profiler__mono {
|
|
361
|
+
font-family: inherit;
|
|
362
|
+
font-size: 11px;
|
|
363
|
+
line-height: 1.5;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.proteum-profiler__muted {
|
|
367
|
+
color: var(--profiler-muted);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.proteum-profiler__pre {
|
|
371
|
+
margin: 0;
|
|
372
|
+
white-space: pre-wrap;
|
|
373
|
+
word-break: break-word;
|
|
374
|
+
padding-top: 8px;
|
|
375
|
+
border-top: 1px solid var(--profiler-line);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.proteum-profiler__detail {
|
|
379
|
+
display: grid;
|
|
380
|
+
gap: 10px;
|
|
381
|
+
padding: 10px 0 14px;
|
|
382
|
+
border-top: 1px dashed var(--profiler-line);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.proteum-profiler__detailLine {
|
|
386
|
+
display: grid;
|
|
387
|
+
gap: 4px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.proteum-profiler__detailLabel {
|
|
391
|
+
color: var(--profiler-muted);
|
|
392
|
+
font-size: 10px;
|
|
393
|
+
letter-spacing: 0.08em;
|
|
394
|
+
text-transform: uppercase;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.proteum-profiler__tags {
|
|
398
|
+
display: flex;
|
|
399
|
+
flex-wrap: wrap;
|
|
400
|
+
gap: 8px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.proteum-profiler__tag {
|
|
404
|
+
display: inline-flex;
|
|
405
|
+
align-items: center;
|
|
406
|
+
min-height: 0;
|
|
407
|
+
padding: 0;
|
|
408
|
+
font-size: 11px;
|
|
409
|
+
color: var(--profiler-muted);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.proteum-profiler__tag::before,
|
|
413
|
+
.proteum-profiler__tag::after {
|
|
414
|
+
color: var(--profiler-line-strong);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.proteum-profiler__tag::before {
|
|
418
|
+
content: '[';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.proteum-profiler__tag::after {
|
|
422
|
+
content: ']';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.proteum-profiler__empty {
|
|
426
|
+
padding: 12px 0;
|
|
427
|
+
border-top: 1px dashed var(--profiler-line);
|
|
428
|
+
color: var(--profiler-muted);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
@media (max-width: 900px) {
|
|
432
|
+
.proteum-profiler__panel {
|
|
433
|
+
height: 50vh;
|
|
434
|
+
max-height: 50vh;
|
|
435
|
+
margin: 0;
|
|
436
|
+
border-left: 0;
|
|
437
|
+
border-right: 0;
|
|
438
|
+
border-radius: 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.proteum-profiler__bar {
|
|
442
|
+
padding-inline: 8px;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.proteum-profiler__panelHeader,
|
|
446
|
+
.proteum-profiler__panelTabs,
|
|
447
|
+
.proteum-profiler__panelBody {
|
|
448
|
+
padding-inline: 10px;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.proteum-profiler__panelTabs {
|
|
452
|
+
gap: 10px;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.proteum-profiler__metricRow {
|
|
456
|
+
grid-template-columns: minmax(90px, 110px) 1fr;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.proteum-profiler__select {
|
|
460
|
+
min-width: 132px;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
`;
|
|
464
|
+
|
|
465
|
+
type TSessionSummary = {
|
|
466
|
+
apiAsyncCount: number;
|
|
467
|
+
apiSyncCount: number;
|
|
468
|
+
errorCount: number;
|
|
469
|
+
primaryTrace?: TProfilerSessionTrace;
|
|
470
|
+
renderMs?: number;
|
|
471
|
+
routeLabel: string;
|
|
472
|
+
ssrPayloadBytes?: number;
|
|
473
|
+
statusLabel: string;
|
|
474
|
+
totalMs?: number;
|
|
475
|
+
};
|
|
476
|
+
type TProfilerState = ReturnType<typeof profilerRuntime.getState>;
|
|
477
|
+
|
|
478
|
+
const panelLabels: Record<TProfilerPanel, string> = {
|
|
479
|
+
summary: 'Summary',
|
|
480
|
+
timeline: 'Timeline',
|
|
481
|
+
routing: 'Routing',
|
|
482
|
+
controller: 'Controller',
|
|
483
|
+
ssr: 'SSR',
|
|
484
|
+
api: 'API',
|
|
485
|
+
explain: 'Explain',
|
|
486
|
+
doctor: 'Doctor',
|
|
487
|
+
commands: 'Commands',
|
|
488
|
+
cron: 'Cron',
|
|
489
|
+
errors: 'Errors',
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const getSelectedSession = (sessions: TProfilerNavigationSession[], selectedSessionId?: string, currentSessionId?: string) =>
|
|
493
|
+
sessions.find((session) => session.id === selectedSessionId) ||
|
|
494
|
+
sessions.find((session) => session.id === currentSessionId) ||
|
|
495
|
+
sessions[sessions.length - 1];
|
|
496
|
+
|
|
497
|
+
const getSessionSelectorLabel = (session: TProfilerNavigationSession) => truncate(session.path || session.url || session.label, 56);
|
|
498
|
+
const truncate = (value: string, max = 96) => (value.length <= max ? value : `${value.slice(0, max)}...`);
|
|
499
|
+
const readNumber = (value: TTraceSummaryValue | undefined) => (typeof value === 'number' ? value : undefined);
|
|
500
|
+
const readString = (value: TTraceSummaryValue | undefined) => (typeof value === 'string' ? value : undefined);
|
|
501
|
+
const formatDuration = (value?: number) => (value === undefined ? 'pending' : `${Math.round(value)} ms`);
|
|
502
|
+
const formatBytes = (value?: number) => (value === undefined ? 'n/a' : `${(value / 1024).toFixed(value >= 1024 ? 1 : 2)} KB`);
|
|
503
|
+
const formatTimestamp = (value?: string) => {
|
|
504
|
+
if (!value) return 'never';
|
|
505
|
+
const date = new Date(value);
|
|
506
|
+
return Number.isNaN(date.valueOf()) ? value : date.toLocaleString();
|
|
507
|
+
};
|
|
508
|
+
const formatCronFrequency = (task: TProfilerCronTask) =>
|
|
509
|
+
task.frequency.kind === 'cron' ? task.frequency.value : `once at ${formatTimestamp(task.frequency.value)}`;
|
|
510
|
+
const formatStructuredValue = (value: unknown) => {
|
|
511
|
+
try {
|
|
512
|
+
return JSON.stringify(value, null, 2);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return String(value);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const renderSummaryValue = (value: TTraceSummaryValue | undefined): string => {
|
|
519
|
+
if (value === undefined) return '';
|
|
520
|
+
if (value === null) return 'null';
|
|
521
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
522
|
+
if (value.kind === 'undefined') return 'undefined';
|
|
523
|
+
if (value.kind === 'redacted') return `[redacted: ${value.reason}]`;
|
|
524
|
+
if (value.kind === 'error') return `${value.name}: ${value.message}`;
|
|
525
|
+
if (value.kind === 'array') return `Array(${value.length})`;
|
|
526
|
+
if (value.kind === 'object') return `${value.constructorName} { ${Object.keys(value.entries).join(', ')} }`;
|
|
527
|
+
if (value.kind === 'buffer') return `Buffer(${value.byteLength})`;
|
|
528
|
+
if ('value' in value) return String(value.value);
|
|
529
|
+
if ('size' in value) return String(value.size);
|
|
530
|
+
if ('name' in value) return value.name;
|
|
531
|
+
return JSON.stringify(value);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const toSummaryJsonValue = (value: TTraceSummaryValue | undefined): unknown => {
|
|
535
|
+
if (value === undefined) return 'undefined';
|
|
536
|
+
if (value === null) return null;
|
|
537
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
538
|
+
if (value.kind === 'undefined') return 'undefined';
|
|
539
|
+
if (value.kind === 'redacted') return `[redacted: ${value.reason}]`;
|
|
540
|
+
if (value.kind === 'bigint') return `${value.value}n`;
|
|
541
|
+
if (value.kind === 'symbol') return value.value;
|
|
542
|
+
if (value.kind === 'function') return `[Function ${value.name}]`;
|
|
543
|
+
if (value.kind === 'date') return value.value;
|
|
544
|
+
if (value.kind === 'error') return { name: value.name, message: value.message, stack: value.stack };
|
|
545
|
+
if (value.kind === 'buffer') return `[Buffer ${value.byteLength} bytes]`;
|
|
546
|
+
if (value.kind === 'map') return `[Map(${value.size})]`;
|
|
547
|
+
if (value.kind === 'set') return `[Set(${value.size})]`;
|
|
548
|
+
if (value.kind === 'array') {
|
|
549
|
+
const items = value.items.map((item) => toSummaryJsonValue(item));
|
|
550
|
+
if (value.truncated) items.push(`... ${Math.max(0, value.length - value.items.length)} more item(s)`);
|
|
551
|
+
return items;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const objectValue: Record<string, unknown> = {};
|
|
555
|
+
for (const [key, entry] of Object.entries(value.entries)) objectValue[key] = toSummaryJsonValue(entry);
|
|
556
|
+
if (value.truncated) objectValue.__truncated = `${Math.max(0, value.keys.length - Object.keys(value.entries).length)} more key(s)`;
|
|
557
|
+
return objectValue;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const formatSummaryJson = (value: TTraceSummaryValue | undefined) => {
|
|
561
|
+
if (value === undefined) return 'undefined';
|
|
562
|
+
if (typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'undefined') return 'undefined';
|
|
563
|
+
return JSON.stringify(toSummaryJsonValue(value), null, 2);
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const formatSummaryLiteral = (value: TTraceSummaryValue | undefined, depth = 1): string => {
|
|
567
|
+
if (value === undefined) return '';
|
|
568
|
+
if (value === null) return 'null';
|
|
569
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
570
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
571
|
+
if (depth <= 0) return renderSummaryValue(value);
|
|
572
|
+
if (value.kind === 'undefined') return 'undefined';
|
|
573
|
+
if (value.kind === 'redacted') return `[redacted: ${value.reason}]`;
|
|
574
|
+
if (value.kind === 'bigint') return `${value.value}n`;
|
|
575
|
+
if (value.kind === 'symbol') return value.value;
|
|
576
|
+
if (value.kind === 'function') return `[Function ${value.name}]`;
|
|
577
|
+
if (value.kind === 'date') return JSON.stringify(value.value);
|
|
578
|
+
if (value.kind === 'error') return `${value.name}(${JSON.stringify(value.message)})`;
|
|
579
|
+
if (value.kind === 'buffer') return `Buffer(${value.byteLength})`;
|
|
580
|
+
if (value.kind === 'map') return `Map(${value.size})`;
|
|
581
|
+
if (value.kind === 'set') return `Set(${value.size})`;
|
|
582
|
+
if (value.kind === 'array') {
|
|
583
|
+
const items = value.items.slice(0, 4).map((item) => formatSummaryLiteral(item, depth - 1));
|
|
584
|
+
if (value.truncated || value.length > items.length) items.push('...');
|
|
585
|
+
return `[${items.join(', ')}]`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const entries = Object.entries(value.entries)
|
|
589
|
+
.slice(0, 4)
|
|
590
|
+
.map(([key, entry]) => `${key}: ${formatSummaryLiteral(entry, depth - 1)}`);
|
|
591
|
+
if (value.truncated || value.keys.length > entries.length) entries.push('...');
|
|
592
|
+
return `{ ${entries.join(', ')} }`;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const getApiReferenceName = (method: string, path: string, fallbackLabel?: string) => {
|
|
596
|
+
if (path.startsWith('/api/')) return path.slice('/api/'.length).split('/').filter(Boolean).join('.');
|
|
597
|
+
|
|
598
|
+
const rawName = `${method} ${path}`.trim();
|
|
599
|
+
if (rawName) return rawName;
|
|
600
|
+
return fallbackLabel || 'request';
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const formatApiReference = (method: string, path: string, requestData?: TTraceSummaryValue, fallbackLabel?: string) => {
|
|
604
|
+
const args = formatSummaryLiteral(requestData, 1);
|
|
605
|
+
return `${getApiReferenceName(method, path, fallbackLabel)}(${truncate(args, 112)})`;
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const getTraceRequestData = (trace: TRequestTrace | undefined) =>
|
|
609
|
+
trace?.events.find((event) => event.type === 'request.start')?.details.data;
|
|
610
|
+
|
|
611
|
+
const getTraceResultData = (trace: TRequestTrace | undefined) =>
|
|
612
|
+
[...findTraceEvents(trace, ['controller.result'])]
|
|
613
|
+
.reverse()
|
|
614
|
+
.find((event) => event.details.kind === 'json' && event.details.data !== undefined)?.details.data;
|
|
615
|
+
|
|
616
|
+
const findTraceEvents = (trace: TRequestTrace | undefined, eventTypes: string[]) =>
|
|
617
|
+
trace?.events.filter((event) => eventTypes.includes(event.type)) || [];
|
|
618
|
+
|
|
619
|
+
const getSummary = (session: TProfilerNavigationSession): TSessionSummary => {
|
|
620
|
+
const primaryTrace =
|
|
621
|
+
session.traces.find((trace) => trace.kind === 'initial-root' && trace.trace) ||
|
|
622
|
+
session.traces.find((trace) => trace.kind === 'navigation-data' && trace.trace) ||
|
|
623
|
+
session.traces.find((trace) => trace.trace);
|
|
624
|
+
const trace = primaryTrace?.trace;
|
|
625
|
+
const syncCalls = session.traces.flatMap((traceItem) =>
|
|
626
|
+
traceItem.trace?.calls.filter((call) => call.origin === 'ssr-fetcher' || call.origin === 'api-batch-fetcher') || [],
|
|
627
|
+
);
|
|
628
|
+
const asyncCount = session.traces.filter((traceItem) => traceItem.kind === 'async').length;
|
|
629
|
+
const errorCount =
|
|
630
|
+
session.steps.filter((step) => step.status === 'error').length +
|
|
631
|
+
session.traces.filter((traceItem) => traceItem.status === 'error').length +
|
|
632
|
+
syncCalls.filter((call) => call.errorMessage || (call.statusCode !== undefined && call.statusCode >= 400)).length;
|
|
633
|
+
const renderStart = trace?.events.find((event) => event.type === 'render.start');
|
|
634
|
+
const renderEnd = trace?.events.find((event) => event.type === 'render.end');
|
|
635
|
+
const localRender = [...session.steps].reverse().find((step) => step.label === 'Render' && step.durationMs !== undefined);
|
|
636
|
+
const ssrPayload = trace?.events.find((event) => event.type === 'ssr.payload');
|
|
637
|
+
const routeLabel = session.routeLabel || readString(renderStart?.details.routeId) || session.path;
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
apiAsyncCount: asyncCount,
|
|
641
|
+
apiSyncCount: syncCalls.length,
|
|
642
|
+
errorCount,
|
|
643
|
+
primaryTrace,
|
|
644
|
+
renderMs:
|
|
645
|
+
renderStart && renderEnd
|
|
646
|
+
? Math.max(0, renderEnd.elapsedMs - renderStart.elapsedMs)
|
|
647
|
+
: localRender?.durationMs,
|
|
648
|
+
routeLabel,
|
|
649
|
+
ssrPayloadBytes: readNumber(ssrPayload?.details.serializedBytes),
|
|
650
|
+
statusLabel: session.kind === 'client-navigation' ? 'NAV' : trace ? `${trace.statusCode || 'pending'} ${trace.method}` : 'SSR',
|
|
651
|
+
totalMs: session.kind === 'client-navigation' ? session.durationMs : trace?.durationMs ?? session.durationMs,
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const StatusToken = ({ label, onClick, tone = 'ok' }: { label: string; onClick: () => void; tone?: 'ok' | 'warn' | 'error' }) => (
|
|
656
|
+
<button className={`proteum-profiler__token proteum-profiler__token--${tone}`} onClick={onClick} type="button">
|
|
657
|
+
{label}
|
|
658
|
+
</button>
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const SummaryRow = ({ label, value }: { label: string; value: React.ReactNode }) => (
|
|
662
|
+
<div className="proteum-profiler__metricRow">
|
|
663
|
+
<div className="proteum-profiler__metricLabel">{label}</div>
|
|
664
|
+
<div className="proteum-profiler__metricValue">{value}</div>
|
|
665
|
+
</div>
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
const ApiRequestEntry = ({
|
|
669
|
+
durationMs,
|
|
670
|
+
errorMessage,
|
|
671
|
+
finishedAt,
|
|
672
|
+
label,
|
|
673
|
+
method,
|
|
674
|
+
path,
|
|
675
|
+
requestData,
|
|
676
|
+
result,
|
|
677
|
+
startedAt,
|
|
678
|
+
statusCode,
|
|
679
|
+
statusLabel,
|
|
680
|
+
tags,
|
|
681
|
+
}: {
|
|
682
|
+
durationMs?: number;
|
|
683
|
+
errorMessage?: string;
|
|
684
|
+
finishedAt?: string;
|
|
685
|
+
label?: string;
|
|
686
|
+
method: string;
|
|
687
|
+
path: string;
|
|
688
|
+
requestData?: TTraceSummaryValue;
|
|
689
|
+
result?: TTraceSummaryValue;
|
|
690
|
+
startedAt: string;
|
|
691
|
+
statusCode?: number;
|
|
692
|
+
statusLabel?: string;
|
|
693
|
+
tags: string[];
|
|
694
|
+
}) => {
|
|
695
|
+
const [isOpen, setOpen] = React.useState(false);
|
|
696
|
+
const statusText = statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
|
|
697
|
+
|
|
698
|
+
return (
|
|
699
|
+
<>
|
|
700
|
+
<button className="proteum-profiler__row proteum-profiler__row--interactive" onClick={() => setOpen((current) => !current)} type="button">
|
|
701
|
+
<div className="proteum-profiler__rowHeader">
|
|
702
|
+
<strong>{formatApiReference(method, path, requestData, label)}</strong>
|
|
703
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
704
|
+
{formatDuration(durationMs)} | {statusText}
|
|
705
|
+
</span>
|
|
706
|
+
</div>
|
|
707
|
+
{method || path ? <div className="proteum-profiler__mono proteum-profiler__muted">{method} {path}</div> : null}
|
|
708
|
+
<div className="proteum-profiler__tags">
|
|
709
|
+
{tags.map((tag) => (
|
|
710
|
+
<span className="proteum-profiler__tag" key={`${label || method}:${path}:${tag}`}>
|
|
711
|
+
{tag}
|
|
712
|
+
</span>
|
|
713
|
+
))}
|
|
714
|
+
{errorMessage ? <span className="proteum-profiler__tag">{truncate(errorMessage, 72)}</span> : null}
|
|
715
|
+
</div>
|
|
716
|
+
</button>
|
|
717
|
+
|
|
718
|
+
{isOpen ? (
|
|
719
|
+
<div className="proteum-profiler__detail">
|
|
720
|
+
<div className="proteum-profiler__detailLine">
|
|
721
|
+
<div className="proteum-profiler__detailLabel">Performance</div>
|
|
722
|
+
<div className="proteum-profiler__mono">
|
|
723
|
+
duration={formatDuration(durationMs)} | status={statusText} | started={formatTimestamp(startedAt)}
|
|
724
|
+
{finishedAt ? ` | finished=${formatTimestamp(finishedAt)}` : ''}
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
<div className="proteum-profiler__detailLine">
|
|
728
|
+
<div className="proteum-profiler__detailLabel">Arguments</div>
|
|
729
|
+
<pre className="proteum-profiler__mono proteum-profiler__pre">{formatSummaryJson(requestData)}</pre>
|
|
730
|
+
</div>
|
|
731
|
+
<div className="proteum-profiler__detailLine">
|
|
732
|
+
<div className="proteum-profiler__detailLabel">Result</div>
|
|
733
|
+
<pre className="proteum-profiler__mono proteum-profiler__pre">{formatSummaryJson(result)}</pre>
|
|
734
|
+
</div>
|
|
735
|
+
{errorMessage ? (
|
|
736
|
+
<div className="proteum-profiler__detailLine">
|
|
737
|
+
<div className="proteum-profiler__detailLabel">Error</div>
|
|
738
|
+
<div className="proteum-profiler__mono">{errorMessage}</div>
|
|
739
|
+
</div>
|
|
740
|
+
) : null}
|
|
741
|
+
</div>
|
|
742
|
+
) : null}
|
|
743
|
+
</>
|
|
744
|
+
);
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const TraceRows = ({ trace }: { trace: TRequestTrace }) => (
|
|
748
|
+
<div className="proteum-profiler__section">
|
|
749
|
+
<div className="proteum-profiler__sectionHeader">
|
|
750
|
+
<div className="proteum-profiler__sectionTitle">
|
|
751
|
+
{trace.method} {trace.path}
|
|
752
|
+
</div>
|
|
753
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">{trace.id}</div>
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
{trace.calls.length > 0 && (
|
|
757
|
+
<div className="proteum-profiler__list">
|
|
758
|
+
{trace.calls.map((call) => (
|
|
759
|
+
<div className="proteum-profiler__row" key={call.id}>
|
|
760
|
+
<div className="proteum-profiler__rowHeader">
|
|
761
|
+
<strong>
|
|
762
|
+
{call.label} {call.method ? `(${call.method} ${call.path})` : ''}
|
|
763
|
+
</strong>
|
|
764
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
765
|
+
{formatDuration(call.durationMs)}
|
|
766
|
+
{call.statusCode !== undefined ? ` | ${call.statusCode}` : ''}
|
|
767
|
+
</span>
|
|
768
|
+
</div>
|
|
769
|
+
<div className="proteum-profiler__tags">
|
|
770
|
+
<span className="proteum-profiler__tag">{call.origin}</span>
|
|
771
|
+
{call.fetcherId ? <span className="proteum-profiler__tag">fetcher:{call.fetcherId}</span> : null}
|
|
772
|
+
{call.requestDataKeys.map((key) => (
|
|
773
|
+
<span className="proteum-profiler__tag" key={`${call.id}:req:${key}`}>
|
|
774
|
+
req:{key}
|
|
775
|
+
</span>
|
|
776
|
+
))}
|
|
777
|
+
{call.resultKeys.map((key) => (
|
|
778
|
+
<span className="proteum-profiler__tag" key={`${call.id}:res:${key}`}>
|
|
779
|
+
res:{key}
|
|
780
|
+
</span>
|
|
781
|
+
))}
|
|
782
|
+
{call.errorMessage ? <span className="proteum-profiler__tag">{truncate(call.errorMessage, 72)}</span> : null}
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
))}
|
|
786
|
+
</div>
|
|
787
|
+
)}
|
|
788
|
+
|
|
789
|
+
<div className="proteum-profiler__list">
|
|
790
|
+
{trace.events.map((event) => (
|
|
791
|
+
<div className="proteum-profiler__row" key={`${trace.id}:${event.index}`}>
|
|
792
|
+
<div className="proteum-profiler__rowHeader">
|
|
793
|
+
<strong>{event.type}</strong>
|
|
794
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(event.elapsedMs)}</span>
|
|
795
|
+
</div>
|
|
796
|
+
<div className="proteum-profiler__tags">
|
|
797
|
+
{Object.entries(event.details).map(([key, value]) => (
|
|
798
|
+
<span className="proteum-profiler__tag" key={`${trace.id}:${event.index}:${key}`}>
|
|
799
|
+
{key}:{truncate(renderSummaryValue(value), 72)}
|
|
800
|
+
</span>
|
|
801
|
+
))}
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
))}
|
|
805
|
+
</div>
|
|
806
|
+
</div>
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
const SimpleSection = ({ empty, rows, title }: { empty: string; rows: Array<{ key: string; title: string; value: string }>; title: string }) => (
|
|
810
|
+
<div className="proteum-profiler__section">
|
|
811
|
+
<div className="proteum-profiler__sectionTitle">{title}</div>
|
|
812
|
+
{rows.length === 0 ? (
|
|
813
|
+
<div className="proteum-profiler__empty">{empty}</div>
|
|
814
|
+
) : (
|
|
815
|
+
<div className="proteum-profiler__list">
|
|
816
|
+
{rows.map((row) => (
|
|
817
|
+
<div className="proteum-profiler__row" key={row.key}>
|
|
818
|
+
<div className="proteum-profiler__rowHeader">
|
|
819
|
+
<strong>{row.title}</strong>
|
|
820
|
+
</div>
|
|
821
|
+
<div className="proteum-profiler__mono">{row.value}</div>
|
|
822
|
+
</div>
|
|
823
|
+
))}
|
|
824
|
+
</div>
|
|
825
|
+
)}
|
|
826
|
+
</div>
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
const TextBlocks = ({ blocks }: { blocks: THumanTextBlock[] }) => (
|
|
830
|
+
<>
|
|
831
|
+
{blocks.map((block) => (
|
|
832
|
+
<div className="proteum-profiler__section" key={block.title}>
|
|
833
|
+
<div className="proteum-profiler__sectionTitle">{block.title}</div>
|
|
834
|
+
{block.items.length === 0 ? (
|
|
835
|
+
<div className="proteum-profiler__empty">{block.empty || 'none'}</div>
|
|
836
|
+
) : (
|
|
837
|
+
<div className="proteum-profiler__list">
|
|
838
|
+
{block.items.map((item, index) => (
|
|
839
|
+
<div className="proteum-profiler__row" key={`${block.title}:${index}`}>
|
|
840
|
+
<div className="proteum-profiler__mono">{item}</div>
|
|
841
|
+
</div>
|
|
842
|
+
))}
|
|
843
|
+
</div>
|
|
844
|
+
)}
|
|
845
|
+
</div>
|
|
846
|
+
))}
|
|
847
|
+
</>
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession, summary: TSessionSummary, state: TProfilerState) => {
|
|
851
|
+
const primaryTrace = summary.primaryTrace?.trace;
|
|
852
|
+
|
|
853
|
+
if (panel === 'summary') {
|
|
854
|
+
return (
|
|
855
|
+
<div className="proteum-profiler__metrics">
|
|
856
|
+
<SummaryRow label="Session" value={session.label} />
|
|
857
|
+
<SummaryRow label="Status" value={summary.statusLabel} />
|
|
858
|
+
<SummaryRow label="Duration" value={formatDuration(summary.totalMs)} />
|
|
859
|
+
<SummaryRow label="Route" value={summary.routeLabel} />
|
|
860
|
+
<SummaryRow
|
|
861
|
+
label="SSR"
|
|
862
|
+
value={
|
|
863
|
+
summary.ssrPayloadBytes !== undefined
|
|
864
|
+
? `${formatDuration(summary.renderMs)} | ${formatBytes(summary.ssrPayloadBytes)}`
|
|
865
|
+
: formatDuration(summary.renderMs)
|
|
866
|
+
}
|
|
867
|
+
/>
|
|
868
|
+
<SummaryRow label="API" value={`sync ${summary.apiSyncCount} / async ${summary.apiAsyncCount}`} />
|
|
869
|
+
<SummaryRow label="Errors" value={String(summary.errorCount)} />
|
|
870
|
+
<SummaryRow label="Request" value={session.requestId || 'client-only'} />
|
|
871
|
+
</div>
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (panel === 'timeline') {
|
|
876
|
+
return (
|
|
877
|
+
<div className="proteum-profiler__section">
|
|
878
|
+
<div className="proteum-profiler__sectionTitle">Navigation steps</div>
|
|
879
|
+
<div className="proteum-profiler__list">
|
|
880
|
+
{session.steps.map((step) => (
|
|
881
|
+
<div className="proteum-profiler__row" key={step.id}>
|
|
882
|
+
<div className="proteum-profiler__rowHeader">
|
|
883
|
+
<strong>{step.label}</strong>
|
|
884
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
|
|
885
|
+
</div>
|
|
886
|
+
<div className="proteum-profiler__tags">
|
|
887
|
+
<span className="proteum-profiler__tag">{step.status}</span>
|
|
888
|
+
{Object.entries(step.details || {}).map(([key, value]) => (
|
|
889
|
+
<span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
|
|
890
|
+
{key}:{String(value)}
|
|
891
|
+
</span>
|
|
892
|
+
))}
|
|
893
|
+
{step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
))}
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
{session.traces.map((trace) =>
|
|
900
|
+
trace.trace ? (
|
|
901
|
+
<TraceRows key={trace.id} trace={trace.trace} />
|
|
902
|
+
) : (
|
|
903
|
+
<div className="proteum-profiler__row" key={trace.id}>
|
|
904
|
+
<div className="proteum-profiler__rowHeader">
|
|
905
|
+
<strong>{trace.label}</strong>
|
|
906
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{trace.status}</span>
|
|
907
|
+
</div>
|
|
908
|
+
<div className="proteum-profiler__mono">{trace.method} {trace.path}</div>
|
|
909
|
+
</div>
|
|
910
|
+
),
|
|
911
|
+
)}
|
|
912
|
+
</div>
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (panel === 'routing') {
|
|
917
|
+
return (
|
|
918
|
+
<SimpleSection
|
|
919
|
+
empty="No routing data captured yet."
|
|
920
|
+
rows={findTraceEvents(primaryTrace, [
|
|
921
|
+
'resolve.start',
|
|
922
|
+
'resolve.controller-route',
|
|
923
|
+
'resolve.route-match',
|
|
924
|
+
'resolve.routes-evaluated',
|
|
925
|
+
'resolve.not-found',
|
|
926
|
+
]).map((event) => ({
|
|
927
|
+
key: `${event.index}:${event.type}`,
|
|
928
|
+
title: event.type,
|
|
929
|
+
value: Object.entries(event.details)
|
|
930
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
931
|
+
.join(' '),
|
|
932
|
+
}))}
|
|
933
|
+
title="Routing"
|
|
934
|
+
/>
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (panel === 'controller') {
|
|
939
|
+
return (
|
|
940
|
+
<SimpleSection
|
|
941
|
+
empty="No controller data captured yet."
|
|
942
|
+
rows={findTraceEvents(primaryTrace, ['controller.start', 'controller.result', 'setup.options', 'context.create']).map(
|
|
943
|
+
(event) => ({
|
|
944
|
+
key: `${event.index}:${event.type}`,
|
|
945
|
+
title: event.type,
|
|
946
|
+
value: Object.entries(event.details)
|
|
947
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
948
|
+
.join(' '),
|
|
949
|
+
}),
|
|
950
|
+
)}
|
|
951
|
+
title="Controller"
|
|
952
|
+
/>
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (panel === 'ssr') {
|
|
957
|
+
return (
|
|
958
|
+
<SimpleSection
|
|
959
|
+
empty="No SSR data captured for this session."
|
|
960
|
+
rows={findTraceEvents(primaryTrace, ['page.data', 'ssr.payload', 'render.start', 'render.end']).map((event) => ({
|
|
961
|
+
key: `${event.index}:${event.type}`,
|
|
962
|
+
title: event.type,
|
|
963
|
+
value: Object.entries(event.details)
|
|
964
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
965
|
+
.join(' '),
|
|
966
|
+
}))}
|
|
967
|
+
title="SSR"
|
|
968
|
+
/>
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (panel === 'api') {
|
|
973
|
+
const syncCalls = session.traces.flatMap((trace) =>
|
|
974
|
+
trace.trace?.calls.filter((call) => call.origin !== 'client-async') || [],
|
|
975
|
+
);
|
|
976
|
+
const asyncTraces = session.traces.filter((trace) => trace.kind === 'async');
|
|
977
|
+
|
|
978
|
+
return (
|
|
979
|
+
<div className="proteum-profiler__section">
|
|
980
|
+
<div className="proteum-profiler__sectionTitle">Synchronous calls</div>
|
|
981
|
+
{syncCalls.length === 0 ? (
|
|
982
|
+
<div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
|
|
983
|
+
) : (
|
|
984
|
+
<div className="proteum-profiler__list">
|
|
985
|
+
{syncCalls.map((call: TTraceCall) => (
|
|
986
|
+
<ApiRequestEntry
|
|
987
|
+
durationMs={call.durationMs}
|
|
988
|
+
errorMessage={call.errorMessage}
|
|
989
|
+
finishedAt={call.finishedAt}
|
|
990
|
+
key={call.id}
|
|
991
|
+
label={call.label}
|
|
992
|
+
method={call.method}
|
|
993
|
+
path={call.path}
|
|
994
|
+
requestData={call.requestData}
|
|
995
|
+
result={call.result}
|
|
996
|
+
startedAt={call.startedAt}
|
|
997
|
+
statusCode={call.statusCode}
|
|
998
|
+
tags={[
|
|
999
|
+
call.origin,
|
|
1000
|
+
...(call.fetcherId ? [`fetcher:${call.fetcherId}`] : []),
|
|
1001
|
+
...call.requestDataKeys.map((key) => `arg:${key}`),
|
|
1002
|
+
...call.resultKeys.map((key) => `res:${key}`),
|
|
1003
|
+
]}
|
|
1004
|
+
/>
|
|
1005
|
+
))}
|
|
1006
|
+
</div>
|
|
1007
|
+
)}
|
|
1008
|
+
|
|
1009
|
+
<div className="proteum-profiler__sectionTitle">Async requests</div>
|
|
1010
|
+
{asyncTraces.length === 0 ? (
|
|
1011
|
+
<div className="proteum-profiler__empty">No async API calls captured.</div>
|
|
1012
|
+
) : (
|
|
1013
|
+
<div className="proteum-profiler__list">
|
|
1014
|
+
{asyncTraces.map((trace) => (
|
|
1015
|
+
<ApiRequestEntry
|
|
1016
|
+
durationMs={trace.durationMs}
|
|
1017
|
+
errorMessage={trace.errorMessage || trace.trace?.errorMessage}
|
|
1018
|
+
finishedAt={trace.finishedAt}
|
|
1019
|
+
key={trace.id}
|
|
1020
|
+
label={trace.label}
|
|
1021
|
+
method={trace.method}
|
|
1022
|
+
path={trace.path}
|
|
1023
|
+
requestData={getTraceRequestData(trace.trace)}
|
|
1024
|
+
result={getTraceResultData(trace.trace)}
|
|
1025
|
+
startedAt={trace.startedAt}
|
|
1026
|
+
statusCode={trace.trace?.statusCode}
|
|
1027
|
+
statusLabel={trace.status}
|
|
1028
|
+
tags={[trace.status, ...(trace.requestId ? [`request:${trace.requestId}`] : [])]}
|
|
1029
|
+
/>
|
|
1030
|
+
))}
|
|
1031
|
+
</div>
|
|
1032
|
+
)}
|
|
1033
|
+
</div>
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (panel === 'explain') {
|
|
1038
|
+
const explain = state.explain;
|
|
1039
|
+
const blocks = explain.manifest
|
|
1040
|
+
? [
|
|
1041
|
+
{ title: 'Overview', items: buildExplainSummaryItems(explain.manifest) },
|
|
1042
|
+
...buildExplainBlocks(explain.manifest, [...explainSectionNames]),
|
|
1043
|
+
]
|
|
1044
|
+
: [];
|
|
1045
|
+
|
|
1046
|
+
return (
|
|
1047
|
+
<div className="proteum-profiler__section">
|
|
1048
|
+
<div className="proteum-profiler__sectionHeader">
|
|
1049
|
+
<div className="proteum-profiler__sectionTitle">Explain</div>
|
|
1050
|
+
<div className="proteum-profiler__actions">
|
|
1051
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshExplain()} type="button">
|
|
1052
|
+
Refresh
|
|
1053
|
+
</button>
|
|
1054
|
+
</div>
|
|
1055
|
+
</div>
|
|
1056
|
+
|
|
1057
|
+
{explain.errorMessage ? (
|
|
1058
|
+
<div className="proteum-profiler__row">
|
|
1059
|
+
<div className="proteum-profiler__rowHeader">
|
|
1060
|
+
<strong>Last explain panel error</strong>
|
|
1061
|
+
</div>
|
|
1062
|
+
<div className="proteum-profiler__mono">{explain.errorMessage}</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
) : null}
|
|
1065
|
+
|
|
1066
|
+
{explain.status === 'loading' && !explain.manifest ? (
|
|
1067
|
+
<div className="proteum-profiler__empty">Loading explain data...</div>
|
|
1068
|
+
) : !explain.manifest ? (
|
|
1069
|
+
<div className="proteum-profiler__empty">No explain manifest is available.</div>
|
|
1070
|
+
) : (
|
|
1071
|
+
<>
|
|
1072
|
+
<div className="proteum-profiler__row">
|
|
1073
|
+
<div className="proteum-profiler__rowHeader">
|
|
1074
|
+
<strong>Manifest snapshot</strong>
|
|
1075
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
1076
|
+
{explain.lastLoadedAt ? formatTimestamp(explain.lastLoadedAt) : 'Not loaded'}
|
|
1077
|
+
</span>
|
|
1078
|
+
</div>
|
|
1079
|
+
<div className="proteum-profiler__mono">
|
|
1080
|
+
Same manifest-backed sections as `proteum explain`, rendered from the shared diagnostics contract.
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
<TextBlocks blocks={blocks} />
|
|
1084
|
+
</>
|
|
1085
|
+
)}
|
|
1086
|
+
</div>
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (panel === 'doctor') {
|
|
1091
|
+
const doctor = state.doctor;
|
|
1092
|
+
const doctorRows =
|
|
1093
|
+
doctor.response?.diagnostics.map((diagnostic, index) => ({
|
|
1094
|
+
key: `${diagnostic.code}:${index}`,
|
|
1095
|
+
title: `[${diagnostic.level}] ${diagnostic.code}`,
|
|
1096
|
+
value: `${diagnostic.message} source=${diagnostic.filepath}${formatManifestLocation(
|
|
1097
|
+
diagnostic.sourceLocation?.line,
|
|
1098
|
+
diagnostic.sourceLocation?.column,
|
|
1099
|
+
)}${diagnostic.relatedFilepaths?.length ? ` related=${diagnostic.relatedFilepaths.join(',')}` : ''}`,
|
|
1100
|
+
})) || [];
|
|
1101
|
+
const doctorBlocks = state.explain.manifest ? buildDoctorBlocks(state.explain.manifest) : [];
|
|
1102
|
+
|
|
1103
|
+
return (
|
|
1104
|
+
<div className="proteum-profiler__section">
|
|
1105
|
+
<div className="proteum-profiler__sectionHeader">
|
|
1106
|
+
<div className="proteum-profiler__sectionTitle">Doctor</div>
|
|
1107
|
+
<div className="proteum-profiler__actions">
|
|
1108
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshDoctor()} type="button">
|
|
1109
|
+
Refresh
|
|
1110
|
+
</button>
|
|
1111
|
+
</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
|
|
1114
|
+
{doctor.errorMessage ? (
|
|
1115
|
+
<div className="proteum-profiler__row">
|
|
1116
|
+
<div className="proteum-profiler__rowHeader">
|
|
1117
|
+
<strong>Last doctor panel error</strong>
|
|
1118
|
+
</div>
|
|
1119
|
+
<div className="proteum-profiler__mono">{doctor.errorMessage}</div>
|
|
1120
|
+
</div>
|
|
1121
|
+
) : null}
|
|
1122
|
+
|
|
1123
|
+
{doctor.status === 'loading' && !doctor.response ? (
|
|
1124
|
+
<div className="proteum-profiler__empty">Loading doctor diagnostics...</div>
|
|
1125
|
+
) : !doctor.response ? (
|
|
1126
|
+
<div className="proteum-profiler__empty">No doctor diagnostics are available.</div>
|
|
1127
|
+
) : (
|
|
1128
|
+
<>
|
|
1129
|
+
<div className="proteum-profiler__metrics">
|
|
1130
|
+
<SummaryRow label="Errors" value={String(doctor.response.summary.errors)} />
|
|
1131
|
+
<SummaryRow label="Warnings" value={String(doctor.response.summary.warnings)} />
|
|
1132
|
+
<SummaryRow label="Strict" value={doctor.response.summary.strictFailed ? 'failed' : 'ok'} />
|
|
1133
|
+
<SummaryRow
|
|
1134
|
+
label="Refreshed"
|
|
1135
|
+
value={doctor.lastLoadedAt ? formatTimestamp(doctor.lastLoadedAt) : 'Not loaded'}
|
|
1136
|
+
/>
|
|
1137
|
+
</div>
|
|
1138
|
+
{doctorBlocks.length > 0 ? (
|
|
1139
|
+
<TextBlocks blocks={doctorBlocks} />
|
|
1140
|
+
) : (
|
|
1141
|
+
<SimpleSection empty="No manifest diagnostics were found." rows={doctorRows} title="Diagnostics" />
|
|
1142
|
+
)}
|
|
1143
|
+
</>
|
|
1144
|
+
)}
|
|
1145
|
+
</div>
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (panel === 'commands') {
|
|
1150
|
+
const commandsState = state.commands;
|
|
1151
|
+
|
|
1152
|
+
return (
|
|
1153
|
+
<div className="proteum-profiler__section">
|
|
1154
|
+
<div className="proteum-profiler__sectionHeader">
|
|
1155
|
+
<div className="proteum-profiler__sectionTitle">Available commands</div>
|
|
1156
|
+
<div className="proteum-profiler__actions">
|
|
1157
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshCommands()} type="button">
|
|
1158
|
+
Refresh
|
|
1159
|
+
</button>
|
|
1160
|
+
</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
|
|
1163
|
+
<div className="proteum-profiler__row">
|
|
1164
|
+
<div className="proteum-profiler__rowHeader">
|
|
1165
|
+
<strong>Dev commands</strong>
|
|
1166
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
1167
|
+
{commandsState.commands.length} command{commandsState.commands.length === 1 ? '' : 's'}
|
|
1168
|
+
</span>
|
|
1169
|
+
</div>
|
|
1170
|
+
<div className="proteum-profiler__mono">
|
|
1171
|
+
Commands live under /commands, extend the Proteum Commands class, and run only in a dev context.
|
|
1172
|
+
</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
|
|
1175
|
+
{commandsState.errorMessage ? (
|
|
1176
|
+
<div className="proteum-profiler__row">
|
|
1177
|
+
<div className="proteum-profiler__rowHeader">
|
|
1178
|
+
<strong>Last command panel error</strong>
|
|
1179
|
+
</div>
|
|
1180
|
+
<div className="proteum-profiler__mono">{commandsState.errorMessage}</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
) : null}
|
|
1183
|
+
|
|
1184
|
+
{commandsState.status === 'loading' && commandsState.commands.length === 0 ? (
|
|
1185
|
+
<div className="proteum-profiler__empty">Loading commands...</div>
|
|
1186
|
+
) : commandsState.commands.length === 0 ? (
|
|
1187
|
+
<div className="proteum-profiler__empty">No commands are registered for this app.</div>
|
|
1188
|
+
) : (
|
|
1189
|
+
<div className="proteum-profiler__list">
|
|
1190
|
+
{commandsState.commands.map((command: TDevCommandDefinition) => {
|
|
1191
|
+
const execution = commandsState.executions[command.path] as TDevCommandExecution | undefined;
|
|
1192
|
+
return (
|
|
1193
|
+
<div className="proteum-profiler__row" key={command.path}>
|
|
1194
|
+
<div className="proteum-profiler__rowHeader">
|
|
1195
|
+
<strong>{command.path}</strong>
|
|
1196
|
+
<div className="proteum-profiler__actions">
|
|
1197
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
1198
|
+
{execution ? formatTimestamp(execution.finishedAt) : 'Never run'}
|
|
1199
|
+
</span>
|
|
1200
|
+
<button
|
|
1201
|
+
className="proteum-profiler__pill"
|
|
1202
|
+
onClick={() => void profilerRuntime.runCommand(command.path)}
|
|
1203
|
+
type="button"
|
|
1204
|
+
>
|
|
1205
|
+
Run now
|
|
1206
|
+
</button>
|
|
1207
|
+
</div>
|
|
1208
|
+
</div>
|
|
1209
|
+
|
|
1210
|
+
<div className="proteum-profiler__tags">
|
|
1211
|
+
<span className="proteum-profiler__tag">{command.className}</span>
|
|
1212
|
+
<span className="proteum-profiler__tag">{command.methodName}</span>
|
|
1213
|
+
<span className="proteum-profiler__tag">{command.scope}</span>
|
|
1214
|
+
{execution ? <span className="proteum-profiler__tag">{execution.status}</span> : null}
|
|
1215
|
+
{execution ? (
|
|
1216
|
+
<span className="proteum-profiler__tag">{formatDuration(execution.durationMs)}</span>
|
|
1217
|
+
) : null}
|
|
1218
|
+
{execution?.errorMessage ? (
|
|
1219
|
+
<span className="proteum-profiler__tag">{truncate(execution.errorMessage, 72)}</span>
|
|
1220
|
+
) : null}
|
|
1221
|
+
</div>
|
|
1222
|
+
|
|
1223
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">
|
|
1224
|
+
source {command.filepath}:{command.sourceLocation.line}:{command.sourceLocation.column}
|
|
1225
|
+
{commandsState.lastLoadedAt
|
|
1226
|
+
? ` | refreshed ${formatTimestamp(commandsState.lastLoadedAt)}`
|
|
1227
|
+
: ''}
|
|
1228
|
+
</div>
|
|
1229
|
+
|
|
1230
|
+
{execution ? (
|
|
1231
|
+
<div className="proteum-profiler__section">
|
|
1232
|
+
<div className="proteum-profiler__sectionTitle">Last result</div>
|
|
1233
|
+
<pre className="proteum-profiler__mono proteum-profiler__pre">
|
|
1234
|
+
{execution.result?.json !== undefined
|
|
1235
|
+
? formatStructuredValue(execution.result.json)
|
|
1236
|
+
: execution.result
|
|
1237
|
+
? formatStructuredValue(execution.result.summary)
|
|
1238
|
+
: execution.errorMessage || 'undefined'}
|
|
1239
|
+
</pre>
|
|
1240
|
+
</div>
|
|
1241
|
+
) : null}
|
|
1242
|
+
</div>
|
|
1243
|
+
);
|
|
1244
|
+
})}
|
|
1245
|
+
</div>
|
|
1246
|
+
)}
|
|
1247
|
+
</div>
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (panel === 'cron') {
|
|
1252
|
+
const cron = state.cron;
|
|
1253
|
+
|
|
1254
|
+
return (
|
|
1255
|
+
<div className="proteum-profiler__section">
|
|
1256
|
+
<div className="proteum-profiler__sectionHeader">
|
|
1257
|
+
<div className="proteum-profiler__sectionTitle">Registered tasks</div>
|
|
1258
|
+
<div className="proteum-profiler__actions">
|
|
1259
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshCronTasks()} type="button">
|
|
1260
|
+
Refresh
|
|
1261
|
+
</button>
|
|
1262
|
+
</div>
|
|
1263
|
+
</div>
|
|
1264
|
+
|
|
1265
|
+
<div className="proteum-profiler__row">
|
|
1266
|
+
<div className="proteum-profiler__rowHeader">
|
|
1267
|
+
<strong>Dev mode</strong>
|
|
1268
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
1269
|
+
{cron.tasks.length} task{cron.tasks.length === 1 ? '' : 's'}
|
|
1270
|
+
</span>
|
|
1271
|
+
</div>
|
|
1272
|
+
<div className="proteum-profiler__mono">
|
|
1273
|
+
Automatic execution is disabled in dev. Registered cron tasks stay visible here and only run when
|
|
1274
|
+
triggered manually.
|
|
1275
|
+
</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
|
|
1278
|
+
{cron.errorMessage ? (
|
|
1279
|
+
<div className="proteum-profiler__row">
|
|
1280
|
+
<div className="proteum-profiler__rowHeader">
|
|
1281
|
+
<strong>Last cron panel error</strong>
|
|
1282
|
+
</div>
|
|
1283
|
+
<div className="proteum-profiler__mono">{cron.errorMessage}</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
) : null}
|
|
1286
|
+
|
|
1287
|
+
{cron.status === 'loading' && cron.tasks.length === 0 ? (
|
|
1288
|
+
<div className="proteum-profiler__empty">Loading cron tasks...</div>
|
|
1289
|
+
) : cron.tasks.length === 0 ? (
|
|
1290
|
+
<div className="proteum-profiler__empty">No cron tasks are registered for this app.</div>
|
|
1291
|
+
) : (
|
|
1292
|
+
<div className="proteum-profiler__list">
|
|
1293
|
+
{cron.tasks.map((task) => (
|
|
1294
|
+
<div className="proteum-profiler__row" key={task.name}>
|
|
1295
|
+
<div className="proteum-profiler__rowHeader">
|
|
1296
|
+
<strong>{task.name}</strong>
|
|
1297
|
+
<div className="proteum-profiler__actions">
|
|
1298
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
1299
|
+
{task.running
|
|
1300
|
+
? 'Running...'
|
|
1301
|
+
: task.lastRunFinishedAt
|
|
1302
|
+
? formatTimestamp(task.lastRunFinishedAt)
|
|
1303
|
+
: 'Never run'}
|
|
1304
|
+
</span>
|
|
1305
|
+
<button
|
|
1306
|
+
className="proteum-profiler__pill"
|
|
1307
|
+
disabled={task.running}
|
|
1308
|
+
onClick={() => void profilerRuntime.runCronTask(task.name)}
|
|
1309
|
+
type="button"
|
|
1310
|
+
>
|
|
1311
|
+
{task.running ? 'Running...' : 'Run now'}
|
|
1312
|
+
</button>
|
|
1313
|
+
</div>
|
|
1314
|
+
</div>
|
|
1315
|
+
|
|
1316
|
+
<div className="proteum-profiler__tags">
|
|
1317
|
+
<span className="proteum-profiler__tag">schedule:{truncate(formatCronFrequency(task), 64)}</span>
|
|
1318
|
+
<span className="proteum-profiler__tag">
|
|
1319
|
+
next:{task.nextInvocation ? formatTimestamp(task.nextInvocation) : 'none'}
|
|
1320
|
+
</span>
|
|
1321
|
+
<span className="proteum-profiler__tag">autoexec:{task.autoexec ? 'yes' : 'no'}</span>
|
|
1322
|
+
<span className="proteum-profiler__tag">
|
|
1323
|
+
automatic:{task.automaticExecution ? 'enabled' : 'disabled in dev'}
|
|
1324
|
+
</span>
|
|
1325
|
+
<span className="proteum-profiler__tag">runs:{task.runCount}</span>
|
|
1326
|
+
{task.lastTrigger ? <span className="proteum-profiler__tag">trigger:{task.lastTrigger}</span> : null}
|
|
1327
|
+
{task.lastRunStatus ? <span className="proteum-profiler__tag">{task.lastRunStatus}</span> : null}
|
|
1328
|
+
{task.lastRunDurationMs !== undefined ? (
|
|
1329
|
+
<span className="proteum-profiler__tag">{formatDuration(task.lastRunDurationMs)}</span>
|
|
1330
|
+
) : null}
|
|
1331
|
+
{task.lastErrorMessage ? (
|
|
1332
|
+
<span className="proteum-profiler__tag">{truncate(task.lastErrorMessage, 72)}</span>
|
|
1333
|
+
) : null}
|
|
1334
|
+
</div>
|
|
1335
|
+
|
|
1336
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">
|
|
1337
|
+
registered {formatTimestamp(task.registeredAt)}
|
|
1338
|
+
{cron.lastLoadedAt ? ` | refreshed ${formatTimestamp(cron.lastLoadedAt)}` : ''}
|
|
1339
|
+
</div>
|
|
1340
|
+
</div>
|
|
1341
|
+
))}
|
|
1342
|
+
</div>
|
|
1343
|
+
)}
|
|
1344
|
+
</div>
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const errorRows = [
|
|
1349
|
+
...session.steps
|
|
1350
|
+
.filter((step) => step.status === 'error')
|
|
1351
|
+
.map((step) => ({ key: step.id, title: step.label, value: step.errorMessage || 'Step failed' })),
|
|
1352
|
+
...session.traces
|
|
1353
|
+
.filter((trace) => trace.status === 'error')
|
|
1354
|
+
.map((trace) => ({ key: trace.id, title: trace.label, value: trace.errorMessage || 'Request failed' })),
|
|
1355
|
+
...findTraceEvents(primaryTrace, ['error']).map((event) => ({
|
|
1356
|
+
key: `${event.index}:error`,
|
|
1357
|
+
title: event.type,
|
|
1358
|
+
value: Object.entries(event.details)
|
|
1359
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
1360
|
+
.join(' '),
|
|
1361
|
+
})),
|
|
1362
|
+
];
|
|
1363
|
+
|
|
1364
|
+
return <SimpleSection empty="No errors captured." rows={errorRows} title="Errors" />;
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
export default function DevProfiler() {
|
|
1368
|
+
const [state, setState] = React.useState(() => profilerRuntime.getState());
|
|
1369
|
+
|
|
1370
|
+
React.useEffect(() => profilerRuntime.subscribe(() => setState(profilerRuntime.getState())), []);
|
|
1371
|
+
React.useEffect(() => {
|
|
1372
|
+
void profilerRuntime.refreshCommands();
|
|
1373
|
+
void profilerRuntime.refreshCronTasks();
|
|
1374
|
+
}, []);
|
|
1375
|
+
|
|
1376
|
+
React.useEffect(() => {
|
|
1377
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
1378
|
+
if (event.key === 'Escape' && profilerRuntime.getState().uiState === 'expanded') {
|
|
1379
|
+
profilerRuntime.setUiState('minimized');
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
window.addEventListener('keydown', onKeyDown);
|
|
1384
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
1385
|
+
}, []);
|
|
1386
|
+
|
|
1387
|
+
if (!window.dev) return null;
|
|
1388
|
+
|
|
1389
|
+
const session = getSelectedSession(state.sessions, state.selectedSessionId, state.currentSessionId);
|
|
1390
|
+
if (!session) return null;
|
|
1391
|
+
|
|
1392
|
+
const summary = getSummary(session);
|
|
1393
|
+
const tone = summary.errorCount > 0 ? 'error' : (summary.totalMs || 0) > 500 ? 'warn' : 'ok';
|
|
1394
|
+
const primaryTrace = summary.primaryTrace?.trace;
|
|
1395
|
+
const minimizedLabel =
|
|
1396
|
+
session.kind === 'client-navigation'
|
|
1397
|
+
? session.label
|
|
1398
|
+
: primaryTrace
|
|
1399
|
+
? `${primaryTrace.statusCode || 'pending'} ${primaryTrace.method} ${primaryTrace.path}`
|
|
1400
|
+
: session.label;
|
|
1401
|
+
const recentSessions = state.sessions.slice(-6).reverse();
|
|
1402
|
+
|
|
1403
|
+
return (
|
|
1404
|
+
<div className="proteum-profiler">
|
|
1405
|
+
<style dangerouslySetInnerHTML={{ __html: profilerStyles }} />
|
|
1406
|
+
|
|
1407
|
+
{state.uiState === 'pinned-handle' ? (
|
|
1408
|
+
<button
|
|
1409
|
+
className="proteum-profiler__handle"
|
|
1410
|
+
onClick={() => profilerRuntime.setUiState('minimized')}
|
|
1411
|
+
type="button"
|
|
1412
|
+
>
|
|
1413
|
+
Proteum Profiler
|
|
1414
|
+
</button>
|
|
1415
|
+
) : (
|
|
1416
|
+
<>
|
|
1417
|
+
{state.uiState === 'expanded' ? (
|
|
1418
|
+
<div className="proteum-profiler__panel">
|
|
1419
|
+
<div className="proteum-profiler__panelHeader">
|
|
1420
|
+
<select
|
|
1421
|
+
aria-label="Profiler path selector"
|
|
1422
|
+
className="proteum-profiler__select"
|
|
1423
|
+
onChange={(event) => profilerRuntime.selectSession(event.currentTarget.value)}
|
|
1424
|
+
value={session.id}
|
|
1425
|
+
>
|
|
1426
|
+
{recentSessions.map((recentSession) => (
|
|
1427
|
+
<option key={recentSession.id} value={recentSession.id}>
|
|
1428
|
+
{getSessionSelectorLabel(recentSession)}
|
|
1429
|
+
</option>
|
|
1430
|
+
))}
|
|
1431
|
+
</select>
|
|
1432
|
+
|
|
1433
|
+
<div className="proteum-profiler__panelTabs">
|
|
1434
|
+
{(Object.keys(panelLabels) as TProfilerPanel[]).map((panel) => (
|
|
1435
|
+
<button
|
|
1436
|
+
className={`proteum-profiler__pill ${
|
|
1437
|
+
state.activePanel === panel ? 'proteum-profiler__pill--active' : ''
|
|
1438
|
+
}`}
|
|
1439
|
+
key={panel}
|
|
1440
|
+
onClick={() => profilerRuntime.openPanel(panel)}
|
|
1441
|
+
type="button"
|
|
1442
|
+
>
|
|
1443
|
+
{panelLabels[panel]}
|
|
1444
|
+
</button>
|
|
1445
|
+
))}
|
|
1446
|
+
</div>
|
|
1447
|
+
|
|
1448
|
+
<div className="proteum-profiler__actions">
|
|
1449
|
+
<button className="proteum-profiler__pill" onClick={() => profilerRuntime.setUiState('minimized')} type="button">
|
|
1450
|
+
Collapse
|
|
1451
|
+
</button>
|
|
1452
|
+
<button className="proteum-profiler__pill" onClick={() => profilerRuntime.setUiState('pinned-handle')} type="button">
|
|
1453
|
+
Hide
|
|
1454
|
+
</button>
|
|
1455
|
+
</div>
|
|
1456
|
+
</div>
|
|
1457
|
+
|
|
1458
|
+
<div className="proteum-profiler__panelBody">{renderPanel(state.activePanel, session, summary, state)}</div>
|
|
1459
|
+
</div>
|
|
1460
|
+
) : null}
|
|
1461
|
+
|
|
1462
|
+
<div className="proteum-profiler__bar">
|
|
1463
|
+
<button
|
|
1464
|
+
className="proteum-profiler__token proteum-profiler__token--brand"
|
|
1465
|
+
onClick={() => profilerRuntime.openPanel('summary')}
|
|
1466
|
+
type="button"
|
|
1467
|
+
>
|
|
1468
|
+
Proteum
|
|
1469
|
+
</button>
|
|
1470
|
+
<StatusToken label={truncate(minimizedLabel, 56)} onClick={() => profilerRuntime.openPanel('summary')} tone={tone} />
|
|
1471
|
+
<StatusToken
|
|
1472
|
+
label={formatDuration(summary.totalMs)}
|
|
1473
|
+
onClick={() => profilerRuntime.openPanel('summary')}
|
|
1474
|
+
tone={tone}
|
|
1475
|
+
/>
|
|
1476
|
+
<StatusToken
|
|
1477
|
+
label={truncate(summary.routeLabel, 28)}
|
|
1478
|
+
onClick={() => profilerRuntime.openPanel('routing')}
|
|
1479
|
+
tone="ok"
|
|
1480
|
+
/>
|
|
1481
|
+
<StatusToken
|
|
1482
|
+
label={
|
|
1483
|
+
summary.ssrPayloadBytes !== undefined
|
|
1484
|
+
? `${formatDuration(summary.renderMs)} ${formatBytes(summary.ssrPayloadBytes)}`
|
|
1485
|
+
: formatDuration(summary.renderMs)
|
|
1486
|
+
}
|
|
1487
|
+
onClick={() => profilerRuntime.openPanel('ssr')}
|
|
1488
|
+
tone="ok"
|
|
1489
|
+
/>
|
|
1490
|
+
<StatusToken
|
|
1491
|
+
label={`API ${summary.apiSyncCount} / ${summary.apiAsyncCount}`}
|
|
1492
|
+
onClick={() => profilerRuntime.openPanel('api')}
|
|
1493
|
+
tone={summary.apiAsyncCount > 0 || summary.apiSyncCount > 0 ? 'ok' : 'warn'}
|
|
1494
|
+
/>
|
|
1495
|
+
{summary.errorCount > 0 ? (
|
|
1496
|
+
<StatusToken
|
|
1497
|
+
label={`${summary.errorCount} error${summary.errorCount === 1 ? '' : 's'}`}
|
|
1498
|
+
onClick={() => profilerRuntime.openPanel('errors')}
|
|
1499
|
+
tone="error"
|
|
1500
|
+
/>
|
|
1501
|
+
) : null}
|
|
1502
|
+
<div className="proteum-profiler__spacer" />
|
|
1503
|
+
<button className="proteum-profiler__token" onClick={() => profilerRuntime.setUiState('pinned-handle')} type="button">
|
|
1504
|
+
Hide
|
|
1505
|
+
</button>
|
|
1506
|
+
</div>
|
|
1507
|
+
</>
|
|
1508
|
+
)}
|
|
1509
|
+
</div>
|
|
1510
|
+
);
|
|
1511
|
+
}
|