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.
- package/AGENTS.md +51 -93
- package/README.md +44 -1
- package/agents/framework/AGENTS.md +155 -788
- package/agents/project/AGENTS.md +81 -110
- 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/commands/dev.ts +1 -0
- package/cli/commands/trace.ts +210 -0
- package/cli/compiler/client/index.ts +30 -8
- package/cli/compiler/server/index.ts +28 -6
- package/cli/paths.ts +16 -1
- package/cli/presentation/commands.ts +23 -1
- package/cli/presentation/devSession.ts +5 -0
- package/cli/runtime/commands.ts +31 -0
- package/common/dev/requestTrace.ts +81 -0
- package/docs/request-tracing.md +115 -0
- package/package.json +1 -1
- package/server/app/container/config.ts +15 -0
- package/server/app/container/index.ts +3 -0
- package/server/app/container/trace/index.ts +284 -0
- package/server/services/prisma/index.ts +61 -5
- package/server/services/router/http/index.ts +40 -0
- package/server/services/router/index.ts +159 -6
- package/server/services/router/response/index.ts +80 -7
- package/server/services/router/response/page/document.tsx +16 -0
- package/server/services/router/response/page/index.tsx +27 -1
- 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,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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|