proteum 2.1.9 → 2.2.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/.codex/environments/environment.toml +11 -0
- package/AGENTS.md +27 -11
- package/README.md +30 -11
- package/agents/project/AGENTS.md +172 -123
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/app-root/AGENTS.md +16 -0
- package/agents/project/client/AGENTS.md +5 -5
- package/agents/project/client/pages/AGENTS.md +13 -13
- package/agents/project/diagnostics.md +19 -10
- package/agents/project/optimizations.md +5 -6
- package/agents/project/root/AGENTS.md +297 -0
- package/agents/project/server/routes/AGENTS.md +2 -2
- package/agents/project/server/services/AGENTS.md +4 -2
- package/agents/project/tests/AGENTS.md +9 -2
- package/cli/app/index.ts +31 -7
- package/cli/commands/configure.ts +226 -0
- package/cli/commands/dev.ts +0 -2
- package/cli/commands/diagnose.ts +33 -1
- package/cli/commands/explain.ts +1 -1
- package/cli/commands/migrate.ts +51 -0
- package/cli/commands/orient.ts +169 -0
- package/cli/commands/perf.ts +8 -1
- package/cli/commands/verify.ts +1003 -49
- package/cli/compiler/artifacts/manifest.ts +4 -4
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +12 -3
- package/cli/compiler/client/index.ts +65 -19
- package/cli/compiler/common/files/style.ts +47 -2
- package/cli/compiler/common/generatedRouteModules.ts +31 -38
- package/cli/compiler/common/index.ts +10 -0
- package/cli/compiler/common/proteumManifest.ts +1 -0
- package/cli/compiler/server/index.ts +34 -9
- package/cli/context.ts +6 -1
- package/cli/index.ts +7 -8
- package/cli/migrate/pageContract.ts +516 -0
- package/cli/paths.ts +47 -6
- package/cli/presentation/commands.ts +100 -10
- package/cli/presentation/devSession.ts +4 -6
- package/cli/presentation/help.ts +2 -2
- package/cli/presentation/ink.ts +10 -5
- package/cli/presentation/welcome.ts +2 -4
- package/cli/runtime/commands.ts +94 -1
- package/cli/scaffold/index.ts +2 -2
- package/cli/scaffold/templates.ts +4 -2
- package/cli/utils/agents.ts +273 -58
- package/client/dev/profiler/index.tsx +3 -2
- package/client/router.ts +10 -2
- package/client/services/router/index.tsx +6 -22
- package/common/dev/connect.ts +20 -4
- package/common/dev/console.ts +7 -0
- package/common/dev/contractsDoctor.ts +354 -0
- package/common/dev/diagnostics.ts +10 -7
- package/common/dev/inspection.ts +830 -38
- package/common/dev/performance.ts +19 -5
- package/common/dev/profiler.ts +1 -0
- package/common/dev/proteumManifest.ts +5 -4
- package/common/dev/requestTrace.ts +78 -1
- package/common/env/proteumEnv.ts +10 -3
- package/common/router/contracts.ts +8 -11
- package/common/router/index.ts +2 -2
- package/common/router/pageData.ts +72 -0
- package/common/router/register.ts +10 -46
- package/common/router/response/page.ts +28 -16
- package/docs/assets/unique-domains-chip.png +0 -0
- package/docs/dev-sessions.md +8 -4
- package/docs/diagnostics.md +77 -11
- package/docs/migrate-from-2.1.3.md +388 -0
- package/docs/request-tracing.md +42 -9
- package/package.json +6 -1
- package/scripts/update-codex-agents.ts +2 -2
- package/server/app/container/console/index.ts +11 -1
- package/server/app/container/trace/index.ts +370 -72
- package/server/app/devDiagnostics.ts +1 -1
- package/server/app/index.ts +5 -1
- package/server/services/auth/index.ts +9 -0
- package/server/services/prisma/index.ts +15 -12
- package/server/services/router/http/index.ts +1 -1
- package/server/services/router/index.ts +105 -23
- package/server/services/router/request/api.ts +7 -1
- package/server/services/router/request/index.ts +2 -1
- package/server/services/router/response/index.ts +8 -28
- package/types/global/vendors.d.ts +12 -0
- package/types/vendors.d.ts +12 -0
- package/common/router/pageSetup.ts +0 -51
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
3
4
|
|
|
4
5
|
import type ApplicationContainer from '..';
|
|
6
|
+
import context from '@server/context';
|
|
7
|
+
import type { ChannelInfos } from '../console';
|
|
5
8
|
import {
|
|
6
9
|
traceCaptureModes,
|
|
7
10
|
type TTraceCaptureMode,
|
|
@@ -13,13 +16,18 @@ import {
|
|
|
13
16
|
type TTraceSqlQueryCallerOrigin,
|
|
14
17
|
type TTraceSqlQueryKind,
|
|
15
18
|
type TTraceSummaryValue,
|
|
19
|
+
type TRequestProfiling,
|
|
20
|
+
type TRequestProfilingApiCall,
|
|
21
|
+
type TRequestProfilingSqlQuery,
|
|
16
22
|
type TRequestTrace,
|
|
17
23
|
type TTraceMemorySnapshot,
|
|
18
24
|
type TRequestTraceListItem,
|
|
19
25
|
} from '@common/dev/requestTrace';
|
|
26
|
+
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
20
27
|
|
|
21
28
|
export type Config = {
|
|
22
29
|
enable: boolean;
|
|
30
|
+
profilerEnable: boolean;
|
|
23
31
|
requestsLimit: number;
|
|
24
32
|
eventsLimit: number;
|
|
25
33
|
capture: TTraceCaptureMode;
|
|
@@ -28,11 +36,21 @@ export type Config = {
|
|
|
28
36
|
|
|
29
37
|
type TTraceInspectable = object | PrimitiveValue | bigint | symbol | null | undefined | (() => void);
|
|
30
38
|
type TTraceDetails = { [key: string]: TTraceInspectable };
|
|
39
|
+
type TSerializeJsonValueOptions = { redactSensitive: boolean };
|
|
40
|
+
type TActiveRequestRecord = {
|
|
41
|
+
profiling: TRequestProfiling;
|
|
42
|
+
trace?: TRequestTrace;
|
|
43
|
+
capture?: TTraceCaptureMode;
|
|
44
|
+
};
|
|
31
45
|
|
|
32
46
|
const capturePriority: Record<TTraceCaptureMode, number> = { summary: 0, resolve: 1, deep: 2 };
|
|
33
47
|
const sensitiveKeyPattern =
|
|
34
48
|
/(^|\.)(authorization|cookie|set-cookie|password|pass|pwd|secret|token|refreshToken|accessToken|apiKey|apiSecret|secretAccessKey|accessKeyId|privateKey|session|jwt|rawBody)$/i;
|
|
35
49
|
const maxStringLength = 240;
|
|
50
|
+
const normalizeFilepath = (value: string) => value.replace(/\\/g, '/');
|
|
51
|
+
const sqlCommentPattern = /\/\*[\s\S]*?\*\//g;
|
|
52
|
+
const sqlLineCommentPattern = /--.*$/gm;
|
|
53
|
+
const stackFilepathPatterns = [/\((\/.+?):\d+:\d+\)$/, /at (\/.+?):\d+:\d+$/];
|
|
36
54
|
|
|
37
55
|
const isTraceCaptureMode = (value: string): value is TTraceCaptureMode =>
|
|
38
56
|
traceCaptureModes.includes(value as TTraceCaptureMode);
|
|
@@ -42,8 +60,13 @@ const isSensitiveKeyPath = (keyPath: string[]) => sensitiveKeyPattern.test(keyPa
|
|
|
42
60
|
const summarizeString = (value: string) =>
|
|
43
61
|
value.length <= maxStringLength ? value : `${value.slice(0, maxStringLength)}…`;
|
|
44
62
|
|
|
45
|
-
const serializeJsonValue = (
|
|
46
|
-
|
|
63
|
+
const serializeJsonValue = (
|
|
64
|
+
value: unknown,
|
|
65
|
+
keyPath: string[],
|
|
66
|
+
seen: WeakSet<object>,
|
|
67
|
+
{ redactSensitive }: TSerializeJsonValueOptions,
|
|
68
|
+
): unknown => {
|
|
69
|
+
if (redactSensitive && isSensitiveKeyPath(keyPath)) return `[redacted: Sensitive key ${keyPath[keyPath.length - 1] || 'value'}]`;
|
|
47
70
|
if (value === undefined || value === null) return value;
|
|
48
71
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
49
72
|
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
@@ -53,11 +76,14 @@ const serializeJsonValue = (value: unknown, keyPath: string[], seen: WeakSet<obj
|
|
|
53
76
|
if (value instanceof Date) return value.toISOString();
|
|
54
77
|
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack };
|
|
55
78
|
if (Buffer.isBuffer(value)) return `[Buffer ${value.byteLength} bytes]`;
|
|
56
|
-
if (value instanceof Map)
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
if (value instanceof Map)
|
|
80
|
+
return Array.from(value.entries()).map(([entryKey, entryValue], index) =>
|
|
81
|
+
serializeJsonValue([entryKey, entryValue], [...keyPath, `[${index}]`], seen, { redactSensitive }),
|
|
82
|
+
);
|
|
59
83
|
if (value instanceof Set) {
|
|
60
|
-
return Array.from(value.values()).map((entryValue, index) =>
|
|
84
|
+
return Array.from(value.values()).map((entryValue, index) =>
|
|
85
|
+
serializeJsonValue(entryValue, [...keyPath, `[${index}]`], seen, { redactSensitive }),
|
|
86
|
+
);
|
|
61
87
|
}
|
|
62
88
|
|
|
63
89
|
if (typeof value !== 'object') return String(value);
|
|
@@ -66,19 +92,22 @@ const serializeJsonValue = (value: unknown, keyPath: string[], seen: WeakSet<obj
|
|
|
66
92
|
seen.add(value);
|
|
67
93
|
|
|
68
94
|
if (Array.isArray(value)) {
|
|
69
|
-
return value.map((item, index) => serializeJsonValue(item, [...keyPath, `[${index}]`], seen));
|
|
95
|
+
return value.map((item, index) => serializeJsonValue(item, [...keyPath, `[${index}]`], seen, { redactSensitive }));
|
|
70
96
|
}
|
|
71
97
|
|
|
72
98
|
const serialized: Record<string, unknown> = {};
|
|
73
99
|
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
74
|
-
const nextValue = serializeJsonValue(entryValue, [...keyPath, entryKey], seen);
|
|
100
|
+
const nextValue = serializeJsonValue(entryValue, [...keyPath, entryKey], seen, { redactSensitive });
|
|
75
101
|
if (nextValue !== undefined) serialized[entryKey] = nextValue;
|
|
76
102
|
}
|
|
77
103
|
|
|
78
104
|
return serialized;
|
|
79
105
|
};
|
|
80
106
|
|
|
81
|
-
const serializeCaptureValue = (value: TTraceInspectable, key: string) =>
|
|
107
|
+
const serializeCaptureValue = (value: TTraceInspectable, key: string) =>
|
|
108
|
+
serializeJsonValue(value, [key], new WeakSet<object>(), { redactSensitive: true });
|
|
109
|
+
const serializeRawCaptureValue = (value: TTraceInspectable, key: string) =>
|
|
110
|
+
serializeJsonValue(value, [key], new WeakSet<object>(), { redactSensitive: false });
|
|
82
111
|
|
|
83
112
|
const summarizeError = (error: Error): TTraceSummaryValue => ({
|
|
84
113
|
kind: 'error',
|
|
@@ -173,9 +202,14 @@ const snapshotMemory = (): TTraceMemorySnapshot => {
|
|
|
173
202
|
};
|
|
174
203
|
|
|
175
204
|
export default class Trace {
|
|
176
|
-
private requests = new Map<string,
|
|
205
|
+
private requests = new Map<string, TActiveRequestRecord>();
|
|
177
206
|
private order: string[] = [];
|
|
178
207
|
private armedCapture?: TTraceCaptureMode;
|
|
208
|
+
private manifestCache?: {
|
|
209
|
+
manifest: TProteumManifest;
|
|
210
|
+
mtimeMs: number;
|
|
211
|
+
serviceByFilepath: Map<string, string>;
|
|
212
|
+
};
|
|
179
213
|
private activeMeasurements = new Map<
|
|
180
214
|
string,
|
|
181
215
|
{
|
|
@@ -189,10 +223,129 @@ export default class Trace {
|
|
|
189
223
|
private config: Config,
|
|
190
224
|
) {}
|
|
191
225
|
|
|
192
|
-
public
|
|
226
|
+
public isDevTraceEnabled() {
|
|
193
227
|
return __DEV__ && this.config.enable && this.container.Environment.profile === 'dev';
|
|
194
228
|
}
|
|
195
229
|
|
|
230
|
+
public isProfilingEnabled() {
|
|
231
|
+
return this.config.profilerEnable;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public shouldInstrumentRequests() {
|
|
235
|
+
return this.isDevTraceEnabled() || this.isProfilingEnabled();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private getContextChannel() {
|
|
239
|
+
return context.getStore() as ChannelInfos | undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private readManifestCache() {
|
|
243
|
+
const manifestFilepath = path.join(this.container.path.root, '.proteum', 'manifest.json');
|
|
244
|
+
if (!fs.existsSync(manifestFilepath)) return undefined;
|
|
245
|
+
|
|
246
|
+
const stats = fs.statSync(manifestFilepath);
|
|
247
|
+
if (this.manifestCache && this.manifestCache.mtimeMs === stats.mtimeMs) return this.manifestCache;
|
|
248
|
+
|
|
249
|
+
const manifest = fs.readJSONSync(manifestFilepath) as TProteumManifest;
|
|
250
|
+
const serviceByFilepath = new Map<string, string>();
|
|
251
|
+
for (const service of [...manifest.services.app, ...manifest.services.routerPlugins]) {
|
|
252
|
+
if (!service.sourceFilepath) continue;
|
|
253
|
+
serviceByFilepath.set(normalizeFilepath(path.resolve(service.sourceFilepath)), service.registeredName);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.manifestCache = {
|
|
257
|
+
manifest,
|
|
258
|
+
mtimeMs: stats.mtimeMs,
|
|
259
|
+
serviceByFilepath,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
return this.manifestCache;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private getStackFilepaths(stack?: string) {
|
|
266
|
+
if (!stack) return [];
|
|
267
|
+
|
|
268
|
+
const filepaths: string[] = [];
|
|
269
|
+
for (const line of stack.split('\n')) {
|
|
270
|
+
const trimmedLine = line.trim();
|
|
271
|
+
let matchedFilepath: string | undefined;
|
|
272
|
+
|
|
273
|
+
for (const pattern of stackFilepathPatterns) {
|
|
274
|
+
const match = trimmedLine.match(pattern);
|
|
275
|
+
if (match?.[1]) {
|
|
276
|
+
matchedFilepath = match[1];
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!matchedFilepath) continue;
|
|
282
|
+
if (matchedFilepath.includes('/node_modules/')) continue;
|
|
283
|
+
|
|
284
|
+
filepaths.push(normalizeFilepath(path.resolve(matchedFilepath)));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return filepaths;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private inferServiceLabelFromStack(stack?: string) {
|
|
291
|
+
const manifestCache = this.readManifestCache();
|
|
292
|
+
if (!manifestCache) return undefined;
|
|
293
|
+
|
|
294
|
+
for (const filepath of this.getStackFilepaths(stack)) {
|
|
295
|
+
const serviceLabel = manifestCache.serviceByFilepath.get(filepath);
|
|
296
|
+
if (serviceLabel) return serviceLabel;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private normalizeSqlQuery(query: string) {
|
|
303
|
+
return query
|
|
304
|
+
.replace(sqlCommentPattern, ' ')
|
|
305
|
+
.replace(sqlLineCommentPattern, ' ')
|
|
306
|
+
.replace(/'([^']|'')*'/g, '?')
|
|
307
|
+
.replace(/"([^"]|"")*"/g, '?')
|
|
308
|
+
.replace(/\b\d+(?:\.\d+)?\b/g, '?')
|
|
309
|
+
.replace(/\s+/g, ' ')
|
|
310
|
+
.trim()
|
|
311
|
+
.toUpperCase();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private createSqlFingerprint(query: string) {
|
|
315
|
+
const normalized = this.normalizeSqlQuery(query);
|
|
316
|
+
|
|
317
|
+
if (!normalized) return undefined;
|
|
318
|
+
|
|
319
|
+
return createHash('sha1').update(normalized).digest('hex').slice(0, 12);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private createProfiling(input: {
|
|
323
|
+
enabled: boolean;
|
|
324
|
+
id: string;
|
|
325
|
+
method: string;
|
|
326
|
+
path: string;
|
|
327
|
+
url: string;
|
|
328
|
+
profilerOrigin?: string;
|
|
329
|
+
profilerParentRequestId?: string;
|
|
330
|
+
}): TRequestProfiling {
|
|
331
|
+
return {
|
|
332
|
+
enabled: input.enabled,
|
|
333
|
+
requestId: input.id,
|
|
334
|
+
method: input.method,
|
|
335
|
+
path: input.path,
|
|
336
|
+
url: input.url,
|
|
337
|
+
startedAt: nowIso(),
|
|
338
|
+
profilerOrigin: input.profilerOrigin,
|
|
339
|
+
profilerParentRequestId: input.profilerParentRequestId,
|
|
340
|
+
apiCalls: [],
|
|
341
|
+
sqlQueries: [],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private getRecord(requestId: string) {
|
|
346
|
+
return this.requests.get(requestId);
|
|
347
|
+
}
|
|
348
|
+
|
|
196
349
|
public armNextRequest(capture: string) {
|
|
197
350
|
if (!isTraceCaptureMode(capture)) {
|
|
198
351
|
throw new Error(`Unsupported trace capture mode "${capture}". Expected one of: ${traceCaptureModes.join(', ')}.`);
|
|
@@ -213,46 +366,72 @@ export default class Trace {
|
|
|
213
366
|
profilerOrigin?: string;
|
|
214
367
|
profilerParentRequestId?: string;
|
|
215
368
|
}) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.armedCapture = undefined;
|
|
220
|
-
|
|
221
|
-
const trace: TRequestTrace = {
|
|
369
|
+
const profilingEnabled = this.shouldInstrumentRequests();
|
|
370
|
+
const profiling = this.createProfiling({
|
|
371
|
+
enabled: profilingEnabled,
|
|
222
372
|
id: input.id,
|
|
223
373
|
method: input.method,
|
|
224
374
|
path: input.path,
|
|
225
375
|
url: input.url,
|
|
226
|
-
capture,
|
|
227
|
-
profilerSessionId: input.profilerSessionId,
|
|
228
376
|
profilerOrigin: input.profilerOrigin,
|
|
229
377
|
profilerParentRequestId: input.profilerParentRequestId,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
378
|
+
});
|
|
379
|
+
if (!profilingEnabled) return profiling;
|
|
380
|
+
|
|
381
|
+
const traceEnabled = this.isDevTraceEnabled();
|
|
382
|
+
const capture = traceEnabled ? this.armedCapture ?? this.config.capture : undefined;
|
|
383
|
+
this.armedCapture = undefined;
|
|
384
|
+
|
|
385
|
+
const trace =
|
|
386
|
+
traceEnabled
|
|
387
|
+
? ({
|
|
388
|
+
id: input.id,
|
|
389
|
+
method: input.method,
|
|
390
|
+
path: input.path,
|
|
391
|
+
url: input.url,
|
|
392
|
+
capture: capture as TTraceCaptureMode,
|
|
393
|
+
profilerSessionId: input.profilerSessionId,
|
|
394
|
+
profilerOrigin: input.profilerOrigin,
|
|
395
|
+
profilerParentRequestId: input.profilerParentRequestId,
|
|
396
|
+
startedAt: profiling.startedAt,
|
|
397
|
+
droppedEvents: 0,
|
|
398
|
+
requestDataJson: serializeCaptureValue(input.data, 'requestData'),
|
|
399
|
+
calls: [],
|
|
400
|
+
sqlQueries: [],
|
|
401
|
+
events: [],
|
|
402
|
+
} satisfies TRequestTrace)
|
|
403
|
+
: undefined;
|
|
404
|
+
|
|
405
|
+
this.requests.set(input.id, {
|
|
406
|
+
profiling,
|
|
407
|
+
trace,
|
|
408
|
+
capture,
|
|
409
|
+
});
|
|
410
|
+
this.activeMeasurements.set(input.id, { cpu: process.cpuUsage(), memory: snapshotMemory() });
|
|
237
411
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
412
|
+
if (trace) {
|
|
413
|
+
this.order.push(input.id);
|
|
414
|
+
this.trimRequestBuffer();
|
|
415
|
+
this.record(input.id, 'request.start', { method: input.method, path: input.path, url: input.url, headers: input.headers, data: input.data });
|
|
416
|
+
}
|
|
242
417
|
|
|
243
|
-
|
|
418
|
+
return profiling;
|
|
244
419
|
}
|
|
245
420
|
|
|
246
421
|
public setRequestUser(requestId: string, user?: string) {
|
|
247
|
-
const
|
|
248
|
-
if (!
|
|
422
|
+
const record = this.getRecord(requestId);
|
|
423
|
+
if (!record) return;
|
|
424
|
+
|
|
425
|
+
record.profiling.user = user;
|
|
426
|
+
if (!record.trace) return;
|
|
249
427
|
|
|
428
|
+
const trace = record.trace;
|
|
250
429
|
trace.user = user;
|
|
251
430
|
if (user) this.record(requestId, 'request.user', { user });
|
|
252
431
|
}
|
|
253
432
|
|
|
254
433
|
public getCapture(requestId: string) {
|
|
255
|
-
return this.
|
|
434
|
+
return this.getRecord(requestId)?.capture;
|
|
256
435
|
}
|
|
257
436
|
|
|
258
437
|
public shouldCapture(requestId: string, minimumCapture: TTraceCaptureMode) {
|
|
@@ -263,7 +442,8 @@ export default class Trace {
|
|
|
263
442
|
}
|
|
264
443
|
|
|
265
444
|
public record(requestId: string, type: TTraceEventType, details: TTraceDetails, minimumCapture: TTraceCaptureMode = 'summary') {
|
|
266
|
-
const
|
|
445
|
+
const record = this.getRecord(requestId);
|
|
446
|
+
const trace = record?.trace;
|
|
267
447
|
if (!trace || !this.shouldCapture(requestId, minimumCapture)) return;
|
|
268
448
|
|
|
269
449
|
if (trace.events.length >= this.config.eventsLimit) {
|
|
@@ -283,28 +463,41 @@ export default class Trace {
|
|
|
283
463
|
}
|
|
284
464
|
|
|
285
465
|
public finishRequest(requestId: string, output: { statusCode: number; user?: string; errorMessage?: string }) {
|
|
286
|
-
const
|
|
287
|
-
if (!
|
|
466
|
+
const record = this.getRecord(requestId);
|
|
467
|
+
if (!record) return;
|
|
468
|
+
|
|
469
|
+
const { profiling, trace } = record;
|
|
470
|
+
if (output.user) profiling.user = output.user;
|
|
471
|
+
profiling.statusCode = output.statusCode;
|
|
472
|
+
profiling.errorMessage = output.errorMessage;
|
|
288
473
|
|
|
289
|
-
if (output.user) trace.user = output.user;
|
|
290
|
-
trace.statusCode = output.statusCode;
|
|
291
|
-
trace.errorMessage = output.errorMessage;
|
|
292
474
|
const measurement = this.activeMeasurements.get(requestId);
|
|
293
475
|
if (measurement) {
|
|
294
|
-
const cpu = process.cpuUsage(measurement.cpu);
|
|
295
|
-
trace.performance = {
|
|
296
|
-
cpu: {
|
|
297
|
-
systemMicros: cpu.system,
|
|
298
|
-
userMicros: cpu.user,
|
|
299
|
-
},
|
|
300
|
-
memory: {
|
|
301
|
-
after: snapshotMemory(),
|
|
302
|
-
before: measurement.memory,
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
476
|
this.activeMeasurements.delete(requestId);
|
|
477
|
+
|
|
478
|
+
if (trace) {
|
|
479
|
+
const cpu = process.cpuUsage(measurement.cpu);
|
|
480
|
+
trace.performance = {
|
|
481
|
+
cpu: {
|
|
482
|
+
systemMicros: cpu.system,
|
|
483
|
+
userMicros: cpu.user,
|
|
484
|
+
},
|
|
485
|
+
memory: {
|
|
486
|
+
after: snapshotMemory(),
|
|
487
|
+
before: measurement.memory,
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
306
491
|
}
|
|
307
492
|
|
|
493
|
+
profiling.finishedAt = nowIso();
|
|
494
|
+
profiling.durationMs = Math.max(0, Date.parse(profiling.finishedAt) - Date.parse(profiling.startedAt));
|
|
495
|
+
|
|
496
|
+
if (!trace) return;
|
|
497
|
+
|
|
498
|
+
if (output.user) trace.user = output.user;
|
|
499
|
+
trace.statusCode = output.statusCode;
|
|
500
|
+
trace.errorMessage = output.errorMessage;
|
|
308
501
|
this.record(
|
|
309
502
|
requestId,
|
|
310
503
|
'request.finish',
|
|
@@ -312,8 +505,8 @@ export default class Trace {
|
|
|
312
505
|
'summary',
|
|
313
506
|
);
|
|
314
507
|
|
|
315
|
-
trace.finishedAt =
|
|
316
|
-
trace.durationMs =
|
|
508
|
+
trace.finishedAt = profiling.finishedAt;
|
|
509
|
+
trace.durationMs = profiling.durationMs;
|
|
317
510
|
|
|
318
511
|
if (this.config.persistOnError && trace.statusCode >= 500) {
|
|
319
512
|
trace.persistedFilepath = this.exportRequest(requestId);
|
|
@@ -330,16 +523,47 @@ export default class Trace {
|
|
|
330
523
|
fetcherId?: string;
|
|
331
524
|
connectedProjectNamespace?: string;
|
|
332
525
|
connectedControllerAccessor?: string;
|
|
526
|
+
ownerLabel?: string;
|
|
527
|
+
ownerFilepath?: string;
|
|
528
|
+
serviceLabel?: string;
|
|
529
|
+
cacheKey?: string;
|
|
530
|
+
cachePhase?: string;
|
|
333
531
|
parentId?: string;
|
|
334
532
|
requestDataKeys?: string[];
|
|
335
533
|
requestData?: TTraceInspectable;
|
|
336
534
|
},
|
|
337
535
|
) {
|
|
338
|
-
const
|
|
339
|
-
if (!
|
|
536
|
+
const record = this.getRecord(requestId);
|
|
537
|
+
if (!record) return undefined;
|
|
538
|
+
const channel = this.getContextChannel();
|
|
539
|
+
const inferredServiceLabel = input.serviceLabel || channel?.serviceLabel || this.inferServiceLabelFromStack(new Error().stack);
|
|
540
|
+
const callIndex = record.profiling.apiCalls.length;
|
|
541
|
+
const startedAt = nowIso();
|
|
542
|
+
const callId = `${requestId}:call:${callIndex}`;
|
|
543
|
+
|
|
544
|
+
const profilingCall: TRequestProfilingApiCall = {
|
|
545
|
+
id: callId,
|
|
546
|
+
origin: input.origin,
|
|
547
|
+
label: input.label,
|
|
548
|
+
method: input.method || '',
|
|
549
|
+
path: input.path || '',
|
|
550
|
+
fetcherId: input.fetcherId,
|
|
551
|
+
connectedProjectNamespace: input.connectedProjectNamespace,
|
|
552
|
+
connectedControllerAccessor: input.connectedControllerAccessor,
|
|
553
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
554
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
555
|
+
serviceLabel: inferredServiceLabel,
|
|
556
|
+
startedAt,
|
|
557
|
+
requestBodyJson: input.requestData !== undefined ? serializeRawCaptureValue(input.requestData, 'requestData') : undefined,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
record.profiling.apiCalls.push(profilingCall);
|
|
561
|
+
|
|
562
|
+
const trace = record.trace;
|
|
563
|
+
if (!trace) return callId;
|
|
340
564
|
|
|
341
565
|
const call: TTraceCall = {
|
|
342
|
-
id:
|
|
566
|
+
id: callId,
|
|
343
567
|
parentId: input.parentId,
|
|
344
568
|
origin: input.origin,
|
|
345
569
|
label: input.label,
|
|
@@ -348,7 +572,12 @@ export default class Trace {
|
|
|
348
572
|
fetcherId: input.fetcherId,
|
|
349
573
|
connectedProjectNamespace: input.connectedProjectNamespace,
|
|
350
574
|
connectedControllerAccessor: input.connectedControllerAccessor,
|
|
351
|
-
|
|
575
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
576
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
577
|
+
serviceLabel: inferredServiceLabel,
|
|
578
|
+
cacheKey: input.cacheKey || channel?.cacheKey,
|
|
579
|
+
cachePhase: input.cachePhase || channel?.cachePhase,
|
|
580
|
+
startedAt,
|
|
352
581
|
requestDataKeys: input.requestDataKeys || [],
|
|
353
582
|
requestData: input.requestData !== undefined ? summarizeCaptureValue(input.requestData, trace.capture, 'requestData') : undefined,
|
|
354
583
|
requestDataJson: input.requestData !== undefined ? serializeCaptureValue(input.requestData, 'requestData') : undefined,
|
|
@@ -356,7 +585,7 @@ export default class Trace {
|
|
|
356
585
|
};
|
|
357
586
|
|
|
358
587
|
trace.calls.push(call);
|
|
359
|
-
return
|
|
588
|
+
return callId;
|
|
360
589
|
}
|
|
361
590
|
|
|
362
591
|
public finishCall(
|
|
@@ -371,12 +600,24 @@ export default class Trace {
|
|
|
371
600
|
) {
|
|
372
601
|
if (!callId) return;
|
|
373
602
|
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
603
|
+
const record = this.getRecord(requestId);
|
|
604
|
+
const profilingCall = record?.profiling.apiCalls.find((candidate) => candidate.id === callId);
|
|
605
|
+
if (!record || !profilingCall) return;
|
|
606
|
+
|
|
607
|
+
profilingCall.finishedAt = nowIso();
|
|
608
|
+
profilingCall.durationMs = Math.max(0, Date.parse(profilingCall.finishedAt) - Date.parse(profilingCall.startedAt));
|
|
609
|
+
profilingCall.statusCode = output.statusCode;
|
|
610
|
+
profilingCall.errorMessage = output.errorMessage;
|
|
611
|
+
profilingCall.responseBodyJson = output.result !== undefined ? serializeRawCaptureValue(output.result, 'result') : undefined;
|
|
612
|
+
|
|
613
|
+
const trace = record.trace;
|
|
614
|
+
if (!trace) return;
|
|
615
|
+
|
|
616
|
+
const call = trace.calls.find((candidate) => candidate.id === callId);
|
|
617
|
+
if (!call) return;
|
|
377
618
|
|
|
378
|
-
call.finishedAt =
|
|
379
|
-
call.durationMs =
|
|
619
|
+
call.finishedAt = profilingCall.finishedAt;
|
|
620
|
+
call.durationMs = profilingCall.durationMs;
|
|
380
621
|
call.statusCode = output.statusCode;
|
|
381
622
|
call.errorMessage = output.errorMessage;
|
|
382
623
|
call.resultKeys = output.resultKeys || [];
|
|
@@ -385,7 +626,7 @@ export default class Trace {
|
|
|
385
626
|
}
|
|
386
627
|
|
|
387
628
|
public setRequestResult(requestId: string, result: TTraceInspectable) {
|
|
388
|
-
const trace = this.
|
|
629
|
+
const trace = this.getRecord(requestId)?.trace;
|
|
389
630
|
if (!trace) return;
|
|
390
631
|
|
|
391
632
|
trace.resultJson = serializeCaptureValue(result, 'result');
|
|
@@ -405,23 +646,61 @@ export default class Trace {
|
|
|
405
646
|
kind: TTraceSqlQueryKind;
|
|
406
647
|
model?: string;
|
|
407
648
|
operation: string;
|
|
649
|
+
ownerLabel?: string;
|
|
650
|
+
ownerFilepath?: string;
|
|
651
|
+
serviceLabel?: string;
|
|
652
|
+
connectedNamespace?: string;
|
|
408
653
|
paramsJson?: unknown;
|
|
409
654
|
paramsText?: string;
|
|
410
655
|
query: string;
|
|
411
656
|
target?: string;
|
|
412
657
|
},
|
|
413
658
|
) {
|
|
414
|
-
const
|
|
415
|
-
if (!
|
|
659
|
+
const record = this.getRecord(requestId);
|
|
660
|
+
if (!record) return;
|
|
661
|
+
const channel = this.getContextChannel();
|
|
416
662
|
|
|
417
663
|
const durationMs = Math.max(0, input.durationMs || 0);
|
|
418
664
|
const finishedAt = input.finishedAt || nowIso();
|
|
419
665
|
const finishedAtMs = Date.parse(finishedAt);
|
|
420
666
|
const startedAt =
|
|
421
667
|
Number.isFinite(finishedAtMs) && durationMs > 0 ? new Date(finishedAtMs - durationMs).toISOString() : finishedAt;
|
|
668
|
+
const fingerprint = this.createSqlFingerprint(input.query);
|
|
669
|
+
const normalizedQuery = this.normalizeSqlQuery(input.query) || undefined;
|
|
670
|
+
const inferredServiceLabel = input.serviceLabel || channel?.serviceLabel || this.inferServiceLabelFromStack(new Error().stack);
|
|
671
|
+
|
|
672
|
+
const profilingQuery: TRequestProfilingSqlQuery = {
|
|
673
|
+
id: `${requestId}:sql:${record.profiling.sqlQueries.length}`,
|
|
674
|
+
callerCallId: input.callerCallId,
|
|
675
|
+
callerFetcherId: input.callerFetcherId,
|
|
676
|
+
callerLabel: input.callerLabel,
|
|
677
|
+
callerMethod: input.callerMethod || '',
|
|
678
|
+
callerOrigin: input.callerOrigin || 'request',
|
|
679
|
+
callerPath: input.callerPath || '',
|
|
680
|
+
durationMs,
|
|
681
|
+
finishedAt,
|
|
682
|
+
kind: input.kind,
|
|
683
|
+
model: input.model,
|
|
684
|
+
operation: input.operation,
|
|
685
|
+
fingerprint,
|
|
686
|
+
normalizedQuery,
|
|
687
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
688
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
689
|
+
serviceLabel: inferredServiceLabel,
|
|
690
|
+
connectedNamespace: input.connectedNamespace || channel?.connectedNamespace,
|
|
691
|
+
paramsJson: input.paramsJson,
|
|
692
|
+
paramsText: input.paramsText,
|
|
693
|
+
query: input.query.trim(),
|
|
694
|
+
startedAt,
|
|
695
|
+
target: input.target,
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
record.profiling.sqlQueries.push(profilingQuery);
|
|
699
|
+
const trace = record.trace;
|
|
700
|
+
if (!trace) return;
|
|
422
701
|
|
|
423
702
|
const sqlQuery: TTraceSqlQuery = {
|
|
424
|
-
id:
|
|
703
|
+
id: profilingQuery.id,
|
|
425
704
|
callerCallId: input.callerCallId,
|
|
426
705
|
callerFetcherId: input.callerFetcherId,
|
|
427
706
|
callerLabel: input.callerLabel,
|
|
@@ -433,6 +712,12 @@ export default class Trace {
|
|
|
433
712
|
kind: input.kind,
|
|
434
713
|
model: input.model,
|
|
435
714
|
operation: input.operation,
|
|
715
|
+
fingerprint,
|
|
716
|
+
normalizedQuery,
|
|
717
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
718
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
719
|
+
serviceLabel: inferredServiceLabel,
|
|
720
|
+
connectedNamespace: input.connectedNamespace || channel?.connectedNamespace,
|
|
436
721
|
paramsJson: input.paramsJson,
|
|
437
722
|
paramsText: input.paramsText,
|
|
438
723
|
query: input.query.trim(),
|
|
@@ -447,7 +732,7 @@ export default class Trace {
|
|
|
447
732
|
return [...this.order]
|
|
448
733
|
.reverse()
|
|
449
734
|
.slice(0, limit)
|
|
450
|
-
.map((requestId) => this.
|
|
735
|
+
.map((requestId) => this.getRecord(requestId)?.trace)
|
|
451
736
|
.filter((trace): trace is TRequestTrace => trace !== undefined)
|
|
452
737
|
.map((trace) => ({
|
|
453
738
|
id: trace.id,
|
|
@@ -476,21 +761,25 @@ export default class Trace {
|
|
|
476
761
|
return [...this.order]
|
|
477
762
|
.reverse()
|
|
478
763
|
.slice(0, Math.max(1, limit))
|
|
479
|
-
.map((requestId) => this.
|
|
764
|
+
.map((requestId) => this.getRecord(requestId)?.trace)
|
|
480
765
|
.filter((trace): trace is TRequestTrace => trace !== undefined);
|
|
481
766
|
}
|
|
482
767
|
|
|
483
768
|
public getLatestRequest() {
|
|
484
769
|
const latestRequestId = this.order[this.order.length - 1];
|
|
485
|
-
return latestRequestId ? this.
|
|
770
|
+
return latestRequestId ? this.getRecord(latestRequestId)?.trace : undefined;
|
|
486
771
|
}
|
|
487
772
|
|
|
488
773
|
public getRequest(requestId: string) {
|
|
489
|
-
return this.
|
|
774
|
+
return this.getRecord(requestId)?.trace;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
public getProfiling(requestId: string) {
|
|
778
|
+
return this.getRecord(requestId)?.profiling;
|
|
490
779
|
}
|
|
491
780
|
|
|
492
781
|
public exportRequest(requestId: string, filepath?: string) {
|
|
493
|
-
const trace = this.
|
|
782
|
+
const trace = this.getRecord(requestId)?.trace;
|
|
494
783
|
if (!trace) throw new Error(`Trace ${requestId} was not found.`);
|
|
495
784
|
|
|
496
785
|
const outputFilepath =
|
|
@@ -505,6 +794,15 @@ export default class Trace {
|
|
|
505
794
|
return outputFilepath;
|
|
506
795
|
}
|
|
507
796
|
|
|
797
|
+
public releaseRequest(requestId: string) {
|
|
798
|
+
const record = this.getRecord(requestId);
|
|
799
|
+
if (!record) return;
|
|
800
|
+
if (record.trace) return;
|
|
801
|
+
|
|
802
|
+
this.requests.delete(requestId);
|
|
803
|
+
this.activeMeasurements.delete(requestId);
|
|
804
|
+
}
|
|
805
|
+
|
|
508
806
|
private trimRequestBuffer() {
|
|
509
807
|
const overflow = this.order.length - this.config.requestsLimit;
|
|
510
808
|
if (overflow <= 0) return;
|
|
@@ -186,6 +186,6 @@ export default class DevDiagnosticsRegistry<TApplication extends Application = A
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
public perfRequest(requestIdOrPath: string): TPerfRequestResponse {
|
|
189
|
-
return { request: resolvePerfRequest(this.readPerfRequests(), requestIdOrPath) };
|
|
189
|
+
return { request: resolvePerfRequest(this.readPerfRequests(), requestIdOrPath, this.readManifest()) };
|
|
190
190
|
}
|
|
191
191
|
}
|