proteum 2.1.0 → 2.1.2
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 +44 -98
- package/README.md +143 -10
- package/agents/framework/AGENTS.md +146 -886
- package/agents/project/AGENTS.md +73 -127
- 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/app/config.ts +7 -20
- package/cli/bin.js +8 -0
- package/cli/commands/command.ts +243 -0
- package/cli/commands/commandLocalRunner.js +198 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/deploy/web.ts +1 -2
- package/cli/commands/dev.ts +98 -2
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -186
- package/cli/commands/init.ts +2 -94
- package/cli/commands/trace.ts +228 -0
- package/cli/compiler/artifacts/commands.ts +217 -0
- package/cli/compiler/artifacts/manifest.ts +35 -21
- package/cli/compiler/artifacts/services.ts +300 -1
- package/cli/compiler/client/index.ts +43 -8
- package/cli/compiler/common/commands.ts +175 -0
- package/cli/compiler/common/index.ts +1 -1
- package/cli/compiler/common/proteumManifest.ts +15 -114
- package/cli/compiler/index.ts +25 -2
- package/cli/compiler/server/index.ts +31 -6
- package/cli/index.ts +1 -4
- package/cli/paths.ts +16 -1
- package/cli/presentation/commands.ts +104 -14
- package/cli/presentation/devSession.ts +22 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +121 -4
- package/cli/scaffold/index.ts +720 -0
- package/cli/scaffold/templates.ts +344 -0
- package/cli/scaffold/types.ts +26 -0
- package/cli/tsconfig.json +4 -1
- package/cli/utils/check.ts +1 -1
- package/client/app/component.tsx +13 -9
- package/client/dev/profiler/index.tsx +2511 -0
- package/client/dev/profiler/noop.tsx +5 -0
- package/client/dev/profiler/runtime.noop.ts +116 -0
- package/client/dev/profiler/runtime.ts +840 -0
- package/client/services/router/components/router.tsx +30 -2
- package/client/services/router/index.tsx +27 -3
- package/client/services/router/request/api.ts +133 -17
- package/commands/proteum/diagnostics.ts +11 -0
- package/common/dev/commands.ts +50 -0
- package/common/dev/diagnostics.ts +298 -0
- package/common/dev/profiler.ts +92 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +115 -0
- package/common/env/proteumEnv.ts +284 -0
- package/common/router/index.ts +4 -22
- package/docs/dev-commands.md +93 -0
- package/docs/diagnostics.md +88 -0
- package/docs/request-tracing.md +132 -0
- package/eslint.js +11 -6
- package/package.json +3 -3
- package/server/app/commands.ts +35 -370
- package/server/app/commandsManager.ts +393 -0
- package/server/app/container/config.ts +11 -49
- package/server/app/container/console/index.ts +2 -3
- package/server/app/container/index.ts +5 -2
- package/server/app/container/trace/index.ts +364 -0
- package/server/app/devCommands.ts +192 -0
- package/server/app/devDiagnostics.ts +53 -0
- package/server/app/index.ts +29 -6
- package/server/index.ts +0 -1
- package/server/services/auth/index.ts +525 -61
- package/server/services/auth/router/index.ts +106 -7
- package/server/services/cron/CronTask.ts +73 -5
- package/server/services/cron/index.ts +34 -11
- package/server/services/fetch/index.ts +3 -10
- package/server/services/prisma/index.ts +66 -4
- package/server/services/router/http/index.ts +173 -6
- package/server/services/router/index.ts +200 -12
- package/server/services/router/request/api.ts +30 -1
- package/server/services/router/response/index.ts +83 -10
- package/server/services/router/response/page/document.tsx +16 -0
- package/server/services/router/response/page/index.tsx +27 -1
- package/skills/clean-project-code/SKILL.md +7 -2
- package/test-results/.last-run.json +4 -0
- package/types/aliases.d.ts +6 -0
- package/types/global/utils.d.ts +7 -14
- 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
|
@@ -17,12 +17,14 @@ import helmet from 'helmet'; // Diverses protections
|
|
|
17
17
|
import compression from 'compression';
|
|
18
18
|
import fileUpload from 'express-fileupload';
|
|
19
19
|
import cookieParser from 'cookie-parser';
|
|
20
|
-
import * as csp from 'express-csp-header';
|
|
21
20
|
|
|
22
21
|
// Core
|
|
23
22
|
import Container from '@server/app/container';
|
|
23
|
+
import type CronManager from '@server/services/cron';
|
|
24
|
+
import type CronTask from '@server/services/cron/CronTask';
|
|
24
25
|
import type { TServerRouter } from '..';
|
|
25
26
|
import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
|
|
27
|
+
import { explainSectionNames } from '@common/dev/diagnostics';
|
|
26
28
|
|
|
27
29
|
// Middlewaees (core)
|
|
28
30
|
import { isMutipart, MiddlewareFormData } from './multipart';
|
|
@@ -50,6 +52,27 @@ export type Config = {
|
|
|
50
52
|
|
|
51
53
|
export type Hooks = {};
|
|
52
54
|
|
|
55
|
+
type TContentSecurityPolicyOptions = NonNullable<Parameters<typeof helmet.contentSecurityPolicy>[0]>;
|
|
56
|
+
type TContentSecurityPolicyDirectives = NonNullable<TContentSecurityPolicyOptions['directives']>;
|
|
57
|
+
|
|
58
|
+
const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPolicyOptions => {
|
|
59
|
+
const directives: TContentSecurityPolicyDirectives = {
|
|
60
|
+
defaultSrc:
|
|
61
|
+
config.default && config.default.length > 0
|
|
62
|
+
? [...config.default]
|
|
63
|
+
: helmet.contentSecurityPolicy.dangerouslyDisableDefaultSrc,
|
|
64
|
+
scriptSrc: ["'unsafe-inline'", "'self'", "'unsafe-eval'", ...config.scripts],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (config.styles && config.styles.length > 0) directives.styleSrc = [...config.styles];
|
|
68
|
+
if (config.images && config.images.length > 0) directives.imgSrc = [...config.images];
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
useDefaults: false,
|
|
72
|
+
directives,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
53
76
|
/*----------------------------------
|
|
54
77
|
- FUNCTION
|
|
55
78
|
----------------------------------*/
|
|
@@ -200,12 +223,9 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
200
223
|
|
|
201
224
|
if (this.config.cors !== undefined) routes.use(cors(this.config.cors));
|
|
202
225
|
|
|
203
|
-
routes.use(
|
|
204
|
-
csp.expressCspHeader({
|
|
205
|
-
directives: { 'script-src': [csp.INLINE, csp.SELF, csp.UNSAFE_EVAL, ...this.config.csp.scripts] },
|
|
206
|
-
}),
|
|
207
|
-
);
|
|
226
|
+
routes.use(helmet.contentSecurityPolicy(createContentSecurityPolicy(this.config.csp)));
|
|
208
227
|
|
|
228
|
+
this.registerDevTraceRoutes(routes);
|
|
209
229
|
routes.use(routeRequest);
|
|
210
230
|
|
|
211
231
|
/*----------------------------------
|
|
@@ -227,4 +247,151 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
227
247
|
public async cleanup() {
|
|
228
248
|
this.http.close();
|
|
229
249
|
}
|
|
250
|
+
|
|
251
|
+
private registerDevTraceRoutes(routes: express.Express) {
|
|
252
|
+
if (!__DEV__ || this.app.env.profile !== 'dev') return;
|
|
253
|
+
|
|
254
|
+
if (this.app.container.Trace.isEnabled()) {
|
|
255
|
+
routes.get('/__proteum/trace/requests', (req, res) => {
|
|
256
|
+
const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
|
|
257
|
+
const parsedLimit = typeof rawLimit === 'string' ? Number.parseInt(rawLimit, 10) : NaN;
|
|
258
|
+
const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 20;
|
|
259
|
+
|
|
260
|
+
res.json({ requests: this.app.container.Trace.listRequests(limit) });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
routes.get('/__proteum/trace/latest', (_req, res) => {
|
|
264
|
+
const request = this.app.container.Trace.getLatestRequest();
|
|
265
|
+
if (!request) {
|
|
266
|
+
res.status(404).json({ error: 'No request trace is available yet.' });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
res.json({ request });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
routes.get('/__proteum/trace/requests/:id', (req, res) => {
|
|
274
|
+
const request = this.app.container.Trace.getRequest(req.params.id);
|
|
275
|
+
if (!request) {
|
|
276
|
+
res.status(404).json({ error: `Trace ${req.params.id} was not found.` });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
res.json({ request });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
routes.post('/__proteum/trace/arm', (req, res) => {
|
|
284
|
+
const rawCapture = typeof req.body.capture === 'string' ? req.body.capture : 'deep';
|
|
285
|
+
const capture = this.app.container.Trace.armNextRequest(rawCapture);
|
|
286
|
+
|
|
287
|
+
res.json({ armed: true, capture });
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
routes.get('/__proteum/explain', (req, res) => {
|
|
292
|
+
const rawSections = [
|
|
293
|
+
...(Array.isArray(req.query.section) ? req.query.section : req.query.section ? [req.query.section] : []),
|
|
294
|
+
...(Array.isArray(req.query.sections)
|
|
295
|
+
? req.query.sections.flatMap((value) => (typeof value === 'string' ? value.split(',') : []))
|
|
296
|
+
: typeof req.query.sections === 'string'
|
|
297
|
+
? req.query.sections.split(',')
|
|
298
|
+
: []),
|
|
299
|
+
]
|
|
300
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
301
|
+
.filter(Boolean);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const diagnostics = this.app.getDevDiagnostics();
|
|
305
|
+
const sections = diagnostics.normalizeExplainSections(rawSections);
|
|
306
|
+
res.json(diagnostics.explain(sections));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
309
|
+
const isBadRequest = explainSectionNames.some((sectionName) => message.includes(sectionName)) || message.includes('Unknown explain section');
|
|
310
|
+
res.status(isBadRequest ? 400 : 500).json({ error: message });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
routes.get('/__proteum/doctor', (req, res) => {
|
|
315
|
+
const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
|
|
316
|
+
const strict = rawStrict === '1' || rawStrict === 'true';
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
res.json(this.app.getDevDiagnostics().doctor(strict));
|
|
320
|
+
} catch (error) {
|
|
321
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
routes.get('/__proteum/cron/tasks', (_req, res) => {
|
|
326
|
+
const cron = this.getCronManager();
|
|
327
|
+
res.json({
|
|
328
|
+
automaticExecution: cron?.isAutomaticExecutionEnabled() ?? false,
|
|
329
|
+
tasks: cron?.listTasks() ?? [],
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
routes.get('/__proteum/commands', (_req, res) => {
|
|
334
|
+
res.json({ commands: this.app.getDevCommands().list() });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
routes.post('/__proteum/commands/run', async (req, res) => {
|
|
338
|
+
const commandPath = typeof req.body?.path === 'string' ? req.body.path.trim() : '';
|
|
339
|
+
if (!commandPath) {
|
|
340
|
+
res.status(400).json({ error: 'Command path is required.' });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const execution = await this.app.getDevCommands().run(commandPath);
|
|
346
|
+
res.json({ execution });
|
|
347
|
+
} catch (error) {
|
|
348
|
+
const execution =
|
|
349
|
+
error instanceof Error && 'execution' in error && typeof error.execution === 'object'
|
|
350
|
+
? error.execution
|
|
351
|
+
: undefined;
|
|
352
|
+
const statusCode = error instanceof Error && error.name === 'NotFound' ? 404 : 500;
|
|
353
|
+
|
|
354
|
+
res.status(statusCode).json({
|
|
355
|
+
error: error instanceof Error ? error.message : String(error),
|
|
356
|
+
execution,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
routes.post('/__proteum/cron/tasks/run', async (req, res) => {
|
|
362
|
+
const cron = this.getCronManager();
|
|
363
|
+
if (!cron) {
|
|
364
|
+
res.status(404).json({ error: 'Cron service is not registered for this app.' });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : '';
|
|
369
|
+
if (!name) {
|
|
370
|
+
res.status(400).json({ error: 'Cron task name is required.' });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let task: CronTask;
|
|
375
|
+
try {
|
|
376
|
+
task = cron.get(name);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
res.status(404).json({ error: error instanceof Error ? error.message : String(error) });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
await cron.exec(name);
|
|
384
|
+
res.json({ task: task.toProfilerTask() });
|
|
385
|
+
} catch (error) {
|
|
386
|
+
res.status(500).json({
|
|
387
|
+
error: error instanceof Error ? error.message : String(error),
|
|
388
|
+
task: task.toProfilerTask(),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private getCronManager() {
|
|
395
|
+
return (this.app as typeof this.app & { Cron?: CronManager }).Cron;
|
|
396
|
+
}
|
|
230
397
|
}
|
|
@@ -32,11 +32,16 @@ import BaseRouter, {
|
|
|
32
32
|
defaultOptions,
|
|
33
33
|
matchRoute,
|
|
34
34
|
buildUrl,
|
|
35
|
-
TDomainsList,
|
|
36
35
|
} from '@common/router';
|
|
37
36
|
import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@common/router/contracts';
|
|
38
37
|
import { buildRegex, getRegisterPageArgs } from '@common/router/register';
|
|
39
38
|
import { layoutsList, getLayout } from '@common/router/layouts';
|
|
39
|
+
import {
|
|
40
|
+
profilerOriginHeader,
|
|
41
|
+
profilerParentRequestIdHeader,
|
|
42
|
+
profilerSessionIdHeader,
|
|
43
|
+
profilerTraceRequestIdHeader,
|
|
44
|
+
} from '@common/dev/profiler';
|
|
40
45
|
import { TFetcherList } from '@common/router/request/api';
|
|
41
46
|
import type { TFrontRenderer } from '@common/router/response/page';
|
|
42
47
|
|
|
@@ -108,7 +113,7 @@ export type Config<
|
|
|
108
113
|
|
|
109
114
|
disk?: string; // Disk driver ID
|
|
110
115
|
|
|
111
|
-
|
|
116
|
+
currentDomain: string;
|
|
112
117
|
|
|
113
118
|
http: HttpServiceConfig;
|
|
114
119
|
|
|
@@ -356,7 +361,7 @@ export default class ServerRouter<
|
|
|
356
361
|
}
|
|
357
362
|
|
|
358
363
|
public url = (path: string, params: {} = {}, absolute: boolean = true) =>
|
|
359
|
-
buildUrl(path, params, this.config.
|
|
364
|
+
buildUrl(path, params, this.config.currentDomain, absolute);
|
|
360
365
|
|
|
361
366
|
/*----------------------------------
|
|
362
367
|
- REGISTER
|
|
@@ -581,14 +586,33 @@ export default class ServerRouter<
|
|
|
581
586
|
this,
|
|
582
587
|
);
|
|
583
588
|
|
|
589
|
+
this.app.container.Trace.startRequest({
|
|
590
|
+
id: request.id,
|
|
591
|
+
method: request.method,
|
|
592
|
+
path: request.path,
|
|
593
|
+
url: request.url,
|
|
594
|
+
headers: request.headers,
|
|
595
|
+
data: request.data,
|
|
596
|
+
profilerSessionId: request.headers[profilerSessionIdHeader] || undefined,
|
|
597
|
+
profilerOrigin: request.headers[profilerOriginHeader] || undefined,
|
|
598
|
+
profilerParentRequestId: request.headers[profilerParentRequestIdHeader] || undefined,
|
|
599
|
+
});
|
|
600
|
+
if (this.app.container.Trace.isEnabled()) res.setHeader(profilerTraceRequestIdHeader, request.id);
|
|
601
|
+
|
|
584
602
|
let response: ServerResponse<this>;
|
|
585
603
|
try {
|
|
586
604
|
// Hook
|
|
587
605
|
await this.runHook('request', request);
|
|
606
|
+
this.app.container.Trace.setRequestUser(request.id, request.user?.email);
|
|
588
607
|
|
|
589
608
|
// Bulk API Requests
|
|
590
609
|
if (request.path === '/api' && typeof request.data.fetchers === 'object') {
|
|
591
|
-
|
|
610
|
+
await this.resolveApiBatch(request.data.fetchers, request);
|
|
611
|
+
this.app.container.Trace.finishRequest(request.id, {
|
|
612
|
+
statusCode: request.res.statusCode || 200,
|
|
613
|
+
user: request.user?.email,
|
|
614
|
+
});
|
|
615
|
+
return;
|
|
592
616
|
} else {
|
|
593
617
|
response = await this.resolve(
|
|
594
618
|
request,
|
|
@@ -604,7 +628,17 @@ export default class ServerRouter<
|
|
|
604
628
|
// Static pages
|
|
605
629
|
if (cachedPage) {
|
|
606
630
|
console.log('[router] Get static page from cache', req.path);
|
|
631
|
+
this.app.container.Trace.record(
|
|
632
|
+
request.id,
|
|
633
|
+
'response.send',
|
|
634
|
+
{ cached: true, statusCode: response.statusCode, contentType: 'text/html' },
|
|
635
|
+
'summary',
|
|
636
|
+
);
|
|
607
637
|
res.send(cachedPage.rendered);
|
|
638
|
+
this.app.container.Trace.finishRequest(request.id, {
|
|
639
|
+
statusCode: response.statusCode,
|
|
640
|
+
user: request.user?.email,
|
|
641
|
+
});
|
|
608
642
|
return;
|
|
609
643
|
}
|
|
610
644
|
|
|
@@ -613,9 +647,34 @@ export default class ServerRouter<
|
|
|
613
647
|
// Headers
|
|
614
648
|
res.header(response.headers);
|
|
615
649
|
// Data
|
|
650
|
+
this.app.container.Trace.record(
|
|
651
|
+
request.id,
|
|
652
|
+
'response.send',
|
|
653
|
+
{
|
|
654
|
+
cached: false,
|
|
655
|
+
statusCode: response.statusCode,
|
|
656
|
+
contentType: response.headers['Content-Type'] || '',
|
|
657
|
+
headerKeys: Object.keys(response.headers),
|
|
658
|
+
},
|
|
659
|
+
'summary',
|
|
660
|
+
);
|
|
616
661
|
res.send(response.data);
|
|
662
|
+
this.app.container.Trace.finishRequest(request.id, {
|
|
663
|
+
statusCode: response.statusCode,
|
|
664
|
+
user: request.user?.email,
|
|
665
|
+
});
|
|
617
666
|
} else if (response.data !== 'true') {
|
|
667
|
+
this.app.container.Trace.finishRequest(request.id, {
|
|
668
|
+
statusCode: res.statusCode || response.statusCode,
|
|
669
|
+
user: request.user?.email,
|
|
670
|
+
errorMessage: "Can't return data from the controller since response has already been sent via express.",
|
|
671
|
+
});
|
|
618
672
|
throw new Error("Can't return data from the controller since response has already been sent via express.");
|
|
673
|
+
} else {
|
|
674
|
+
this.app.container.Trace.finishRequest(request.id, {
|
|
675
|
+
statusCode: res.statusCode || response.statusCode,
|
|
676
|
+
user: request.user?.email,
|
|
677
|
+
});
|
|
619
678
|
}
|
|
620
679
|
}
|
|
621
680
|
|
|
@@ -655,6 +714,16 @@ export default class ServerRouter<
|
|
|
655
714
|
},
|
|
656
715
|
async () => {
|
|
657
716
|
const timeStart = Date.now();
|
|
717
|
+
const routeStats = {
|
|
718
|
+
total: this.routes.length,
|
|
719
|
+
staticSkipped: 0,
|
|
720
|
+
methodMismatch: 0,
|
|
721
|
+
acceptMismatch: 0,
|
|
722
|
+
pathMismatch: 0,
|
|
723
|
+
matched: 0,
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
this.app.container.Trace.record(request.id, 'resolve.start', { isStatic: Boolean(isStatic) }, 'summary');
|
|
658
727
|
|
|
659
728
|
if (this.status === 'starting') {
|
|
660
729
|
console.log(LogPrefix, `Waiting for servert to be resdy before resolving request`);
|
|
@@ -669,6 +738,16 @@ export default class ServerRouter<
|
|
|
669
738
|
// Controller route
|
|
670
739
|
const controllerRoute = this.controllers[request.path];
|
|
671
740
|
if (controllerRoute !== undefined) {
|
|
741
|
+
this.app.container.Trace.record(
|
|
742
|
+
request.id,
|
|
743
|
+
'resolve.controller-route',
|
|
744
|
+
{
|
|
745
|
+
path: request.path,
|
|
746
|
+
accept: controllerRoute.options.accept || '',
|
|
747
|
+
filepath: controllerRoute.options.filepath || '',
|
|
748
|
+
},
|
|
749
|
+
'summary',
|
|
750
|
+
);
|
|
672
751
|
// Create response
|
|
673
752
|
await response.runController(controllerRoute);
|
|
674
753
|
if (response.wasProvided) return resolve(response);
|
|
@@ -679,21 +758,81 @@ export default class ServerRouter<
|
|
|
679
758
|
|
|
680
759
|
// Classic routes
|
|
681
760
|
for (const route of this.routes) {
|
|
682
|
-
if (isStatic && !route.options.whenStatic)
|
|
761
|
+
if (isStatic && !route.options.whenStatic) {
|
|
762
|
+
routeStats.staticSkipped++;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
683
765
|
|
|
684
766
|
// Match Method
|
|
685
|
-
if (request.method !== route.method && route.method !== '*')
|
|
767
|
+
if (request.method !== route.method && route.method !== '*') {
|
|
768
|
+
routeStats.methodMismatch++;
|
|
769
|
+
if (this.app.container.Trace.shouldCapture(request.id, 'deep')) {
|
|
770
|
+
this.app.container.Trace.record(
|
|
771
|
+
request.id,
|
|
772
|
+
'resolve.route-skip',
|
|
773
|
+
{
|
|
774
|
+
reason: 'method',
|
|
775
|
+
routeMethod: route.method,
|
|
776
|
+
requestMethod: request.method,
|
|
777
|
+
routePath: route.path || '',
|
|
778
|
+
routeId: route.options.id || '',
|
|
779
|
+
filepath: route.options.filepath || '',
|
|
780
|
+
},
|
|
781
|
+
'deep',
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
686
786
|
|
|
687
787
|
// Match Response format
|
|
688
|
-
if (!request.accepts(route.options.accept))
|
|
788
|
+
if (!request.accepts(route.options.accept)) {
|
|
789
|
+
routeStats.acceptMismatch++;
|
|
790
|
+
if (this.app.container.Trace.shouldCapture(request.id, 'deep')) {
|
|
791
|
+
this.app.container.Trace.record(
|
|
792
|
+
request.id,
|
|
793
|
+
'resolve.route-skip',
|
|
794
|
+
{
|
|
795
|
+
reason: 'accept',
|
|
796
|
+
routeAccept: route.options.accept || '',
|
|
797
|
+
routePath: route.path || '',
|
|
798
|
+
routeId: route.options.id || '',
|
|
799
|
+
filepath: route.options.filepath || '',
|
|
800
|
+
},
|
|
801
|
+
'deep',
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
689
806
|
|
|
690
807
|
const isMatching = matchRoute(route, request);
|
|
691
|
-
if (!isMatching)
|
|
808
|
+
if (!isMatching) {
|
|
809
|
+
routeStats.pathMismatch++;
|
|
810
|
+
if (this.app.container.Trace.shouldCapture(request.id, 'deep')) {
|
|
811
|
+
this.app.container.Trace.record(
|
|
812
|
+
request.id,
|
|
813
|
+
'resolve.route-skip',
|
|
814
|
+
{
|
|
815
|
+
reason: 'path',
|
|
816
|
+
routePath: route.path || '',
|
|
817
|
+
routeId: route.options.id || '',
|
|
818
|
+
filepath: route.options.filepath || '',
|
|
819
|
+
},
|
|
820
|
+
'deep',
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
692
825
|
|
|
826
|
+
routeStats.matched++;
|
|
693
827
|
await this.resolvedRoute(route, response, timeStart);
|
|
694
|
-
if (response.wasProvided)
|
|
828
|
+
if (response.wasProvided) {
|
|
829
|
+
this.app.container.Trace.record(request.id, 'resolve.routes-evaluated', routeStats, 'resolve');
|
|
830
|
+
return resolve(response);
|
|
831
|
+
}
|
|
695
832
|
}
|
|
696
833
|
|
|
834
|
+
this.app.container.Trace.record(request.id, 'resolve.routes-evaluated', routeStats, 'resolve');
|
|
835
|
+
this.app.container.Trace.record(request.id, 'resolve.not-found', { path: request.path }, 'summary');
|
|
697
836
|
reject(new NotFound());
|
|
698
837
|
} catch (error) {
|
|
699
838
|
const typedError =
|
|
@@ -721,6 +860,19 @@ export default class ServerRouter<
|
|
|
721
860
|
private async resolvedRoute(route: TMatchedRoute, response: ServerResponse<this>, timeStart: number) {
|
|
722
861
|
route = await response.resolveRouteOptions(route);
|
|
723
862
|
|
|
863
|
+
this.app.container.Trace.record(
|
|
864
|
+
response.request.id,
|
|
865
|
+
'resolve.route-match',
|
|
866
|
+
{
|
|
867
|
+
routePath: route.path || '',
|
|
868
|
+
routeId: route.options.id || '',
|
|
869
|
+
filepath: route.options.filepath || '',
|
|
870
|
+
accept: route.options.accept || '',
|
|
871
|
+
method: route.method,
|
|
872
|
+
},
|
|
873
|
+
'summary',
|
|
874
|
+
);
|
|
875
|
+
|
|
724
876
|
// Run on resolution hooks. Ex: authentication check
|
|
725
877
|
await this.runHook('resolved', route, response.request, response);
|
|
726
878
|
|
|
@@ -757,10 +909,36 @@ export default class ServerRouter<
|
|
|
757
909
|
if (!fetcher || !('method' in fetcher)) continue;
|
|
758
910
|
|
|
759
911
|
const { method, path, data } = fetcher;
|
|
912
|
+
const callId = this.app.container.Trace.startCall(request.id, {
|
|
913
|
+
origin: 'api-batch-fetcher',
|
|
914
|
+
label: id,
|
|
915
|
+
method,
|
|
916
|
+
path,
|
|
917
|
+
fetcherId: id,
|
|
918
|
+
requestDataKeys: data && typeof data === 'object' ? Object.keys(data) : [],
|
|
919
|
+
requestData: data,
|
|
920
|
+
});
|
|
760
921
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
922
|
+
try {
|
|
923
|
+
const response = await this.resolve(request.children(method, path, data));
|
|
924
|
+
responseData[id] = response.data;
|
|
925
|
+
this.app.container.Trace.finishCall(request.id, callId, {
|
|
926
|
+
statusCode: response.statusCode,
|
|
927
|
+
resultKeys:
|
|
928
|
+
response.data && typeof response.data === 'object' && !Array.isArray(response.data)
|
|
929
|
+
? Object.keys(response.data as Record<string, unknown>)
|
|
930
|
+
: [],
|
|
931
|
+
result: response.data,
|
|
932
|
+
});
|
|
933
|
+
} catch (error) {
|
|
934
|
+
const typedError = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
|
935
|
+
const statusCode = 'http' in typedError ? Number((typedError as Error & { http?: number }).http) : undefined;
|
|
936
|
+
this.app.container.Trace.finishCall(request.id, callId, {
|
|
937
|
+
statusCode: Number.isFinite(statusCode) ? statusCode : undefined,
|
|
938
|
+
errorMessage: typedError.message,
|
|
939
|
+
});
|
|
940
|
+
throw error;
|
|
941
|
+
}
|
|
764
942
|
|
|
765
943
|
// TODO: merge response.headers ?
|
|
766
944
|
}
|
|
@@ -782,6 +960,16 @@ export default class ServerRouter<
|
|
|
782
960
|
|
|
783
961
|
const response = new ServerResponse(request).status(code);
|
|
784
962
|
|
|
963
|
+
this.app.container.Trace.record(
|
|
964
|
+
request.id,
|
|
965
|
+
'error',
|
|
966
|
+
{
|
|
967
|
+
code,
|
|
968
|
+
error,
|
|
969
|
+
},
|
|
970
|
+
'summary',
|
|
971
|
+
);
|
|
972
|
+
|
|
785
973
|
// Rapport / debug
|
|
786
974
|
if (code === 500) {
|
|
787
975
|
// Print the error here so the stacktrace appears in the bug report logs
|
|
@@ -80,7 +80,36 @@ export default class ApiClientRequest extends RequestService implements ApiClien
|
|
|
80
80
|
|
|
81
81
|
// Create a children request to resolve the api data
|
|
82
82
|
const request = this.request.children(method, path, data);
|
|
83
|
-
|
|
83
|
+
const callId = this.request.router.app.container.Trace.startCall(this.request.id, {
|
|
84
|
+
origin: 'ssr-fetcher',
|
|
85
|
+
label: id,
|
|
86
|
+
method,
|
|
87
|
+
path,
|
|
88
|
+
fetcherId: id,
|
|
89
|
+
requestDataKeys: data && typeof data === 'object' ? Object.keys(data) : [],
|
|
90
|
+
requestData: data,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const response = await request.router.resolve(request);
|
|
95
|
+
fetchedData[id] = response.data;
|
|
96
|
+
this.request.router.app.container.Trace.finishCall(this.request.id, callId, {
|
|
97
|
+
statusCode: response.statusCode,
|
|
98
|
+
resultKeys:
|
|
99
|
+
response.data && typeof response.data === 'object' && !Array.isArray(response.data)
|
|
100
|
+
? Object.keys(response.data as Record<string, unknown>)
|
|
101
|
+
: [],
|
|
102
|
+
result: response.data,
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const typedError = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
|
106
|
+
const statusCode = 'http' in typedError ? Number((typedError as Error & { http?: number }).http) : undefined;
|
|
107
|
+
this.request.router.app.container.Trace.finishCall(this.request.id, callId, {
|
|
108
|
+
statusCode: Number.isFinite(statusCode) ? statusCode : undefined,
|
|
109
|
+
errorMessage: typedError.message,
|
|
110
|
+
});
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
84
113
|
}
|
|
85
114
|
|
|
86
115
|
return fetchedData;
|