proteum 2.1.2 → 2.1.6
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 +22 -14
- package/README.md +112 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +26 -11
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +17 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +90 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +136 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +193 -6
- package/cli/scaffold/index.ts +14 -25
- package/cli/scaffold/templates.ts +41 -27
- package/cli/utils/agents.ts +4 -2
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2798 -417
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +56 -1
- package/common/dev/session.ts +24 -0
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +3 -1
- package/server/app/container/trace/index.ts +153 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +352 -0
- package/server/services/router/index.ts +50 -47
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +24 -1
- package/server/services/router/response/page/document.tsx +5 -0
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- package/server/services/security/encrypt/aes/service.json +0 -6
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import got from 'got';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { UsageError } from 'clipanion';
|
|
5
|
+
|
|
6
|
+
import cli from '..';
|
|
7
|
+
import type { TDevSessionErrorResponse, TDevSessionStartResponse } from '../../common/dev/session';
|
|
8
|
+
|
|
9
|
+
const localSessionResultMarker = '__PROTEUM_SESSION_RESULT__';
|
|
10
|
+
|
|
11
|
+
type TResolvedSessionOutput = {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
user: TDevSessionStartResponse['user'];
|
|
14
|
+
session: TDevSessionStartResponse['session'];
|
|
15
|
+
browserCookie: string;
|
|
16
|
+
curlCookieHeader: string;
|
|
17
|
+
playwright: {
|
|
18
|
+
cookies: Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
url: string;
|
|
22
|
+
expires: number;
|
|
23
|
+
httpOnly: boolean;
|
|
24
|
+
secure: boolean;
|
|
25
|
+
sameSite: 'Lax';
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
|
|
31
|
+
|
|
32
|
+
const getRouterPortFromManifest = () => {
|
|
33
|
+
const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
|
|
34
|
+
if (!require('fs-extra').existsSync(manifestFilepath)) return undefined;
|
|
35
|
+
|
|
36
|
+
const manifest = require('fs-extra').readJsonSync(manifestFilepath, { throws: false }) as
|
|
37
|
+
| { env?: { resolved?: { routerPort?: number } } }
|
|
38
|
+
| undefined;
|
|
39
|
+
const port = manifest?.env?.resolved?.routerPort;
|
|
40
|
+
|
|
41
|
+
if (typeof port !== 'number' || port <= 0) return undefined;
|
|
42
|
+
|
|
43
|
+
return String(port);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getRouterPort = () => {
|
|
47
|
+
const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
|
|
48
|
+
if (overridePort) return overridePort;
|
|
49
|
+
|
|
50
|
+
const envPort = process.env.PORT?.trim();
|
|
51
|
+
if (envPort) return envPort;
|
|
52
|
+
|
|
53
|
+
const manifestPort = getRouterPortFromManifest();
|
|
54
|
+
if (manifestPort) return manifestPort;
|
|
55
|
+
|
|
56
|
+
throw new UsageError(
|
|
57
|
+
`Could not determine the router port from PORT or .proteum/manifest.json in ${cli.args.workdir as string}. Pass --port or --url explicitly.`,
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getRouterBaseUrls = () => {
|
|
62
|
+
const explicitUrl = typeof cli.args.url === 'string' && cli.args.url ? cli.args.url.trim() : '';
|
|
63
|
+
if (explicitUrl) return [normalizeBaseUrl(explicitUrl)];
|
|
64
|
+
|
|
65
|
+
const port = getRouterPort();
|
|
66
|
+
return [...new Set([`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`])];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getSessionErrorMessage = (body: TDevSessionErrorResponse | object | string | undefined, statusCode: number) => {
|
|
70
|
+
if (typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string') {
|
|
71
|
+
return body.error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return `Session request failed with status ${statusCode}.`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const hasStructuredSessionError = (body: TDevSessionErrorResponse | object | string | undefined): body is TDevSessionErrorResponse =>
|
|
78
|
+
typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string';
|
|
79
|
+
|
|
80
|
+
const requestSession = async (email: string, role: string) => {
|
|
81
|
+
const attempts: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const baseUrl of getRouterBaseUrls()) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await got(`${baseUrl}/__proteum/session/start`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
json: role ? { email, role } : { email },
|
|
88
|
+
responseType: 'json',
|
|
89
|
+
throwHttpErrors: false,
|
|
90
|
+
retry: { limit: 0 },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (response.statusCode >= 400) {
|
|
94
|
+
if (response.statusCode === 404 && !hasStructuredSessionError(response.body as TDevSessionErrorResponse | object | string | undefined)) {
|
|
95
|
+
attempts.push(`${baseUrl}/__proteum/session/start: returned 404`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new UsageError(
|
|
100
|
+
getSessionErrorMessage(response.body as TDevSessionErrorResponse | object | string | undefined, response.statusCode),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
baseUrl,
|
|
106
|
+
response: response.body as TDevSessionStartResponse,
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof UsageError) throw error;
|
|
110
|
+
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
attempts.push(`${baseUrl}/__proteum/session/start: ${message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new UsageError(
|
|
117
|
+
[
|
|
118
|
+
'Could not reach the Proteum session server.',
|
|
119
|
+
...attempts.map((attempt) => `- ${attempt}`),
|
|
120
|
+
'Make sure the app is running with `proteum dev`, or omit --port/--url to run the session request locally.',
|
|
121
|
+
].join('\n'),
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const buildSessionOutput = ({
|
|
126
|
+
baseUrl,
|
|
127
|
+
response,
|
|
128
|
+
}: {
|
|
129
|
+
baseUrl: string;
|
|
130
|
+
response: TDevSessionStartResponse;
|
|
131
|
+
}): TResolvedSessionOutput => {
|
|
132
|
+
const expires = Math.floor(Date.parse(response.session.expiresAt) / 1000);
|
|
133
|
+
const secure = new URL(baseUrl).protocol === 'https:';
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
baseUrl,
|
|
137
|
+
user: response.user,
|
|
138
|
+
session: response.session,
|
|
139
|
+
browserCookie: `${response.session.cookieName}=${response.session.token}; Path=/`,
|
|
140
|
+
curlCookieHeader: `Cookie: ${response.session.cookieName}=${response.session.token}`,
|
|
141
|
+
playwright: {
|
|
142
|
+
cookies: [
|
|
143
|
+
{
|
|
144
|
+
name: response.session.cookieName,
|
|
145
|
+
value: response.session.token,
|
|
146
|
+
url: baseUrl,
|
|
147
|
+
expires,
|
|
148
|
+
httpOnly: false,
|
|
149
|
+
secure,
|
|
150
|
+
sameSite: 'Lax',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const printJson = (value: object) => {
|
|
158
|
+
console.log(JSON.stringify(value, null, 2));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const renderSession = (value: TResolvedSessionOutput) =>
|
|
162
|
+
[
|
|
163
|
+
`Session ${value.user.email}`,
|
|
164
|
+
`- baseUrl=${value.baseUrl}`,
|
|
165
|
+
`- roles=${value.user.roles.join(',')}`,
|
|
166
|
+
`- expiresAt=${value.session.expiresAt}`,
|
|
167
|
+
'Token',
|
|
168
|
+
value.session.token,
|
|
169
|
+
'Playwright',
|
|
170
|
+
JSON.stringify(value.playwright, null, 2),
|
|
171
|
+
'Browser Cookie',
|
|
172
|
+
value.browserCookie,
|
|
173
|
+
].join('\n');
|
|
174
|
+
|
|
175
|
+
const runLocalSession = async (email: string, role: string) => {
|
|
176
|
+
const runnerFilepath = path.join(cli.paths.core.root, 'cli', 'commands', 'sessionLocalRunner.js');
|
|
177
|
+
|
|
178
|
+
return await new Promise<{ baseUrl: string; response: TDevSessionStartResponse }>((resolve, reject) => {
|
|
179
|
+
const stdoutChunks: Buffer[] = [];
|
|
180
|
+
const stderrChunks: Buffer[] = [];
|
|
181
|
+
const child = spawn(process.execPath, [runnerFilepath, cli.args.workdir as string, email, role], {
|
|
182
|
+
cwd: cli.args.workdir as string,
|
|
183
|
+
env: { ...process.env },
|
|
184
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
child.stdout.on('data', (chunk: Buffer | string) => {
|
|
188
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
child.stderr.on('data', (chunk: Buffer | string) => {
|
|
192
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
child.on('error', (error) => reject(error));
|
|
196
|
+
child.on('close', () => {
|
|
197
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
198
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
199
|
+
const markerLine = stdout
|
|
200
|
+
.split(/\r?\n/)
|
|
201
|
+
.find((line) => line.startsWith(localSessionResultMarker));
|
|
202
|
+
|
|
203
|
+
if (stderr.trim()) {
|
|
204
|
+
process.stderr.write(stderr.endsWith('\n') ? stderr : `${stderr}\n`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!markerLine) {
|
|
208
|
+
reject(
|
|
209
|
+
new Error(
|
|
210
|
+
['Local session runner exited without returning a structured result.', stdout.trim() ? `stdout:\n${stdout.trim()}` : undefined]
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join('\n\n'),
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const payload = JSON.parse(markerLine.slice(localSessionResultMarker.length)) as
|
|
219
|
+
| { session: { baseUrl: string; response: TDevSessionStartResponse } }
|
|
220
|
+
| { error: string };
|
|
221
|
+
|
|
222
|
+
if ('session' in payload) {
|
|
223
|
+
resolve(payload.session);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
reject(new Error(payload.error || 'Session runner failed.'));
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const run = async () => {
|
|
233
|
+
const email = typeof cli.args.email === 'string' ? cli.args.email.trim() : '';
|
|
234
|
+
const role = typeof cli.args.role === 'string' ? cli.args.role.trim() : '';
|
|
235
|
+
const shouldPrintJson = cli.args.json === true;
|
|
236
|
+
const shouldUseRemoteServer =
|
|
237
|
+
(typeof cli.args.port === 'string' && cli.args.port.length > 0) ||
|
|
238
|
+
(typeof cli.args.url === 'string' && cli.args.url.length > 0);
|
|
239
|
+
|
|
240
|
+
if (!email) {
|
|
241
|
+
throw new UsageError('An email is required. Example: proteum session admin@example.com --role ADMIN');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const resolved = buildSessionOutput(
|
|
245
|
+
shouldUseRemoteServer ? await requestSession(email, role) : await runLocalSession(email, role),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (shouldPrintJson) {
|
|
249
|
+
printJson(resolved);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(renderSession(resolved));
|
|
254
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const tsNode = require('ts-node');
|
|
5
|
+
|
|
6
|
+
const resultMarker = '__PROTEUM_SESSION_RESULT__';
|
|
7
|
+
|
|
8
|
+
const printPayload = (payload) => {
|
|
9
|
+
process.stdout.write(`${resultMarker}${JSON.stringify(payload)}\n`);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const fail = (error) => {
|
|
13
|
+
printPayload({ error: error instanceof Error ? error.message : String(error) });
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getAvailablePort = async () =>
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
const server = net.createServer();
|
|
20
|
+
|
|
21
|
+
server.once('error', reject);
|
|
22
|
+
server.listen(0, '127.0.0.1', () => {
|
|
23
|
+
const address = server.address();
|
|
24
|
+
|
|
25
|
+
if (!address || typeof address === 'string') {
|
|
26
|
+
server.close(() => reject(new Error('Could not determine a local port for the session runner.')));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
server.close((error) => {
|
|
31
|
+
if (error) {
|
|
32
|
+
reject(error);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resolve(address.port);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const closeMultiCompiler = async (multiCompiler) =>
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
multiCompiler.close((error) => {
|
|
44
|
+
if (error) {
|
|
45
|
+
reject(error);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const runCompiler = async (compiler) => {
|
|
54
|
+
const multiCompiler = await compiler.create();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
multiCompiler.run((error, stats) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
reject(error);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (stats && stats.hasErrors()) {
|
|
65
|
+
reject(new Error('Compilation failed for the local dev session runner.'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
compiler.dispose();
|
|
74
|
+
await closeMultiCompiler(multiCompiler);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const waitForServerReady = async (child) =>
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
let settled = false;
|
|
81
|
+
|
|
82
|
+
const finish = (callback, value) => {
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true;
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
child.off('message', onMessage);
|
|
87
|
+
child.off('error', onError);
|
|
88
|
+
child.off('exit', onExit);
|
|
89
|
+
callback(value);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onMessage = (message) => {
|
|
93
|
+
if (!message || message.type !== 'proteum:server-ready' || typeof message.publicUrl !== 'string') return;
|
|
94
|
+
finish(resolve, message.publicUrl);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const onError = (error) => finish(reject, error);
|
|
98
|
+
const onExit = (code, signal) =>
|
|
99
|
+
finish(reject, new Error(`Local session server exited before becoming ready (code=${code}, signal=${signal}).`));
|
|
100
|
+
const timeout = setTimeout(
|
|
101
|
+
() => finish(reject, new Error('Timed out while waiting for the local session server to become ready.')),
|
|
102
|
+
30000,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
child.on('message', onMessage);
|
|
106
|
+
child.once('error', onError);
|
|
107
|
+
child.once('exit', onExit);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const stopServerProcess = async (child) => {
|
|
111
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
112
|
+
|
|
113
|
+
child.kill('SIGTERM');
|
|
114
|
+
|
|
115
|
+
await new Promise((resolve) => {
|
|
116
|
+
const timeout = setTimeout(() => {
|
|
117
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
118
|
+
}, 5000);
|
|
119
|
+
|
|
120
|
+
child.once('exit', () => {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const requestSession = async (baseUrl, email, role) => {
|
|
128
|
+
const response = await fetch(`${baseUrl}/__proteum/session/start`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'content-type': 'application/json' },
|
|
131
|
+
body: JSON.stringify(role ? { email, role } : { email }),
|
|
132
|
+
});
|
|
133
|
+
const body = await response.json();
|
|
134
|
+
|
|
135
|
+
if (response.status >= 400) {
|
|
136
|
+
throw new Error((body && body.error) || `Session request failed with status ${response.status}.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { baseUrl, response: body };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
(async () => {
|
|
143
|
+
const [, , appRootArg = '', email = '', role = ''] = process.argv;
|
|
144
|
+
const appRoot = path.resolve(appRootArg);
|
|
145
|
+
|
|
146
|
+
if (!appRootArg || !email) {
|
|
147
|
+
fail(new Error('sessionLocalRunner requires <appRoot> and <email>.'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
process.chdir(appRoot);
|
|
152
|
+
|
|
153
|
+
tsNode.register({
|
|
154
|
+
transpileOnly: true,
|
|
155
|
+
project: path.join(__dirname, '..', 'tsconfig.json'),
|
|
156
|
+
files: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const port = await getAvailablePort();
|
|
160
|
+
const cli = require('../context.ts').default;
|
|
161
|
+
cli.setArgs({ workdir: appRoot, port: String(port), url: '', json: true });
|
|
162
|
+
|
|
163
|
+
const app = require('../app/index.ts').default;
|
|
164
|
+
const Compiler = require('../compiler/index.ts').default;
|
|
165
|
+
|
|
166
|
+
if (app.env.profile !== 'dev') {
|
|
167
|
+
fail(new Error(`Proteum sessions are only available when ENV_PROFILE=dev. Current profile: ${app.env.profile}.`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const compiler = new Compiler('dev');
|
|
172
|
+
await runCompiler(compiler);
|
|
173
|
+
|
|
174
|
+
const serverProcess = spawn(process.execPath, ['--preserve-symlinks', path.join(app.outputPath('dev'), 'server.js')], {
|
|
175
|
+
cwd: app.paths.root,
|
|
176
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const baseUrl = await waitForServerReady(serverProcess);
|
|
181
|
+
const session = await requestSession(baseUrl, email, role);
|
|
182
|
+
printPayload({ session });
|
|
183
|
+
} finally {
|
|
184
|
+
await stopServerProcess(serverProcess);
|
|
185
|
+
}
|
|
186
|
+
})().catch((error) => {
|
|
187
|
+
fail(error);
|
|
188
|
+
});
|
package/cli/commands/trace.ts
CHANGED
|
@@ -75,6 +75,9 @@ const getTraceErrorMessage = (body: TRequestTraceErrorResponse | object | string
|
|
|
75
75
|
return `Trace request failed with status ${statusCode}.`;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
const hasStructuredTraceError = (body: TRequestTraceErrorResponse | object | string | undefined): body is TRequestTraceErrorResponse =>
|
|
79
|
+
typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string';
|
|
80
|
+
|
|
78
81
|
const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GET' | 'POST'; json?: object }) => {
|
|
79
82
|
const attempts: string[] = [];
|
|
80
83
|
|
|
@@ -89,6 +92,11 @@ const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GE
|
|
|
89
92
|
});
|
|
90
93
|
|
|
91
94
|
if (response.statusCode >= 400) {
|
|
95
|
+
if (response.statusCode === 404 && !hasStructuredTraceError(response.body as TRequestTraceErrorResponse | object | string | undefined)) {
|
|
96
|
+
attempts.push(`${baseUrl}${pathname}: returned 404`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
throw new TraceResponseError(
|
|
93
101
|
getTraceErrorMessage(response.body as TRequestTraceErrorResponse | object | string | undefined, response.statusCode),
|
|
94
102
|
);
|
|
@@ -119,6 +127,7 @@ const renderTraceSummary = (request: TRequestTraceListItem) =>
|
|
|
119
127
|
`capture=${request.capture}`,
|
|
120
128
|
`events=${request.eventCount}`,
|
|
121
129
|
`calls=${request.callCount}`,
|
|
130
|
+
`sql=${request.sqlQueryCount}`,
|
|
122
131
|
request.user ? `user=${request.user}` : '',
|
|
123
132
|
request.errorMessage ? `error=${request.errorMessage}` : '',
|
|
124
133
|
]
|
|
@@ -129,7 +138,7 @@ const renderTrace = (request: TRequestTrace) =>
|
|
|
129
138
|
[
|
|
130
139
|
`Request ${request.id}`,
|
|
131
140
|
`- ${request.method} ${request.path} status=${request.statusCode ?? 'pending'} capture=${request.capture}`,
|
|
132
|
-
`- started=${request.startedAt} durationMs=${request.durationMs ?? 'pending'} events=${request.events.length} dropped=${request.droppedEvents}`,
|
|
141
|
+
`- started=${request.startedAt} durationMs=${request.durationMs ?? 'pending'} events=${request.events.length} calls=${request.calls.length} sql=${request.sqlQueries.length} dropped=${request.droppedEvents}`,
|
|
133
142
|
...(request.user ? [`- user=${request.user}`] : []),
|
|
134
143
|
...(request.persistedFilepath ? [`- persisted=${request.persistedFilepath}`] : []),
|
|
135
144
|
'Calls',
|
|
@@ -139,6 +148,13 @@ const renderTrace = (request: TRequestTrace) =>
|
|
|
139
148
|
(call) =>
|
|
140
149
|
`- ${call.origin} ${call.label} ${call.method} ${call.path} status=${call.statusCode ?? 'pending'} durationMs=${call.durationMs ?? 'pending'} req=${call.requestDataKeys.join(',')} res=${call.resultKeys.join(',')}`,
|
|
141
150
|
)),
|
|
151
|
+
'SQL',
|
|
152
|
+
...(request.sqlQueries.length === 0
|
|
153
|
+
? ['- none']
|
|
154
|
+
: request.sqlQueries.map(
|
|
155
|
+
(query) =>
|
|
156
|
+
`- [${query.durationMs}ms] ${query.kind} ${query.operation} ${query.callerMethod} ${query.callerPath} ${query.query}${query.paramsText ? ` params=${query.paramsText}` : ''}`,
|
|
157
|
+
)),
|
|
142
158
|
'Events',
|
|
143
159
|
...request.events.map(
|
|
144
160
|
(event) =>
|