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.
Files changed (95) hide show
  1. package/AGENTS.md +44 -98
  2. package/README.md +143 -10
  3. package/agents/framework/AGENTS.md +146 -886
  4. package/agents/project/AGENTS.md +73 -127
  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/app/config.ts +7 -20
  11. package/cli/bin.js +8 -0
  12. package/cli/commands/command.ts +243 -0
  13. package/cli/commands/commandLocalRunner.js +198 -0
  14. package/cli/commands/create.ts +5 -0
  15. package/cli/commands/deploy/web.ts +1 -2
  16. package/cli/commands/dev.ts +98 -2
  17. package/cli/commands/doctor.ts +8 -74
  18. package/cli/commands/explain.ts +8 -186
  19. package/cli/commands/init.ts +2 -94
  20. package/cli/commands/trace.ts +228 -0
  21. package/cli/compiler/artifacts/commands.ts +217 -0
  22. package/cli/compiler/artifacts/manifest.ts +35 -21
  23. package/cli/compiler/artifacts/services.ts +300 -1
  24. package/cli/compiler/client/index.ts +43 -8
  25. package/cli/compiler/common/commands.ts +175 -0
  26. package/cli/compiler/common/index.ts +1 -1
  27. package/cli/compiler/common/proteumManifest.ts +15 -114
  28. package/cli/compiler/index.ts +25 -2
  29. package/cli/compiler/server/index.ts +31 -6
  30. package/cli/index.ts +1 -4
  31. package/cli/paths.ts +16 -1
  32. package/cli/presentation/commands.ts +104 -14
  33. package/cli/presentation/devSession.ts +22 -3
  34. package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
  35. package/cli/runtime/commands.ts +121 -4
  36. package/cli/scaffold/index.ts +720 -0
  37. package/cli/scaffold/templates.ts +344 -0
  38. package/cli/scaffold/types.ts +26 -0
  39. package/cli/tsconfig.json +4 -1
  40. package/cli/utils/check.ts +1 -1
  41. package/client/app/component.tsx +13 -9
  42. package/client/dev/profiler/index.tsx +2511 -0
  43. package/client/dev/profiler/noop.tsx +5 -0
  44. package/client/dev/profiler/runtime.noop.ts +116 -0
  45. package/client/dev/profiler/runtime.ts +840 -0
  46. package/client/services/router/components/router.tsx +30 -2
  47. package/client/services/router/index.tsx +27 -3
  48. package/client/services/router/request/api.ts +133 -17
  49. package/commands/proteum/diagnostics.ts +11 -0
  50. package/common/dev/commands.ts +50 -0
  51. package/common/dev/diagnostics.ts +298 -0
  52. package/common/dev/profiler.ts +92 -0
  53. package/common/dev/proteumManifest.ts +135 -0
  54. package/common/dev/requestTrace.ts +115 -0
  55. package/common/env/proteumEnv.ts +284 -0
  56. package/common/router/index.ts +4 -22
  57. package/docs/dev-commands.md +93 -0
  58. package/docs/diagnostics.md +88 -0
  59. package/docs/request-tracing.md +132 -0
  60. package/eslint.js +11 -6
  61. package/package.json +3 -3
  62. package/server/app/commands.ts +35 -370
  63. package/server/app/commandsManager.ts +393 -0
  64. package/server/app/container/config.ts +11 -49
  65. package/server/app/container/console/index.ts +2 -3
  66. package/server/app/container/index.ts +5 -2
  67. package/server/app/container/trace/index.ts +364 -0
  68. package/server/app/devCommands.ts +192 -0
  69. package/server/app/devDiagnostics.ts +53 -0
  70. package/server/app/index.ts +29 -6
  71. package/server/index.ts +0 -1
  72. package/server/services/auth/index.ts +525 -61
  73. package/server/services/auth/router/index.ts +106 -7
  74. package/server/services/cron/CronTask.ts +73 -5
  75. package/server/services/cron/index.ts +34 -11
  76. package/server/services/fetch/index.ts +3 -10
  77. package/server/services/prisma/index.ts +66 -4
  78. package/server/services/router/http/index.ts +173 -6
  79. package/server/services/router/index.ts +200 -12
  80. package/server/services/router/request/api.ts +30 -1
  81. package/server/services/router/response/index.ts +83 -10
  82. package/server/services/router/response/page/document.tsx +16 -0
  83. package/server/services/router/response/page/index.tsx +27 -1
  84. package/skills/clean-project-code/SKILL.md +7 -2
  85. package/test-results/.last-run.json +4 -0
  86. package/types/aliases.d.ts +6 -0
  87. package/types/global/utils.d.ts +7 -14
  88. package/Rte.zip +0 -0
  89. package/agents/project/agents.md.zip +0 -0
  90. package/doc/TODO.md +0 -71
  91. package/doc/front/router.md +0 -27
  92. package/doc/workspace/workspace.png +0 -0
  93. package/doc/workspace/workspace2.png +0 -0
  94. package/doc/workspace/workspace_26.01.22.png +0 -0
  95. 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
- domains: TDomainsList;
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.domains, absolute);
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
- return await this.resolveApiBatch(request.data.fetchers, request);
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) continue;
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 !== '*') continue;
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)) continue;
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) continue;
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) return resolve(response);
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
- const response = await this.resolve(request.children(method, path, data));
762
-
763
- responseData[id] = response.data;
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
- fetchedData[id] = await request.router.resolve(request).then((res) => res.data);
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;