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.
Files changed (84) hide show
  1. package/.codex/environments/environment.toml +11 -0
  2. package/AGENTS.md +27 -11
  3. package/README.md +30 -11
  4. package/agents/project/AGENTS.md +172 -123
  5. package/agents/project/CODING_STYLE.md +1 -1
  6. package/agents/project/app-root/AGENTS.md +16 -0
  7. package/agents/project/client/AGENTS.md +5 -5
  8. package/agents/project/client/pages/AGENTS.md +13 -13
  9. package/agents/project/diagnostics.md +19 -10
  10. package/agents/project/optimizations.md +5 -6
  11. package/agents/project/root/AGENTS.md +297 -0
  12. package/agents/project/server/routes/AGENTS.md +2 -2
  13. package/agents/project/server/services/AGENTS.md +4 -2
  14. package/agents/project/tests/AGENTS.md +9 -2
  15. package/cli/app/index.ts +31 -7
  16. package/cli/commands/configure.ts +226 -0
  17. package/cli/commands/dev.ts +0 -2
  18. package/cli/commands/diagnose.ts +33 -1
  19. package/cli/commands/explain.ts +1 -1
  20. package/cli/commands/migrate.ts +51 -0
  21. package/cli/commands/orient.ts +169 -0
  22. package/cli/commands/perf.ts +8 -1
  23. package/cli/commands/verify.ts +1003 -49
  24. package/cli/compiler/artifacts/manifest.ts +4 -4
  25. package/cli/compiler/artifacts/routing.ts +2 -2
  26. package/cli/compiler/artifacts/services.ts +12 -3
  27. package/cli/compiler/client/index.ts +65 -19
  28. package/cli/compiler/common/files/style.ts +47 -2
  29. package/cli/compiler/common/generatedRouteModules.ts +31 -38
  30. package/cli/compiler/common/index.ts +10 -0
  31. package/cli/compiler/common/proteumManifest.ts +1 -0
  32. package/cli/compiler/server/index.ts +34 -9
  33. package/cli/context.ts +6 -1
  34. package/cli/index.ts +7 -8
  35. package/cli/migrate/pageContract.ts +516 -0
  36. package/cli/paths.ts +47 -6
  37. package/cli/presentation/commands.ts +100 -10
  38. package/cli/presentation/devSession.ts +4 -6
  39. package/cli/presentation/help.ts +2 -2
  40. package/cli/presentation/ink.ts +10 -5
  41. package/cli/presentation/welcome.ts +2 -4
  42. package/cli/runtime/commands.ts +94 -1
  43. package/cli/scaffold/index.ts +2 -2
  44. package/cli/scaffold/templates.ts +4 -2
  45. package/cli/utils/agents.ts +273 -58
  46. package/client/dev/profiler/index.tsx +3 -2
  47. package/client/router.ts +10 -2
  48. package/client/services/router/index.tsx +6 -22
  49. package/common/dev/connect.ts +20 -4
  50. package/common/dev/console.ts +7 -0
  51. package/common/dev/contractsDoctor.ts +354 -0
  52. package/common/dev/diagnostics.ts +10 -7
  53. package/common/dev/inspection.ts +830 -38
  54. package/common/dev/performance.ts +19 -5
  55. package/common/dev/profiler.ts +1 -0
  56. package/common/dev/proteumManifest.ts +5 -4
  57. package/common/dev/requestTrace.ts +78 -1
  58. package/common/env/proteumEnv.ts +10 -3
  59. package/common/router/contracts.ts +8 -11
  60. package/common/router/index.ts +2 -2
  61. package/common/router/pageData.ts +72 -0
  62. package/common/router/register.ts +10 -46
  63. package/common/router/response/page.ts +28 -16
  64. package/docs/assets/unique-domains-chip.png +0 -0
  65. package/docs/dev-sessions.md +8 -4
  66. package/docs/diagnostics.md +77 -11
  67. package/docs/migrate-from-2.1.3.md +388 -0
  68. package/docs/request-tracing.md +42 -9
  69. package/package.json +6 -1
  70. package/scripts/update-codex-agents.ts +2 -2
  71. package/server/app/container/console/index.ts +11 -1
  72. package/server/app/container/trace/index.ts +370 -72
  73. package/server/app/devDiagnostics.ts +1 -1
  74. package/server/app/index.ts +5 -1
  75. package/server/services/auth/index.ts +9 -0
  76. package/server/services/prisma/index.ts +15 -12
  77. package/server/services/router/http/index.ts +1 -1
  78. package/server/services/router/index.ts +105 -23
  79. package/server/services/router/request/api.ts +7 -1
  80. package/server/services/router/request/index.ts +2 -1
  81. package/server/services/router/response/index.ts +8 -28
  82. package/types/global/vendors.d.ts +12 -0
  83. package/types/vendors.d.ts +12 -0
  84. 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 = (value: unknown, keyPath: string[], seen: WeakSet<object>): unknown => {
46
- if (isSensitiveKeyPath(keyPath)) return `[redacted: Sensitive key ${keyPath[keyPath.length - 1] || 'value'}]`;
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) return Array.from(value.entries()).map(([entryKey, entryValue], index) =>
57
- serializeJsonValue([entryKey, entryValue], [...keyPath, `[${index}]`], seen),
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) => serializeJsonValue(entryValue, [...keyPath, `[${index}]`], seen));
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) => serializeJsonValue(value, [key], new WeakSet<object>());
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, TRequestTrace>();
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 isEnabled() {
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
- if (!this.isEnabled()) return;
217
-
218
- const capture = this.armedCapture ?? this.config.capture;
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
- startedAt: nowIso(),
231
- droppedEvents: 0,
232
- requestDataJson: serializeCaptureValue(input.data, 'requestData'),
233
- calls: [],
234
- sqlQueries: [],
235
- events: [],
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
- this.requests.set(trace.id, trace);
239
- this.activeMeasurements.set(trace.id, { cpu: process.cpuUsage(), memory: snapshotMemory() });
240
- this.order.push(trace.id);
241
- this.trimRequestBuffer();
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
- this.record(trace.id, 'request.start', { method: input.method, path: input.path, url: input.url, headers: input.headers, data: input.data });
418
+ return profiling;
244
419
  }
245
420
 
246
421
  public setRequestUser(requestId: string, user?: string) {
247
- const trace = this.requests.get(requestId);
248
- if (!trace) return;
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.requests.get(requestId)?.capture;
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 trace = this.requests.get(requestId);
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 trace = this.requests.get(requestId);
287
- if (!trace) return;
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 = nowIso();
316
- trace.durationMs = Math.max(0, Date.parse(trace.finishedAt) - Date.parse(trace.startedAt));
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 trace = this.requests.get(requestId);
339
- if (!trace) return undefined;
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: `${requestId}:call:${trace.calls.length}`,
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
- startedAt: nowIso(),
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 call.id;
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 trace = this.requests.get(requestId);
375
- const call = trace?.calls.find((candidate) => candidate.id === callId);
376
- if (!trace || !call) return;
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 = nowIso();
379
- call.durationMs = Math.max(0, Date.parse(call.finishedAt) - Date.parse(call.startedAt));
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.requests.get(requestId);
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 trace = this.requests.get(requestId);
415
- if (!trace) return;
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: `${requestId}:sql:${trace.sqlQueries.length}`,
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.requests.get(requestId))
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.requests.get(requestId))
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.requests.get(latestRequestId) : undefined;
770
+ return latestRequestId ? this.getRecord(latestRequestId)?.trace : undefined;
486
771
  }
487
772
 
488
773
  public getRequest(requestId: string) {
489
- return this.requests.get(requestId);
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.requests.get(requestId);
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
  }