proteum 2.1.0-2 → 2.1.0-3

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 (37) hide show
  1. package/AGENTS.md +51 -93
  2. package/README.md +44 -1
  3. package/agents/framework/AGENTS.md +155 -788
  4. package/agents/project/AGENTS.md +81 -110
  5. package/agents/project/client/AGENTS.md +22 -93
  6. package/agents/project/client/pages/AGENTS.md +24 -26
  7. package/agents/project/server/routes/AGENTS.md +10 -8
  8. package/agents/project/server/services/AGENTS.md +22 -159
  9. package/agents/project/tests/AGENTS.md +11 -8
  10. package/cli/commands/dev.ts +1 -0
  11. package/cli/commands/trace.ts +210 -0
  12. package/cli/compiler/client/index.ts +30 -8
  13. package/cli/compiler/server/index.ts +28 -6
  14. package/cli/paths.ts +16 -1
  15. package/cli/presentation/commands.ts +23 -1
  16. package/cli/presentation/devSession.ts +5 -0
  17. package/cli/runtime/commands.ts +31 -0
  18. package/common/dev/requestTrace.ts +81 -0
  19. package/docs/request-tracing.md +115 -0
  20. package/package.json +1 -1
  21. package/server/app/container/config.ts +15 -0
  22. package/server/app/container/index.ts +3 -0
  23. package/server/app/container/trace/index.ts +284 -0
  24. package/server/services/prisma/index.ts +61 -5
  25. package/server/services/router/http/index.ts +40 -0
  26. package/server/services/router/index.ts +159 -6
  27. package/server/services/router/response/index.ts +80 -7
  28. package/server/services/router/response/page/document.tsx +16 -0
  29. package/server/services/router/response/page/index.tsx +27 -1
  30. package/Rte.zip +0 -0
  31. package/agents/project/agents.md.zip +0 -0
  32. package/doc/TODO.md +0 -71
  33. package/doc/front/router.md +0 -27
  34. package/doc/workspace/workspace.png +0 -0
  35. package/doc/workspace/workspace2.png +0 -0
  36. package/doc/workspace/workspace_26.01.22.png +0 -0
  37. package/server/services/router/http/session.ts.old +0 -40
@@ -0,0 +1,115 @@
1
+ # Request Tracing
2
+
3
+ Proteum ships with a dev-only in-memory request trace buffer so routing, controller execution, SSR, and render behavior can be inspected without attaching a debugger or scattering temporary logs through the runtime.
4
+
5
+ ## Scope
6
+
7
+ - tracing is available only when the app runs with `profile: dev`
8
+ - traces are exposed through `proteum trace` and the dev-only `__proteum/trace` HTTP endpoints
9
+ - production requests are not traced by this feature
10
+
11
+ ## Main Commands
12
+
13
+ ```bash
14
+ proteum trace requests
15
+ proteum trace latest
16
+ proteum trace show <requestId>
17
+ proteum trace arm --capture deep
18
+ proteum trace export <requestId>
19
+ proteum trace latest --url http://127.0.0.1:3010
20
+ ```
21
+
22
+ Typical debugging flow:
23
+
24
+ ```bash
25
+ proteum trace arm --capture deep --port 3103
26
+ # reproduce the failing request once
27
+ proteum trace requests --port 3103
28
+ proteum trace show <requestId> --port 3103
29
+ ```
30
+
31
+ Use `--url http://host:port` when the dev server is reachable on a non-standard host and `--port` is not enough.
32
+
33
+ ## What Gets Recorded
34
+
35
+ Depending on capture mode, traces can include:
36
+
37
+ - request start, finish, user identity, status code, and duration
38
+ - direct controller route matches
39
+ - route resolution start, match, and deep-mode skip reasons
40
+ - controller start and result shape
41
+ - created router/context keys
42
+ - setup output keys and page data summaries
43
+ - SSR payload shape and serialized byte size
44
+ - render start/end timings and document output sizes
45
+ - normalized request errors
46
+
47
+ ## Capture Modes
48
+
49
+ - `summary`: smallest capture, focused on request lifecycle and high-signal events
50
+ - `resolve`: adds route matching, controller, setup, and context milestones
51
+ - `deep`: adds route skip reasons and deeper summarized payload inspection for one request
52
+
53
+ Use `deep` selectively. It is for one-off investigation, not continuous capture.
54
+
55
+ ## Configuration
56
+
57
+ Add trace settings in `env.yaml`:
58
+
59
+ ```yaml
60
+ trace:
61
+ enable: true
62
+ requestsLimit: 200
63
+ eventsLimit: 800
64
+ capture: resolve
65
+ persistOnError: true
66
+ ```
67
+
68
+ Notes:
69
+
70
+ - `enable` and `persistOnError` still remain dev-only in the current runtime
71
+ - `capture` defaults to `resolve`
72
+ - `requestsLimit` defaults to `200`
73
+ - `eventsLimit` defaults to `800`
74
+
75
+ ## Memory Model
76
+
77
+ Traces are kept in memory per Node process.
78
+
79
+ - requests are stored in a ring buffer capped by `requestsLimit`
80
+ - the oldest request traces are evicted first
81
+ - each request is capped by `eventsLimit`
82
+ - once the event cap is reached, extra events are dropped and counted in `droppedEvents`
83
+ - payloads are summarized rather than stored as raw objects
84
+
85
+ Current summarization rules:
86
+
87
+ - arrays keep at most 10 sampled items
88
+ - objects keep at most 20 keys per level
89
+ - deep capture stops at depth 3
90
+ - long strings are truncated
91
+
92
+ ## Redaction
93
+
94
+ Sensitive values are redacted before they enter the trace store.
95
+
96
+ This includes keys such as:
97
+
98
+ - `cookie`
99
+ - `authorization`
100
+ - `password`
101
+ - token-like fields such as `accessToken`, `refreshToken`, `apiKey`, `jwt`, and similar names
102
+ - `rawBody`
103
+
104
+ The goal is to make traces useful for debugging without turning the dev server into a secret dump.
105
+
106
+ ## Dev HTTP Endpoints
107
+
108
+ These endpoints back the CLI:
109
+
110
+ - `GET /__proteum/trace/requests`
111
+ - `GET /__proteum/trace/latest`
112
+ - `GET /__proteum/trace/requests/:id`
113
+ - `POST /__proteum/trace/arm`
114
+
115
+ The CLI should be the primary interface. Use the HTTP endpoints when you need direct machine access from another local dev tool.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "LLM-first Opinionated Typescript Framework for web applications.",
4
- "version": "2.1.0-2",
4
+ "version": "2.1.0-3",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -67,6 +67,13 @@ export type TEnvConfig = {
67
67
 
68
68
  router: { port: number; domains: TDomainsList };
69
69
  console: { enable: boolean; debug: boolean; bufferLimit: number; level: TLogProfile };
70
+ trace: {
71
+ enable: boolean;
72
+ requestsLimit: number;
73
+ eventsLimit: number;
74
+ capture: 'summary' | 'resolve' | 'deep';
75
+ persistOnError: boolean;
76
+ };
70
77
  };
71
78
 
72
79
  type AppIdentityConfig = {
@@ -126,9 +133,17 @@ export default class ConfigParser {
126
133
  const envFileName = this.appDir + '/env.yaml';
127
134
  const envFile = this.loadYaml(envFileName);
128
135
  const routerPortOverride = getRouterPortOverride();
136
+ const traceConfig = envFile.trace || {};
129
137
  return {
130
138
  ...envFile,
131
139
  router: routerPortOverride === undefined ? envFile.router : { ...envFile.router, port: routerPortOverride },
140
+ trace: {
141
+ enable: traceConfig.enable ?? envFile.profile === 'dev',
142
+ requestsLimit: traceConfig.requestsLimit ?? 200,
143
+ eventsLimit: traceConfig.eventsLimit ?? 800,
144
+ capture: traceConfig.capture ?? 'resolve',
145
+ persistOnError: traceConfig.persistOnError ?? envFile.profile === 'dev',
146
+ },
132
147
  version: BUILD_DATE,
133
148
  };
134
149
  }
@@ -15,6 +15,7 @@ import type { StartedServicesIndex } from '../service';
15
15
  import Services, { ServicesContainer } from '../service/container';
16
16
  import ConfigParser, { TEnvConfig } from './config';
17
17
  import Console from './console';
18
+ import Trace from './trace';
18
19
  import type ServerRequest from '@server/services/router/request';
19
20
 
20
21
  /*----------------------------------
@@ -29,6 +30,7 @@ export class ApplicationContainer<TServicesIndex extends StartedServicesIndex =
29
30
  public Environment: TEnvConfig;
30
31
  public Identity: Config.Identity;
31
32
  public Console: Console;
33
+ public Trace: Trace;
32
34
 
33
35
  public application?: Application;
34
36
 
@@ -52,6 +54,7 @@ export class ApplicationContainer<TServicesIndex extends StartedServicesIndex =
52
54
  this.Environment = configParser.env();
53
55
  this.Identity = configParser.identity();
54
56
  this.Console = new Console(this, this.Environment.console);
57
+ this.Trace = new Trace(this, this.Environment.trace);
55
58
  }
56
59
 
57
60
  /*----------------------------------
@@ -0,0 +1,284 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import type ApplicationContainer from '..';
5
+ import {
6
+ traceCaptureModes,
7
+ type TTraceCaptureMode,
8
+ type TTraceEvent,
9
+ type TTraceEventType,
10
+ type TTraceSummaryValue,
11
+ type TRequestTrace,
12
+ type TRequestTraceListItem,
13
+ } from '@common/dev/requestTrace';
14
+
15
+ export type Config = {
16
+ enable: boolean;
17
+ requestsLimit: number;
18
+ eventsLimit: number;
19
+ capture: TTraceCaptureMode;
20
+ persistOnError: boolean;
21
+ };
22
+
23
+ type TTraceInspectable = object | PrimitiveValue | bigint | symbol | null | undefined | (() => void);
24
+ type TTraceDetails = { [key: string]: TTraceInspectable };
25
+
26
+ const capturePriority: Record<TTraceCaptureMode, number> = { summary: 0, resolve: 1, deep: 2 };
27
+ const sensitiveKeyPattern =
28
+ /(^|\.)(authorization|cookie|set-cookie|password|pass|pwd|secret|token|refreshToken|accessToken|apiKey|apiSecret|secretAccessKey|accessKeyId|privateKey|session|jwt|rawBody)$/i;
29
+ const maxStringLength = 240;
30
+
31
+ const isTraceCaptureMode = (value: string): value is TTraceCaptureMode =>
32
+ traceCaptureModes.includes(value as TTraceCaptureMode);
33
+
34
+ const isSensitiveKeyPath = (keyPath: string[]) => sensitiveKeyPattern.test(keyPath.join('.'));
35
+
36
+ const summarizeString = (value: string) =>
37
+ value.length <= maxStringLength ? value : `${value.slice(0, maxStringLength)}…`;
38
+
39
+ const summarizeError = (error: Error): TTraceSummaryValue => ({
40
+ kind: 'error',
41
+ name: error.name,
42
+ message: error.message,
43
+ stack: error.stack?.split('\n').slice(0, 5).join('\n'),
44
+ });
45
+
46
+ const summarizeValue = (
47
+ value: TTraceInspectable,
48
+ depth: number,
49
+ seen: WeakSet<object>,
50
+ keyPath: string[],
51
+ ): TTraceSummaryValue => {
52
+ if (isSensitiveKeyPath(keyPath)) return { kind: 'redacted', reason: `Sensitive key ${keyPath[keyPath.length - 1] || 'value'}` };
53
+ if (value === undefined) return { kind: 'undefined' };
54
+ if (value === null) return null;
55
+
56
+ const primitiveType = typeof value;
57
+ if (primitiveType === 'string') return summarizeString(value);
58
+ if (primitiveType === 'number' || primitiveType === 'boolean') return value;
59
+ if (primitiveType === 'bigint') return { kind: 'bigint', value: value.toString() };
60
+ if (primitiveType === 'symbol') return { kind: 'symbol', value: value.toString() };
61
+ if (primitiveType === 'function') return { kind: 'function', name: value.name || 'anonymous' };
62
+
63
+ if (value instanceof Date) return { kind: 'date', value: value.toISOString() };
64
+ if (value instanceof Error) return summarizeError(value);
65
+ if (Buffer.isBuffer(value)) return { kind: 'buffer', byteLength: value.byteLength };
66
+ if (value instanceof Map) return { kind: 'map', size: value.size };
67
+ if (value instanceof Set) return { kind: 'set', size: value.size };
68
+
69
+ if (seen.has(value)) {
70
+ return {
71
+ kind: 'object',
72
+ constructorName: value.constructor?.name || 'Object',
73
+ keys: [],
74
+ entries: {},
75
+ truncated: true,
76
+ };
77
+ }
78
+
79
+ seen.add(value);
80
+
81
+ if (Array.isArray(value)) {
82
+ if (depth <= 0) return { kind: 'array', length: value.length, items: [], truncated: value.length > 0 };
83
+
84
+ const items = value
85
+ .slice(0, 10)
86
+ .map((item, index) => summarizeValue(item as TTraceInspectable, depth - 1, seen, [...keyPath, `[${index}]`]));
87
+ return { kind: 'array', length: value.length, items, truncated: value.length > items.length };
88
+ }
89
+
90
+ const constructorName = value.constructor?.name || 'Object';
91
+ const keys = Object.keys(value);
92
+ if (depth <= 0) {
93
+ return { kind: 'object', constructorName, keys, entries: {}, truncated: keys.length > 0 };
94
+ }
95
+
96
+ const entries: { [key: string]: TTraceSummaryValue } = {};
97
+ for (const key of keys.slice(0, 20)) {
98
+ const record = value as Record<string, TTraceInspectable>;
99
+ entries[key] = summarizeValue(record[key], depth - 1, seen, [...keyPath, key]);
100
+ }
101
+
102
+ return { kind: 'object', constructorName, keys, entries, truncated: keys.length > Object.keys(entries).length };
103
+ };
104
+
105
+ const summarizeDetails = (details: TTraceDetails, capture: TTraceCaptureMode) => {
106
+ const depth = capture === 'deep' ? 3 : 1;
107
+ const summarized: { [key: string]: TTraceSummaryValue } = {};
108
+
109
+ for (const key of Object.keys(details)) {
110
+ summarized[key] = summarizeValue(details[key], depth, new WeakSet<object>(), [key]);
111
+ }
112
+
113
+ return summarized;
114
+ };
115
+
116
+ const nowIso = () => new Date().toISOString();
117
+
118
+ export default class Trace {
119
+ private requests = new Map<string, TRequestTrace>();
120
+ private order: string[] = [];
121
+ private armedCapture?: TTraceCaptureMode;
122
+
123
+ public constructor(
124
+ private container: typeof ApplicationContainer,
125
+ private config: Config,
126
+ ) {}
127
+
128
+ public isEnabled() {
129
+ return this.config.enable && this.container.Environment.profile === 'dev';
130
+ }
131
+
132
+ public armNextRequest(capture: string) {
133
+ if (!isTraceCaptureMode(capture)) {
134
+ throw new Error(`Unsupported trace capture mode "${capture}". Expected one of: ${traceCaptureModes.join(', ')}.`);
135
+ }
136
+
137
+ this.armedCapture = capture;
138
+ return capture;
139
+ }
140
+
141
+ public startRequest(input: { id: string; method: string; path: string; url: string; headers: object; data: object }) {
142
+ if (!this.isEnabled()) return;
143
+
144
+ const capture = this.armedCapture ?? this.config.capture;
145
+ this.armedCapture = undefined;
146
+
147
+ const trace: TRequestTrace = {
148
+ id: input.id,
149
+ method: input.method,
150
+ path: input.path,
151
+ url: input.url,
152
+ capture,
153
+ startedAt: nowIso(),
154
+ droppedEvents: 0,
155
+ events: [],
156
+ };
157
+
158
+ this.requests.set(trace.id, trace);
159
+ this.order.push(trace.id);
160
+ this.trimRequestBuffer();
161
+
162
+ this.record(trace.id, 'request.start', { method: input.method, path: input.path, url: input.url, headers: input.headers, data: input.data });
163
+ }
164
+
165
+ public setRequestUser(requestId: string, user?: string) {
166
+ const trace = this.requests.get(requestId);
167
+ if (!trace) return;
168
+
169
+ trace.user = user;
170
+ if (user) this.record(requestId, 'request.user', { user });
171
+ }
172
+
173
+ public getCapture(requestId: string) {
174
+ return this.requests.get(requestId)?.capture;
175
+ }
176
+
177
+ public shouldCapture(requestId: string, minimumCapture: TTraceCaptureMode) {
178
+ const capture = this.getCapture(requestId);
179
+ if (!capture) return false;
180
+
181
+ return capturePriority[capture] >= capturePriority[minimumCapture];
182
+ }
183
+
184
+ public record(requestId: string, type: TTraceEventType, details: TTraceDetails, minimumCapture: TTraceCaptureMode = 'summary') {
185
+ const trace = this.requests.get(requestId);
186
+ if (!trace || !this.shouldCapture(requestId, minimumCapture)) return;
187
+
188
+ if (trace.events.length >= this.config.eventsLimit) {
189
+ trace.droppedEvents++;
190
+ return;
191
+ }
192
+
193
+ const event: TTraceEvent = {
194
+ index: trace.events.length,
195
+ at: nowIso(),
196
+ elapsedMs: Math.max(0, Date.now() - Date.parse(trace.startedAt)),
197
+ type,
198
+ details: summarizeDetails(details, trace.capture),
199
+ };
200
+
201
+ trace.events.push(event);
202
+ }
203
+
204
+ public finishRequest(requestId: string, output: { statusCode: number; user?: string; errorMessage?: string }) {
205
+ const trace = this.requests.get(requestId);
206
+ if (!trace) return;
207
+
208
+ if (output.user) trace.user = output.user;
209
+ trace.statusCode = output.statusCode;
210
+ trace.errorMessage = output.errorMessage;
211
+
212
+ this.record(
213
+ requestId,
214
+ 'request.finish',
215
+ { statusCode: output.statusCode, user: output.user || '', errorMessage: output.errorMessage || '' },
216
+ 'summary',
217
+ );
218
+
219
+ trace.finishedAt = nowIso();
220
+ trace.durationMs = Math.max(0, Date.parse(trace.finishedAt) - Date.parse(trace.startedAt));
221
+
222
+ if (this.config.persistOnError && trace.statusCode >= 500) {
223
+ trace.persistedFilepath = this.exportRequest(requestId);
224
+ }
225
+ }
226
+
227
+ public listRequests(limit = 20): TRequestTraceListItem[] {
228
+ return [...this.order]
229
+ .reverse()
230
+ .slice(0, limit)
231
+ .map((requestId) => this.requests.get(requestId))
232
+ .filter((trace): trace is TRequestTrace => trace !== undefined)
233
+ .map((trace) => ({
234
+ id: trace.id,
235
+ method: trace.method,
236
+ path: trace.path,
237
+ url: trace.url,
238
+ capture: trace.capture,
239
+ startedAt: trace.startedAt,
240
+ finishedAt: trace.finishedAt,
241
+ durationMs: trace.durationMs,
242
+ statusCode: trace.statusCode,
243
+ user: trace.user,
244
+ droppedEvents: trace.droppedEvents,
245
+ persistedFilepath: trace.persistedFilepath,
246
+ errorMessage: trace.errorMessage,
247
+ eventCount: trace.events.length,
248
+ }));
249
+ }
250
+
251
+ public getLatestRequest() {
252
+ const latestRequestId = this.order[this.order.length - 1];
253
+ return latestRequestId ? this.requests.get(latestRequestId) : undefined;
254
+ }
255
+
256
+ public getRequest(requestId: string) {
257
+ return this.requests.get(requestId);
258
+ }
259
+
260
+ public exportRequest(requestId: string, filepath?: string) {
261
+ const trace = this.requests.get(requestId);
262
+ if (!trace) throw new Error(`Trace ${requestId} was not found.`);
263
+
264
+ const outputFilepath =
265
+ filepath ||
266
+ path.join(this.container.path.var, 'traces', trace.startedAt.slice(0, 10), `${trace.id}.json`);
267
+
268
+ fs.ensureDirSync(path.dirname(outputFilepath));
269
+ fs.writeJSONSync(outputFilepath, trace, { spaces: 2 });
270
+
271
+ trace.persistedFilepath = outputFilepath;
272
+
273
+ return outputFilepath;
274
+ }
275
+
276
+ private trimRequestBuffer() {
277
+ const overflow = this.order.length - this.config.requestsLimit;
278
+ if (overflow <= 0) return;
279
+
280
+ for (const requestId of this.order.splice(0, overflow)) {
281
+ this.requests.delete(requestId);
282
+ }
283
+ }
284
+ }
@@ -23,6 +23,62 @@ import { NotFound } from '@common/errors';
23
23
 
24
24
  export type SqlQuery = ReturnType<ModelsManager['SQL']>;
25
25
 
26
+ type DecimalLike = {
27
+ constructor?: { name?: string };
28
+ equals: (value: number) => boolean;
29
+ toNumber: () => number;
30
+ toString: () => string;
31
+ };
32
+
33
+ /*----------------------------------
34
+ - HELPERS
35
+ ----------------------------------*/
36
+
37
+ const isDecimalLike = (value: object): value is DecimalLike =>
38
+ 'constructor' in value &&
39
+ 'equals' in value &&
40
+ 'toNumber' in value &&
41
+ 'toString' in value &&
42
+ typeof value.constructor === 'function' &&
43
+ value.constructor.name === 'Decimal' &&
44
+ typeof value.equals === 'function' &&
45
+ typeof value.toNumber === 'function' &&
46
+ typeof value.toString === 'function';
47
+
48
+ const isPlainObject = (value: object) => {
49
+ const prototype = Object.getPrototypeOf(value);
50
+ return prototype === Object.prototype || prototype === null;
51
+ };
52
+
53
+ const normalizeBigInt = (value: bigint) => {
54
+ const number = Number(value);
55
+ return Number.isSafeInteger(number) ? number : value.toString();
56
+ };
57
+
58
+ const normalizeDecimal = (value: DecimalLike) => {
59
+ const number = value.toNumber();
60
+ return Number.isFinite(number) && value.equals(number) ? number : value.toString();
61
+ };
62
+
63
+ const normalizeSqlScalar = (value: bigint | DecimalLike) =>
64
+ typeof value === 'bigint' ? normalizeBigInt(value) : normalizeDecimal(value);
65
+
66
+ const normalizeSqlResult = <T>(value: T): T => {
67
+ if (typeof value === 'bigint') return normalizeSqlScalar(value) as T;
68
+
69
+ if (Array.isArray(value)) return value.map((item) => normalizeSqlResult(item)) as T;
70
+
71
+ if (value === null || value === undefined || typeof value !== 'object' || value instanceof Date) return value;
72
+
73
+ if (isDecimalLike(value)) return normalizeSqlScalar(value) as T;
74
+
75
+ if (!isPlainObject(value)) return value;
76
+
77
+ return Object.fromEntries(
78
+ Object.entries(value).map(([key, nestedValue]) => [key, normalizeSqlResult(nestedValue)]),
79
+ ) as T;
80
+ };
81
+
26
82
  /*----------------------------------
27
83
  - SERVICE CONFIG
28
84
  ----------------------------------*/
@@ -35,8 +91,7 @@ export type Services = {};
35
91
 
36
92
  // Fix: Do not know how to serialize a BigInt
37
93
  BigInt.prototype.toJSON = function () {
38
- const int = Number.parseInt(this.toString());
39
- return int ?? this.toString();
94
+ return normalizeBigInt(this.valueOf());
40
95
  };
41
96
 
42
97
  /*----------------------------------
@@ -80,9 +135,10 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
80
135
  public SQL<TRowData extends {} | number | string>(strings: TemplateStringsArray, ...data: any[]) {
81
136
  const string = this.string(strings, ...data);
82
137
 
83
- const query = () => {
84
- return this.client.$queryRawUnsafe(string) as Promise<TRowData[]>;
85
- };
138
+ const query = () =>
139
+ this.client.$queryRawUnsafe(string).then((resultatRequetes) => normalizeSqlResult(resultatRequetes)) as Promise<
140
+ TRowData[]
141
+ >;
86
142
 
87
143
  query.all = query;
88
144
  query.value = <TValue extends any = number>() =>
@@ -206,6 +206,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
206
206
  }),
207
207
  );
208
208
 
209
+ this.registerDevTraceRoutes(routes);
209
210
  routes.use(routeRequest);
210
211
 
211
212
  /*----------------------------------
@@ -227,4 +228,43 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
227
228
  public async cleanup() {
228
229
  this.http.close();
229
230
  }
231
+
232
+ private registerDevTraceRoutes(routes: express.Express) {
233
+ if (!this.app.container.Trace.isEnabled()) return;
234
+
235
+ routes.get('/__proteum/trace/requests', (req, res) => {
236
+ const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
237
+ const parsedLimit = typeof rawLimit === 'string' ? Number.parseInt(rawLimit, 10) : NaN;
238
+ const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 20;
239
+
240
+ res.json({ requests: this.app.container.Trace.listRequests(limit) });
241
+ });
242
+
243
+ routes.get('/__proteum/trace/latest', (_req, res) => {
244
+ const request = this.app.container.Trace.getLatestRequest();
245
+ if (!request) {
246
+ res.status(404).json({ error: 'No request trace is available yet.' });
247
+ return;
248
+ }
249
+
250
+ res.json({ request });
251
+ });
252
+
253
+ routes.get('/__proteum/trace/requests/:id', (req, res) => {
254
+ const request = this.app.container.Trace.getRequest(req.params.id);
255
+ if (!request) {
256
+ res.status(404).json({ error: `Trace ${req.params.id} was not found.` });
257
+ return;
258
+ }
259
+
260
+ res.json({ request });
261
+ });
262
+
263
+ routes.post('/__proteum/trace/arm', (req, res) => {
264
+ const rawCapture = typeof req.body.capture === 'string' ? req.body.capture : 'deep';
265
+ const capture = this.app.container.Trace.armNextRequest(rawCapture);
266
+
267
+ res.json({ armed: true, capture });
268
+ });
269
+ }
230
270
  }