proteum 2.2.0 → 2.2.1
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 +3 -1
- package/README.md +11 -2
- package/agents/project/AGENTS.md +11 -7
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +1 -1
- package/agents/project/root/AGENTS.md +6 -4
- package/agents/project/tests/AGENTS.md +8 -1
- package/common/dev/requestTrace.ts +66 -0
- package/common/env/proteumEnv.ts +10 -3
- package/docs/assets/unique-domains-chip.png +0 -0
- package/docs/request-tracing.md +17 -3
- package/package.json +1 -1
- package/server/app/container/trace/index.ts +255 -74
- package/server/services/prisma/index.ts +15 -12
- package/server/services/router/http/index.ts +1 -1
- package/server/services/router/index.ts +41 -9
- package/server/services/router/request/index.ts +2 -1
package/AGENTS.md
CHANGED
|
@@ -32,7 +32,7 @@ cd <worktree path>
|
|
|
32
32
|
npx prisma migrate dev --config ./prisma.config.ts --name <migration name>
|
|
33
33
|
```
|
|
34
34
|
- After implementing a framework feature or change, do not stop at code edits. Boot both reference apps, exercise the affected flow with Playwright or the smallest real Proteum surface, run the relevant `proteum` diagnostics or perf commands, and confirm there is no meaningful regression in runtime behavior, performance, load size, SEO output, or coding-style expectations before finishing.
|
|
35
|
-
- When starting a long-lived reference app dev server for framework work, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit thread-scoped session file such as `var/run/proteum/dev/framework-<app>-<task>.json`, inspect tracked sessions plus current listeners first, for example with `npx proteum dev list --json` and `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`.
|
|
35
|
+
- When starting a long-lived reference app dev server for framework work, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit thread-scoped session file such as `var/run/proteum/dev/framework-<app>-<task>.json`, inspect tracked sessions plus current listeners first, for example with `npx proteum dev list --json` and `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`. After the server is ready, print the live server URL as a clickable Markdown link such as `[http://localhost:3100](http://localhost:3100)`.
|
|
36
36
|
- Do not use `--replace-existing` unless you are restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
|
|
37
37
|
- When a reference app uses local `file:` connected projects for the affected flow, boot every connected producer app as well, each on its own free port and thread-scoped session file, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before starting or validating the consumer app.
|
|
38
38
|
- Before retrying a boot on the same app, changing ports, or finishing the task, stop every framework-started dev session with `npx proteum dev stop --session-file <path>` or `npx proteum dev stop --all --stale`.
|
|
@@ -51,11 +51,13 @@ npx prisma migrate dev --config ./prisma.config.ts --name <migration name>
|
|
|
51
51
|
|
|
52
52
|
- Validate framework changes against the reference apps:
|
|
53
53
|
- `/Users/gaetan/Desktop/Projets/crosspath/platform`: Standalone app
|
|
54
|
+
- `/Users/gaetan/Desktop/Projets/crosspath/website`: Standalone app
|
|
54
55
|
- `/Users/gaetan/Desktop/Projets/unique.domains/platform`: Monorepo including the following apps:
|
|
55
56
|
- `/Users/gaetan/Desktop/Projets/unique.domains/platform/apps/product`
|
|
56
57
|
- `/Users/gaetan/Desktop/Projets/unique.domains/platform/apps/website`
|
|
57
58
|
- Inspect how both apps currently use the touched feature, runtime, API, compiler behavior, or generated output before proposing or implementing changes.
|
|
58
59
|
- Keep the developer-facing contract synchronized when framework work changes CLI commands, profiler capabilities, or the `proteum dev` banner. Update the live surfaces together in the same pass: CLI command/help definitions, profiler panels and dev-only endpoints, banner text/examples, and the most relevant agent docs that describe them, especially `AGENTS.md`, `agents/project/AGENTS.md`, `agents/project/root/AGENTS.md`, `agents/project/app-root/AGENTS.md`, `agents/project/diagnostics.md`, and any narrower `agents/project/**/AGENTS.md` file that mentions the changed workflow.
|
|
60
|
+
- Keep the same-system trace contract explicit when request instrumentation changes: `TRACE_*` controls the retained dev trace store plus the trace/perf CLI, dev-only HTTP endpoints, and bottom profiler, while `ENABLE_PROFILER` enables the reduced request-local `request.profiling` snapshot and `request.finished` hook payload without retaining finished requests globally unless dev trace is also enabled.
|
|
59
61
|
- Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the banner. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner.
|
|
60
62
|
- Keep core changes aligned with the explicit controller/page architecture in `agents/project/root/AGENTS.md` and its standalone composition in `agents/project/AGENTS.md`.
|
|
61
63
|
- Prefer removing framework magic when the same result can be expressed with explicit contracts, generated code, or typed context.
|
package/README.md
CHANGED
|
@@ -6,6 +6,12 @@ It is built for teams that want explicit server contracts, server-first renderin
|
|
|
6
6
|
|
|
7
7
|
Migration guide for older apps: [docs/migrate-from-2.1.3.md](docs/migrate-from-2.1.3.md)
|
|
8
8
|
|
|
9
|
+
## Sponsor
|
|
10
|
+
|
|
11
|
+
Proteum is sponsored by [Unique Domains](https://unique.domains/?utm_source=github&utm_medium=referral&utm_campaign=repo_proteum&utm_content=top_sponsor).
|
|
12
|
+
|
|
13
|
+
[](https://unique.domains/?utm_source=github&utm_medium=referral&utm_campaign=repo_proteum&utm_content=top_sponsor)
|
|
14
|
+
|
|
9
15
|
## Why Proteum
|
|
10
16
|
|
|
11
17
|
Most full-stack frameworks optimize first for human convenience.
|
|
@@ -101,6 +107,7 @@ Optional trace env vars:
|
|
|
101
107
|
- `TRACE_EVENTS_LIMIT`
|
|
102
108
|
- `TRACE_CAPTURE`
|
|
103
109
|
- `TRACE_PERSIST_ON_ERROR`
|
|
110
|
+
- `ENABLE_PROFILER`
|
|
104
111
|
|
|
105
112
|
Optional `proteum.config.ts` fields:
|
|
106
113
|
|
|
@@ -464,6 +471,7 @@ Default behavior:
|
|
|
464
471
|
- payloads are summarized, long strings are truncated, and sensitive fields such as cookies, passwords, and tokens are redacted
|
|
465
472
|
- `TRACE_PERSIST_ON_ERROR` can export crashing requests under `var/traces/`
|
|
466
473
|
- `proteum dev` removes auto-persisted crash traces from `var/traces/` when the dev session stops
|
|
474
|
+
- `ENABLE_PROFILER=true` reuses the same instrumentation path to populate `request.profiling` and the router `request.finished` hook with a reduced request/API/SQL snapshot in any environment, without retaining finished requests in the global trace buffer unless dev trace is also enabled
|
|
467
475
|
|
|
468
476
|
Trace env example:
|
|
469
477
|
|
|
@@ -473,6 +481,7 @@ export TRACE_REQUESTS_LIMIT=200
|
|
|
473
481
|
export TRACE_EVENTS_LIMIT=800
|
|
474
482
|
export TRACE_CAPTURE=resolve
|
|
475
483
|
export TRACE_PERSIST_ON_ERROR=true
|
|
484
|
+
export ENABLE_PROFILER=true
|
|
476
485
|
```
|
|
477
486
|
|
|
478
487
|
Capture modes:
|
|
@@ -500,7 +509,7 @@ Proteum answers those questions with explicit artifacts:
|
|
|
500
509
|
|
|
501
510
|
- `identity.config.ts` for app identity
|
|
502
511
|
- `proteum.config.ts` for compiler and connected-project setup
|
|
503
|
-
- `PORT`, `ENV_*`, `URL`, `URL_INTERNAL`, app-chosen connected-project config values, and `
|
|
512
|
+
- `PORT`, `ENV_*`, `URL`, `URL_INTERNAL`, app-chosen connected-project config values, `TRACE_*`, and `ENABLE_PROFILER` env vars for the environment surface
|
|
504
513
|
- `server/index.ts` for the explicit root service graph
|
|
505
514
|
- `.proteum/manifest.json` for machine-readable app structure
|
|
506
515
|
- `proteum explain --json` for structured framework introspection
|
|
@@ -516,7 +525,7 @@ Proteum answers those questions with explicit artifacts:
|
|
|
516
525
|
If you are an LLM or automation agent, start here:
|
|
517
526
|
|
|
518
527
|
1. Read `identity.config.ts` and `proteum.config.ts`.
|
|
519
|
-
2. Read `PORT`, the relevant `ENV_*`, `URL`, `URL_INTERNAL`, any env values referenced by `proteum.config.ts`,
|
|
528
|
+
2. Read `PORT`, the relevant `ENV_*`, `URL`, `URL_INTERNAL`, any env values referenced by `proteum.config.ts`, plus `TRACE_*` and `ENABLE_PROFILER`, or run `proteum explain env`.
|
|
520
529
|
3. Inspect `server/index.ts` and `server/config/*.ts` for the explicit app bootstrap.
|
|
521
530
|
4. Read `.proteum/manifest.json` or run `proteum explain --json`.
|
|
522
531
|
5. Inspect `server/controllers/**` for request entrypoints.
|
package/agents/project/AGENTS.md
CHANGED
|
@@ -17,12 +17,14 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
|
|
|
17
17
|
- Run `npx proteum refresh`.
|
|
18
18
|
- Read and acknowledge the applicable `AGENTS.md` files.
|
|
19
19
|
- Run `npm i`.
|
|
20
|
-
- Run the dev server with the task-safe elevated-permissions launch workflow from `Task Lifecycle`,
|
|
21
|
-
- If the user pastes raw errors without asking for a fix, do not implement changes. List likely causes and, for each one, give probability, why, and how to fix it.
|
|
20
|
+
- Run the dev server with the task-safe elevated-permissions launch workflow from `Task Lifecycle`, keep it running so user can see the results by himself, and print the live server URL as a clickable Markdown link.
|
|
21
|
+
- If the user pastes raw errors without asking for a fix, do not implement changes. List likely causes and, for each one, give probability, why, and how to fix it. After this, everytime you implemented a fix
|
|
22
|
+
- test, re-run analysis and give a comparizon table of before and after
|
|
23
|
+
- re-print the complete list of suggested fixes, but strike the ones we already implemented or not necessary anymore
|
|
22
24
|
- If the task is ambiguous, generated, connected, or multi-repo, start with `npx proteum orient <query>` before reading large parts of the codebase.
|
|
23
25
|
- If the user reports an issue, or the agent encounters one during exploration, implementation, verification, or runtime reproduction, load and follow root-level `diagnostics.md`.
|
|
24
26
|
- If the task touches client-side files, especially `client/**` and page files, load and apply root-level `optimizations.md` only after implementation for post-implementation checking and optimization. Skip it at task start and skip it for server-only, test-only, doc-only, and non-client refactor tasks unless the user explicitly asks for optimization work.
|
|
25
|
-
- If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, read the relevant files under `./docs/` first, especially `docs/PERSONAS.md`, `docs/PRODUCT.md`, and `docs/MARKETING.md` when they exist. If a dev server is already running, print the dev server URL.
|
|
27
|
+
- If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, read the relevant files under `./docs/` first, especially `docs/PERSONAS.md`, `docs/PRODUCT.md`, and `docs/MARKETING.md` when they exist. If a dev server is already running, print the live dev server URL as a clickable Markdown link.
|
|
26
28
|
- If the task needs new app or artifact boilerplate, prefer `npx proteum init ...` and `npx proteum create ...` before creating files by hand. Use `--dry-run --json` when an agent needs a machine-readable plan before writing files.
|
|
27
29
|
- If you changed `schema.prisma`, do not start testing or validation yet. Ask the user to run the following command in the affected worktree directory, replacing the placeholders, and wait for the user to reply exactly `continue` before resuming validation or tests:
|
|
28
30
|
```
|
|
@@ -51,7 +53,7 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
|
|
|
51
53
|
### During Implementation
|
|
52
54
|
|
|
53
55
|
- After running `npx proteum create ...`, adapt the generated code to the real feature instead of leaving placeholder logic in place.
|
|
54
|
-
- When starting a long-lived dev server for an agent task, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file such as `var/run/proteum/dev/agents/<task>.json`, inspect `npx proteum dev list --json` plus current listeners first, for example with `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`.
|
|
56
|
+
- When starting a long-lived dev server for an agent task, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file such as `var/run/proteum/dev/agents/<task>.json`, inspect `npx proteum dev list --json` plus current listeners first, for example with `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`. After the server is ready, print the live server URL as a clickable Markdown link.
|
|
55
57
|
- Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
|
|
56
58
|
- If the current app depends on local `file:` connected projects, boot every connected producer app too, each with its own task-scoped session file and free port, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before starting or verifying the consumer app.
|
|
57
59
|
- For raw browser automation, use `npx proteum verify browser` when it matches the task, or direct Playwright with a disposable profile when lower-level control is required. Bootstrap protected browser state through `npx proteum session`.
|
|
@@ -60,7 +62,7 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
|
|
|
60
62
|
### Before Finishing
|
|
61
63
|
|
|
62
64
|
- Before finishing, re-check touched files against root-level `CODING_STYLE.md` and any narrower area `AGENTS.md` that applied to the edit. Re-check against root-level `optimizations.md` only for touched client-side files. Re-check against root-level `diagnostics.md` only if the task involved an issue, diagnosis, runtime reproduction, or verification failure.
|
|
63
|
-
- Do not default to project-wide typecheck
|
|
65
|
+
- Do not default to project-wide typecheck or `npx proteum check` after every change. After implementing a new feature or changing existing feature behavior, always update the end-to-end coverage for that behavior and run the full Playwright test suite before finishing. For docs-only, wording-only, type-only, test-only, generated-output cleanup, or clearly local non-runtime refactors, skip Playwright unless the user explicitly asks for it or verification reveals a real issue.
|
|
64
66
|
- Before finishing a task, stop every `proteum dev` session started during the task and confirm cleanup with `npx proteum dev list --json` or an explicit `npx proteum dev stop --session-file <path>`.
|
|
65
67
|
- When you have finished your work, ask the user whether they want a commit message. After providing a commit message or after creating a commit, immediately follow it with this exact prompt and obey it:
|
|
66
68
|
`Explain in short minimalistic and few bullet points what we changed in this thread, like you would do to your grandma. Start with a verb in the past.`
|
|
@@ -96,6 +98,7 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
|
|
|
96
98
|
- Root services are public class fields instantiated with `new ServiceClass(this, config, this)`.
|
|
97
99
|
- Typed root-service config lives in `server/config/*.ts` via `Services.config(ServiceClass, { ... })`.
|
|
98
100
|
- Router plugins are instantiated explicitly inside the `Router` config `plugins` object.
|
|
101
|
+
- Router plugins can subscribe to `request` and `request.finished`; `request.profiling` exists before `request` runs and carries the finalized request/API/SQL snapshot by `request.finished`.
|
|
99
102
|
- Root business services live in `server/services/<Feature>/index.ts`.
|
|
100
103
|
- Root-service config lives in `server/config/*.ts` when the service needs config.
|
|
101
104
|
- Business logic lives in classes that extend `Service` and use `this.services`, `this.models`, and `this.app`.
|
|
@@ -174,6 +177,7 @@ Verify at the correct layer:
|
|
|
174
177
|
- Controller changes: exercise the generated client call or generated `/api/...` endpoint.
|
|
175
178
|
- SSR changes: load the real page and inspect rendered HTML plus browser console.
|
|
176
179
|
- Router or plugin changes: verify request context, auth, redirects, metrics, and validation on a running app.
|
|
180
|
+
- New features or feature-behavior changes: use the cheapest trustworthy verification while iterating, then update the relevant end-to-end coverage and finish by running the full Playwright suite.
|
|
177
181
|
- Generated, connected, or ownership-ambiguous changes: start with `npx proteum orient <query>` and prefer `npx proteum verify owner <query>` before broad global checks.
|
|
178
182
|
- Browser-visible issues: prefer `npx proteum verify browser <path>` or the narrowest targeted Playwright pass only after request-level verification is insufficient.
|
|
179
183
|
- Raw browser execution beyond `npx proteum verify browser`: use direct Playwright with a disposable profile, and keep that step for the final verifier agent unless a narrower surface cannot reproduce the issue.
|
|
@@ -238,7 +242,7 @@ Proteum reads:
|
|
|
238
242
|
- `package.json`
|
|
239
243
|
- `identity.config.ts` for app identity via `Application.identity({ ... })`
|
|
240
244
|
- `proteum.config.ts` for compiler setup via `Application.setup({ transpile, connect })`
|
|
241
|
-
- `process.env` via `PORT`, `ENV_*`, `URL`, `URL_INTERNAL`, any app-chosen connected-project values referenced by `proteum.config.ts`, and `
|
|
245
|
+
- `process.env` via `PORT`, `ENV_*`, `URL`, `URL_INTERNAL`, any app-chosen connected-project values referenced by `proteum.config.ts`, `TRACE_*`, and `ENABLE_PROFILER`
|
|
242
246
|
- `server/config/*.ts`
|
|
243
247
|
- `server/index.ts`
|
|
244
248
|
- `commands/**/*.ts`
|
|
@@ -298,6 +302,6 @@ Prefer scaffold commands before hand-writing boilerplate:
|
|
|
298
302
|
Edit these only when required, and keep changes minimal and explicit:
|
|
299
303
|
|
|
300
304
|
- `tsconfig*.json`
|
|
301
|
-
- `PORT`, `ENV_*`, `URL`, and `
|
|
305
|
+
- `PORT`, `ENV_*`, `URL`, `TRACE_*`, and `ENABLE_PROFILER` env setup
|
|
302
306
|
- Prisma-generated files
|
|
303
307
|
- symbolic links
|
|
@@ -12,5 +12,5 @@ Do not put here: reusable Proteum architecture contracts, shared verification ru
|
|
|
12
12
|
- Run `npx proteum refresh`.
|
|
13
13
|
- Read and acknowledge the applicable `AGENTS.md` files.
|
|
14
14
|
- Run `npm i`.
|
|
15
|
-
- Run the dev server with the task-safe elevated-permissions launch workflow from the reusable root `AGENTS.md`,
|
|
16
|
-
- If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, read the relevant files under `./docs/` first, especially `docs/PERSONAS.md`, `docs/PRODUCT.md`, and `docs/MARKETING.md` when they exist. If a dev server is already running, print the dev server URL.
|
|
15
|
+
- Run the dev server with the task-safe elevated-permissions launch workflow from the reusable root `AGENTS.md`, keep it running so user can see the results by himself, and print the live server URL as a clickable Markdown link.
|
|
16
|
+
- If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, read the relevant files under `./docs/` first, especially `docs/PERSONAS.md`, `docs/PRODUCT.md`, and `docs/MARKETING.md` when they exist. If a dev server is already running, print the live dev server URL as a clickable Markdown link.
|
|
@@ -12,7 +12,7 @@ This file is the canonical source of truth for diagnostics, temporary instrument
|
|
|
12
12
|
|
|
13
13
|
## Runtime Diagnostics
|
|
14
14
|
|
|
15
|
-
- For long-lived dev reproductions, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file, inspect `npx proteum dev list --json` plus current listeners first, for example with `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`.
|
|
15
|
+
- For long-lived dev reproductions, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file, inspect `npx proteum dev list --json` plus current listeners first, for example with `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`. After the server is ready, print the live server URL as a clickable Markdown link.
|
|
16
16
|
- Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
|
|
17
17
|
- Only the bare `npx proteum build` and bare `npx proteum dev` commands print the welcome banner and active Proteum installation method. Any extra argument or option skips the banner. Only `npx proteum dev` clears the interactive terminal before rendering and reports connected app names plus successful connected `/ping` checks in the ready banner; keep that in mind when capturing or comparing command logs during diagnosis.
|
|
18
18
|
- For ownership or repo discovery questions, start with `npx proteum orient <query>` instead of jumping straight into source searches.
|
|
@@ -43,7 +43,7 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
|
|
|
43
43
|
### During Implementation
|
|
44
44
|
|
|
45
45
|
- After running `npx proteum create ...`, adapt the generated code to the real feature instead of leaving placeholder logic in place.
|
|
46
|
-
- When starting a long-lived dev server for an agent task, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file such as `var/run/proteum/dev/agents/<task>.json`, inspect `npx proteum dev list --json` plus current listeners first, for example with `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`.
|
|
46
|
+
- When starting a long-lived dev server for an agent task, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file such as `var/run/proteum/dev/agents/<task>.json`, inspect `npx proteum dev list --json` plus current listeners first, for example with `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`. After the server is ready, print the live server URL as a clickable Markdown link.
|
|
47
47
|
- Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
|
|
48
48
|
- If the current app depends on local `file:` connected projects, boot every connected producer app too, each with its own task-scoped session file and free port, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before starting or verifying the consumer app.
|
|
49
49
|
- For raw browser automation, use `npx proteum verify browser` when it matches the task, or direct Playwright with a disposable profile when lower-level control is required. Bootstrap protected browser state through `npx proteum session`.
|
|
@@ -52,7 +52,7 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
|
|
|
52
52
|
### Before Finishing
|
|
53
53
|
|
|
54
54
|
- Before finishing, re-check touched files against root-level `CODING_STYLE.md` and any narrower area `AGENTS.md` that applied to the edit. Re-check against root-level `optimizations.md` only for touched client-side files. Re-check against root-level `diagnostics.md` only if the task involved an issue, diagnosis, runtime reproduction, or verification failure.
|
|
55
|
-
- Do not default to project-wide typecheck
|
|
55
|
+
- Do not default to project-wide typecheck or `npx proteum check` after every change. After implementing a new feature or changing existing feature behavior, always update the end-to-end coverage for that behavior and run the full Playwright test suite before finishing. For docs-only, wording-only, type-only, test-only, generated-output cleanup, or clearly local non-runtime refactors, skip Playwright unless the user explicitly asks for it or verification reveals a real issue.
|
|
56
56
|
- Before finishing a task, stop every `proteum dev` session started during the task and confirm cleanup with `npx proteum dev list --json` or an explicit `npx proteum dev stop --session-file <path>`.
|
|
57
57
|
- When you have finished your work, ask the user whether they want a commit message. After providing a commit message or after creating a commit, immediately follow it with this exact prompt and obey it:
|
|
58
58
|
`Explain in short minimalistic and few bullet points what we changed in this thread, like you would do to your grandma. Start with a verb in the past.`
|
|
@@ -88,6 +88,7 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
|
|
|
88
88
|
- Root services are public class fields instantiated with `new ServiceClass(this, config, this)`.
|
|
89
89
|
- Typed root-service config lives in `server/config/*.ts` via `Services.config(ServiceClass, { ... })`.
|
|
90
90
|
- Router plugins are instantiated explicitly inside the `Router` config `plugins` object.
|
|
91
|
+
- Router plugins can subscribe to `request` and `request.finished`; `request.profiling` exists before `request` runs and carries the finalized request/API/SQL snapshot by `request.finished`.
|
|
91
92
|
- Root business services live in `server/services/<Feature>/index.ts`.
|
|
92
93
|
- Root-service config lives in `server/config/*.ts` when the service needs config.
|
|
93
94
|
- Business logic lives in classes that extend `Service` and use `this.services`, `this.models`, and `this.app`.
|
|
@@ -166,6 +167,7 @@ Verify at the correct layer:
|
|
|
166
167
|
- Controller changes: exercise the generated client call or generated `/api/...` endpoint.
|
|
167
168
|
- SSR changes: load the real page and inspect rendered HTML plus browser console.
|
|
168
169
|
- Router or plugin changes: verify request context, auth, redirects, metrics, and validation on a running app.
|
|
170
|
+
- New features or feature-behavior changes: use the cheapest trustworthy verification while iterating, then update the relevant end-to-end coverage and finish by running the full Playwright suite.
|
|
169
171
|
- Generated, connected, or ownership-ambiguous changes: start with `npx proteum orient <query>` and prefer `npx proteum verify owner <query>` before broad global checks.
|
|
170
172
|
- Browser-visible issues: prefer `npx proteum verify browser <path>` or the narrowest targeted Playwright pass only after request-level verification is insufficient.
|
|
171
173
|
- Raw browser execution beyond `npx proteum verify browser`: use direct Playwright with a disposable profile, and keep that step for the final verifier agent unless a narrower surface cannot reproduce the issue.
|
|
@@ -230,7 +232,7 @@ Proteum reads:
|
|
|
230
232
|
- `package.json`
|
|
231
233
|
- `identity.config.ts` for app identity via `Application.identity({ ... })`
|
|
232
234
|
- `proteum.config.ts` for compiler setup via `Application.setup({ transpile, connect })`
|
|
233
|
-
- `process.env` via `PORT`, `ENV_*`, `URL`, `URL_INTERNAL`, any app-chosen connected-project values referenced by `proteum.config.ts`, and `
|
|
235
|
+
- `process.env` via `PORT`, `ENV_*`, `URL`, `URL_INTERNAL`, any app-chosen connected-project values referenced by `proteum.config.ts`, `TRACE_*`, and `ENABLE_PROFILER`
|
|
234
236
|
- `server/config/*.ts`
|
|
235
237
|
- `server/index.ts`
|
|
236
238
|
- `commands/**/*.ts`
|
|
@@ -290,6 +292,6 @@ Prefer scaffold commands before hand-writing boilerplate:
|
|
|
290
292
|
Edit these only when required, and keep changes minimal and explicit:
|
|
291
293
|
|
|
292
294
|
- `tsconfig*.json`
|
|
293
|
-
- `PORT`, `ENV_*`, `URL`, and `
|
|
295
|
+
- `PORT`, `ENV_*`, `URL`, `TRACE_*`, and `ENABLE_PROFILER` env setup
|
|
294
296
|
- Prisma-generated files
|
|
295
297
|
- symbolic links
|
|
@@ -10,9 +10,16 @@ Diagnostics source of truth: root-level `diagnostics.md`.
|
|
|
10
10
|
- Understand the real user flow and the main feature branches before writing tests.
|
|
11
11
|
- Test the current controller/page runtime model, not legacy `@Route` or `api.fetch(...)` behavior.
|
|
12
12
|
- Verify routing, controllers, SSR, and router plugins against a running app when behavior depends on real request handling.
|
|
13
|
-
- After implementing a
|
|
13
|
+
- After implementing a new feature or changing existing feature behavior, update the end-to-end coverage for that behavior and run the full Playwright suite before finishing. Use a real browser repro against a running app during iteration when it is the fastest trustworthy loop.
|
|
14
14
|
- Exercise real URLs, generated controller calls, or real browser flows instead of re-deriving framework internals in tests.
|
|
15
|
+
- Organize end-to-end tests following the Crosspath platform layout under `tests/e2e/**`.
|
|
16
|
+
- Put runnable scenario entrypoints in `tests/e2e/features/**`, `tests/e2e/specs/<domain>/**`, or `tests/e2e/journeys/**` depending on scope.
|
|
17
|
+
- Put page objects and reusable UI surface wrappers in `tests/e2e/pages/**`.
|
|
18
|
+
- Put reusable multi-step user flows in `tests/e2e/workflows/**`.
|
|
19
|
+
- Put test data builders in `tests/e2e/factories/**` and generic helpers in `tests/e2e/utils/**`.
|
|
20
|
+
- Keep helpers out of spec files when they are reusable, and do not create ad hoc flat test files or duplicate support abstractions when an existing page, workflow, factory, or utility already fits.
|
|
15
21
|
- Locate elements with `data-testid`.
|
|
16
22
|
- Add `data-testid` where needed instead of relying on brittle selectors.
|
|
23
|
+
- Keep end-to-end tests clean, well organized, and non-redundant. Prefer extending or reshaping the most relevant existing scenario over duplicating coverage, and remove or consolidate overlap when the suite becomes repetitive.
|
|
17
24
|
- Reuse root catalog files from `/client/catalogs/**`, `/server/catalogs/**`, or `/common/catalogs/**` instead of duplicating catalog constants in tests.
|
|
18
25
|
- For protected dev flows, prefer `npx proteum session <email> --role <role>` over automating login unless the login flow itself is under test.
|
|
@@ -114,6 +114,7 @@ export type TTraceSqlQuery = {
|
|
|
114
114
|
model?: string;
|
|
115
115
|
operation: string;
|
|
116
116
|
fingerprint?: string;
|
|
117
|
+
normalizedQuery?: string;
|
|
117
118
|
ownerLabel?: string;
|
|
118
119
|
ownerFilepath?: string;
|
|
119
120
|
serviceLabel?: string;
|
|
@@ -169,6 +170,71 @@ export type TRequestTrace = {
|
|
|
169
170
|
events: TTraceEvent[];
|
|
170
171
|
};
|
|
171
172
|
|
|
173
|
+
export type TRequestProfilingApiCall = {
|
|
174
|
+
id: string;
|
|
175
|
+
origin: TTraceCallOrigin;
|
|
176
|
+
label: string;
|
|
177
|
+
method: string;
|
|
178
|
+
path: string;
|
|
179
|
+
fetcherId?: string;
|
|
180
|
+
connectedProjectNamespace?: string;
|
|
181
|
+
connectedControllerAccessor?: string;
|
|
182
|
+
ownerLabel?: string;
|
|
183
|
+
ownerFilepath?: string;
|
|
184
|
+
serviceLabel?: string;
|
|
185
|
+
startedAt: string;
|
|
186
|
+
finishedAt?: string;
|
|
187
|
+
durationMs?: number;
|
|
188
|
+
statusCode?: number;
|
|
189
|
+
errorMessage?: string;
|
|
190
|
+
requestBodyJson?: unknown;
|
|
191
|
+
responseBodyJson?: unknown;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export type TRequestProfilingSqlQuery = {
|
|
195
|
+
id: string;
|
|
196
|
+
callerCallId?: string;
|
|
197
|
+
callerFetcherId?: string;
|
|
198
|
+
callerLabel?: string;
|
|
199
|
+
callerMethod: string;
|
|
200
|
+
callerOrigin: TTraceSqlQueryCallerOrigin;
|
|
201
|
+
callerPath: string;
|
|
202
|
+
durationMs: number;
|
|
203
|
+
finishedAt: string;
|
|
204
|
+
kind: TTraceSqlQueryKind;
|
|
205
|
+
model?: string;
|
|
206
|
+
operation: string;
|
|
207
|
+
fingerprint?: string;
|
|
208
|
+
normalizedQuery?: string;
|
|
209
|
+
ownerLabel?: string;
|
|
210
|
+
ownerFilepath?: string;
|
|
211
|
+
serviceLabel?: string;
|
|
212
|
+
connectedNamespace?: string;
|
|
213
|
+
paramsJson?: unknown;
|
|
214
|
+
paramsText?: string;
|
|
215
|
+
query: string;
|
|
216
|
+
startedAt: string;
|
|
217
|
+
target?: string;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export type TRequestProfiling = {
|
|
221
|
+
enabled: boolean;
|
|
222
|
+
requestId: string;
|
|
223
|
+
method: string;
|
|
224
|
+
path: string;
|
|
225
|
+
url: string;
|
|
226
|
+
startedAt: string;
|
|
227
|
+
finishedAt?: string;
|
|
228
|
+
durationMs?: number;
|
|
229
|
+
statusCode?: number;
|
|
230
|
+
user?: string;
|
|
231
|
+
errorMessage?: string;
|
|
232
|
+
profilerOrigin?: string;
|
|
233
|
+
profilerParentRequestId?: string;
|
|
234
|
+
apiCalls: TRequestProfilingApiCall[];
|
|
235
|
+
sqlQueries: TRequestProfilingSqlQuery[];
|
|
236
|
+
};
|
|
237
|
+
|
|
172
238
|
export type TRequestTraceListItem = Omit<TRequestTrace, 'events' | 'calls' | 'sqlQueries'> & {
|
|
173
239
|
eventCount: number;
|
|
174
240
|
callCount: number;
|
package/common/env/proteumEnv.ts
CHANGED
|
@@ -36,6 +36,7 @@ export type TProteumEnvConfig = {
|
|
|
36
36
|
connectedProjects: Record<string, TProteumConnectedProjectEnvConfig>;
|
|
37
37
|
trace: {
|
|
38
38
|
enable: boolean;
|
|
39
|
+
profilerEnable: boolean;
|
|
39
40
|
requestsLimit: number;
|
|
40
41
|
eventsLimit: number;
|
|
41
42
|
capture: TProteumTraceCapture;
|
|
@@ -65,7 +66,7 @@ const isProvidedEnvValue = (value: string | undefined) => typeof value === 'stri
|
|
|
65
66
|
|
|
66
67
|
const buildRequiredEnvVariableDefinitions = (_connectedProjects: TConnectedProjectsConfig) => [...baseRequiredProteumEnvVariableDefinitions];
|
|
67
68
|
|
|
68
|
-
const buildOptionalEnvKeys = (_connectedProjects: TConnectedProjectsConfig) => [] as string[];
|
|
69
|
+
const buildOptionalEnvKeys = (_connectedProjects: TConnectedProjectsConfig) => ['ENABLE_PROFILER'] as string[];
|
|
69
70
|
|
|
70
71
|
const formatRequiredEnvVariableStatus = (variable: TProteumRequiredEnvVariable) =>
|
|
71
72
|
`- ${variable.key} possibleValues=${variable.possibleValues.join(' | ')} provided=${variable.provided ? 'yes' : 'no'}`;
|
|
@@ -269,8 +270,8 @@ export const loadOptionalProteumDotenv = (appDir: string) => {
|
|
|
269
270
|
};
|
|
270
271
|
|
|
271
272
|
export const getLoadedProteumEnvVariableKeys = (connectedProjects: TConnectedProjectsConfig = {}) => {
|
|
272
|
-
const requiredKeys = new Set(buildRequiredEnvVariableDefinitions(connectedProjects).map((definition) => definition.key));
|
|
273
|
-
const optionalKeys = new Set(buildOptionalEnvKeys(connectedProjects));
|
|
273
|
+
const requiredKeys = new Set<string>(buildRequiredEnvVariableDefinitions(connectedProjects).map((definition) => definition.key));
|
|
274
|
+
const optionalKeys = new Set<string>(buildOptionalEnvKeys(connectedProjects));
|
|
274
275
|
|
|
275
276
|
return Object.keys(process.env)
|
|
276
277
|
.filter(
|
|
@@ -338,6 +339,11 @@ export const parseProteumEnvConfig = ({
|
|
|
338
339
|
value: process.env.TRACE_ENABLE,
|
|
339
340
|
context,
|
|
340
341
|
});
|
|
342
|
+
const profilerEnable = parseBooleanEnvValue({
|
|
343
|
+
key: 'ENABLE_PROFILER',
|
|
344
|
+
value: process.env.ENABLE_PROFILER,
|
|
345
|
+
context,
|
|
346
|
+
});
|
|
341
347
|
const tracePersistOnError = parseBooleanEnvValue({
|
|
342
348
|
key: 'TRACE_PERSIST_ON_ERROR',
|
|
343
349
|
value: process.env.TRACE_PERSIST_ON_ERROR,
|
|
@@ -387,6 +393,7 @@ export const parseProteumEnvConfig = ({
|
|
|
387
393
|
connectedProjects: resolvedConnectedProjects,
|
|
388
394
|
trace: {
|
|
389
395
|
enable: traceEnable ?? profile === 'dev',
|
|
396
|
+
profilerEnable: profilerEnable ?? false,
|
|
390
397
|
requestsLimit:
|
|
391
398
|
traceRequestsLimit === undefined || traceRequestsLimit === ''
|
|
392
399
|
? 200
|
|
Binary file
|
package/docs/request-tracing.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# Request Tracing
|
|
2
2
|
|
|
3
|
-
Proteum ships with
|
|
3
|
+
Proteum ships with one request-instrumentation system with two runtime shapes:
|
|
4
|
+
|
|
5
|
+
- retained dev traces for `proteum trace`, `proteum perf`, the dev-only HTTP endpoints, and the bottom profiler
|
|
6
|
+
- reduced request-local profiling for `request.profiling` and the router `request.finished` hook
|
|
7
|
+
|
|
8
|
+
The same API and SQL instrumentation feeds both shapes. Dev trace keeps the in-memory buffer and event timeline. Reduced profiling keeps only the finalized request/API/SQL snapshot and releases it after the `request.finished` hook runs.
|
|
4
9
|
|
|
5
10
|
## Scope
|
|
6
11
|
|
|
7
|
-
- tracing is available only when the app runs with `profile: dev`
|
|
12
|
+
- retained dev tracing is available only when the app runs with `profile: dev`
|
|
8
13
|
- traces are exposed through `proteum trace`, `proteum perf`, and the dev-only `__proteum/trace` and `__proteum/perf` HTTP endpoints
|
|
9
14
|
- `proteum diagnose` is a separate composite surface that reads the same framework diagnostics plus one matching request trace and buffered server logs; see [diagnostics.md](diagnostics.md)
|
|
10
|
-
-
|
|
15
|
+
- `ENABLE_PROFILER=true` enables reduced request-local profiling in any environment, including production
|
|
11
16
|
|
|
12
17
|
## Main Commands
|
|
13
18
|
|
|
@@ -77,6 +82,13 @@ Depending on capture mode, traces can include:
|
|
|
77
82
|
- normalized request errors
|
|
78
83
|
- additive owner, service, cache, and connected-boundary metadata propagated from route/controller resolution into downstream calls and SQL
|
|
79
84
|
|
|
85
|
+
Reduced request-local profiling keeps the finalized request summary plus API and SQL rows only:
|
|
86
|
+
|
|
87
|
+
- `request.profiling` exists before the router `request` hook runs
|
|
88
|
+
- `request.profiling.apiCalls` and `request.profiling.sqlQueries` start empty and are populated during request handling
|
|
89
|
+
- the router `request.finished` hook receives that same object after status, duration, API calls, and SQL queries are finalized
|
|
90
|
+
- when only reduced profiling is enabled, finished requests are released immediately after `request.finished` instead of being retained in the global trace buffer
|
|
91
|
+
|
|
80
92
|
## SQL Tracing
|
|
81
93
|
|
|
82
94
|
Prisma query tracing covers both ORM operations and raw queries.
|
|
@@ -140,6 +152,7 @@ export TRACE_REQUESTS_LIMIT=200
|
|
|
140
152
|
export TRACE_EVENTS_LIMIT=800
|
|
141
153
|
export TRACE_CAPTURE=resolve
|
|
142
154
|
export TRACE_PERSIST_ON_ERROR=true
|
|
155
|
+
export ENABLE_PROFILER=true
|
|
143
156
|
```
|
|
144
157
|
|
|
145
158
|
Notes:
|
|
@@ -150,6 +163,7 @@ Notes:
|
|
|
150
163
|
- `eventsLimit` defaults to `800`
|
|
151
164
|
- `proteum dev` removes auto-persisted crash traces from `var/traces/` when the dev session stops
|
|
152
165
|
- explicit `proteum trace export` files under `var/traces/exports/` are left in place
|
|
166
|
+
- `ENABLE_PROFILER` reuses the same request instrumentation path but skips the retained global buffer and event timeline when dev trace is otherwise off
|
|
153
167
|
|
|
154
168
|
## Memory Model
|
|
155
169
|
|
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.2.
|
|
4
|
+
"version": "2.2.1",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
type TTraceSqlQueryCallerOrigin,
|
|
17
17
|
type TTraceSqlQueryKind,
|
|
18
18
|
type TTraceSummaryValue,
|
|
19
|
+
type TRequestProfiling,
|
|
20
|
+
type TRequestProfilingApiCall,
|
|
21
|
+
type TRequestProfilingSqlQuery,
|
|
19
22
|
type TRequestTrace,
|
|
20
23
|
type TTraceMemorySnapshot,
|
|
21
24
|
type TRequestTraceListItem,
|
|
@@ -24,6 +27,7 @@ import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
|
24
27
|
|
|
25
28
|
export type Config = {
|
|
26
29
|
enable: boolean;
|
|
30
|
+
profilerEnable: boolean;
|
|
27
31
|
requestsLimit: number;
|
|
28
32
|
eventsLimit: number;
|
|
29
33
|
capture: TTraceCaptureMode;
|
|
@@ -32,6 +36,12 @@ export type Config = {
|
|
|
32
36
|
|
|
33
37
|
type TTraceInspectable = object | PrimitiveValue | bigint | symbol | null | undefined | (() => void);
|
|
34
38
|
type TTraceDetails = { [key: string]: TTraceInspectable };
|
|
39
|
+
type TSerializeJsonValueOptions = { redactSensitive: boolean };
|
|
40
|
+
type TActiveRequestRecord = {
|
|
41
|
+
profiling: TRequestProfiling;
|
|
42
|
+
trace?: TRequestTrace;
|
|
43
|
+
capture?: TTraceCaptureMode;
|
|
44
|
+
};
|
|
35
45
|
|
|
36
46
|
const capturePriority: Record<TTraceCaptureMode, number> = { summary: 0, resolve: 1, deep: 2 };
|
|
37
47
|
const sensitiveKeyPattern =
|
|
@@ -50,8 +60,13 @@ const isSensitiveKeyPath = (keyPath: string[]) => sensitiveKeyPattern.test(keyPa
|
|
|
50
60
|
const summarizeString = (value: string) =>
|
|
51
61
|
value.length <= maxStringLength ? value : `${value.slice(0, maxStringLength)}…`;
|
|
52
62
|
|
|
53
|
-
const serializeJsonValue = (
|
|
54
|
-
|
|
63
|
+
const serializeJsonValue = (
|
|
64
|
+
value: unknown,
|
|
65
|
+
keyPath: string[],
|
|
66
|
+
seen: WeakSet<object>,
|
|
67
|
+
{ redactSensitive }: TSerializeJsonValueOptions,
|
|
68
|
+
): unknown => {
|
|
69
|
+
if (redactSensitive && isSensitiveKeyPath(keyPath)) return `[redacted: Sensitive key ${keyPath[keyPath.length - 1] || 'value'}]`;
|
|
55
70
|
if (value === undefined || value === null) return value;
|
|
56
71
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
57
72
|
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
@@ -61,11 +76,14 @@ const serializeJsonValue = (value: unknown, keyPath: string[], seen: WeakSet<obj
|
|
|
61
76
|
if (value instanceof Date) return value.toISOString();
|
|
62
77
|
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack };
|
|
63
78
|
if (Buffer.isBuffer(value)) return `[Buffer ${value.byteLength} bytes]`;
|
|
64
|
-
if (value instanceof Map)
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
if (value instanceof Map)
|
|
80
|
+
return Array.from(value.entries()).map(([entryKey, entryValue], index) =>
|
|
81
|
+
serializeJsonValue([entryKey, entryValue], [...keyPath, `[${index}]`], seen, { redactSensitive }),
|
|
82
|
+
);
|
|
67
83
|
if (value instanceof Set) {
|
|
68
|
-
return Array.from(value.values()).map((entryValue, index) =>
|
|
84
|
+
return Array.from(value.values()).map((entryValue, index) =>
|
|
85
|
+
serializeJsonValue(entryValue, [...keyPath, `[${index}]`], seen, { redactSensitive }),
|
|
86
|
+
);
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
if (typeof value !== 'object') return String(value);
|
|
@@ -74,19 +92,22 @@ const serializeJsonValue = (value: unknown, keyPath: string[], seen: WeakSet<obj
|
|
|
74
92
|
seen.add(value);
|
|
75
93
|
|
|
76
94
|
if (Array.isArray(value)) {
|
|
77
|
-
return value.map((item, index) => serializeJsonValue(item, [...keyPath, `[${index}]`], seen));
|
|
95
|
+
return value.map((item, index) => serializeJsonValue(item, [...keyPath, `[${index}]`], seen, { redactSensitive }));
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
const serialized: Record<string, unknown> = {};
|
|
81
99
|
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
82
|
-
const nextValue = serializeJsonValue(entryValue, [...keyPath, entryKey], seen);
|
|
100
|
+
const nextValue = serializeJsonValue(entryValue, [...keyPath, entryKey], seen, { redactSensitive });
|
|
83
101
|
if (nextValue !== undefined) serialized[entryKey] = nextValue;
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
return serialized;
|
|
87
105
|
};
|
|
88
106
|
|
|
89
|
-
const serializeCaptureValue = (value: TTraceInspectable, key: string) =>
|
|
107
|
+
const serializeCaptureValue = (value: TTraceInspectable, key: string) =>
|
|
108
|
+
serializeJsonValue(value, [key], new WeakSet<object>(), { redactSensitive: true });
|
|
109
|
+
const serializeRawCaptureValue = (value: TTraceInspectable, key: string) =>
|
|
110
|
+
serializeJsonValue(value, [key], new WeakSet<object>(), { redactSensitive: false });
|
|
90
111
|
|
|
91
112
|
const summarizeError = (error: Error): TTraceSummaryValue => ({
|
|
92
113
|
kind: 'error',
|
|
@@ -181,7 +202,7 @@ const snapshotMemory = (): TTraceMemorySnapshot => {
|
|
|
181
202
|
};
|
|
182
203
|
|
|
183
204
|
export default class Trace {
|
|
184
|
-
private requests = new Map<string,
|
|
205
|
+
private requests = new Map<string, TActiveRequestRecord>();
|
|
185
206
|
private order: string[] = [];
|
|
186
207
|
private armedCapture?: TTraceCaptureMode;
|
|
187
208
|
private manifestCache?: {
|
|
@@ -202,10 +223,18 @@ export default class Trace {
|
|
|
202
223
|
private config: Config,
|
|
203
224
|
) {}
|
|
204
225
|
|
|
205
|
-
public
|
|
226
|
+
public isDevTraceEnabled() {
|
|
206
227
|
return __DEV__ && this.config.enable && this.container.Environment.profile === 'dev';
|
|
207
228
|
}
|
|
208
229
|
|
|
230
|
+
public isProfilingEnabled() {
|
|
231
|
+
return this.config.profilerEnable;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public shouldInstrumentRequests() {
|
|
235
|
+
return this.isDevTraceEnabled() || this.isProfilingEnabled();
|
|
236
|
+
}
|
|
237
|
+
|
|
209
238
|
private getContextChannel() {
|
|
210
239
|
return context.getStore() as ChannelInfos | undefined;
|
|
211
240
|
}
|
|
@@ -270,8 +299,8 @@ export default class Trace {
|
|
|
270
299
|
return undefined;
|
|
271
300
|
}
|
|
272
301
|
|
|
273
|
-
private
|
|
274
|
-
|
|
302
|
+
private normalizeSqlQuery(query: string) {
|
|
303
|
+
return query
|
|
275
304
|
.replace(sqlCommentPattern, ' ')
|
|
276
305
|
.replace(sqlLineCommentPattern, ' ')
|
|
277
306
|
.replace(/'([^']|'')*'/g, '?')
|
|
@@ -280,12 +309,43 @@ export default class Trace {
|
|
|
280
309
|
.replace(/\s+/g, ' ')
|
|
281
310
|
.trim()
|
|
282
311
|
.toUpperCase();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private createSqlFingerprint(query: string) {
|
|
315
|
+
const normalized = this.normalizeSqlQuery(query);
|
|
283
316
|
|
|
284
317
|
if (!normalized) return undefined;
|
|
285
318
|
|
|
286
319
|
return createHash('sha1').update(normalized).digest('hex').slice(0, 12);
|
|
287
320
|
}
|
|
288
321
|
|
|
322
|
+
private createProfiling(input: {
|
|
323
|
+
enabled: boolean;
|
|
324
|
+
id: string;
|
|
325
|
+
method: string;
|
|
326
|
+
path: string;
|
|
327
|
+
url: string;
|
|
328
|
+
profilerOrigin?: string;
|
|
329
|
+
profilerParentRequestId?: string;
|
|
330
|
+
}): TRequestProfiling {
|
|
331
|
+
return {
|
|
332
|
+
enabled: input.enabled,
|
|
333
|
+
requestId: input.id,
|
|
334
|
+
method: input.method,
|
|
335
|
+
path: input.path,
|
|
336
|
+
url: input.url,
|
|
337
|
+
startedAt: nowIso(),
|
|
338
|
+
profilerOrigin: input.profilerOrigin,
|
|
339
|
+
profilerParentRequestId: input.profilerParentRequestId,
|
|
340
|
+
apiCalls: [],
|
|
341
|
+
sqlQueries: [],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private getRecord(requestId: string) {
|
|
346
|
+
return this.requests.get(requestId);
|
|
347
|
+
}
|
|
348
|
+
|
|
289
349
|
public armNextRequest(capture: string) {
|
|
290
350
|
if (!isTraceCaptureMode(capture)) {
|
|
291
351
|
throw new Error(`Unsupported trace capture mode "${capture}". Expected one of: ${traceCaptureModes.join(', ')}.`);
|
|
@@ -306,46 +366,72 @@ export default class Trace {
|
|
|
306
366
|
profilerOrigin?: string;
|
|
307
367
|
profilerParentRequestId?: string;
|
|
308
368
|
}) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.armedCapture = undefined;
|
|
313
|
-
|
|
314
|
-
const trace: TRequestTrace = {
|
|
369
|
+
const profilingEnabled = this.shouldInstrumentRequests();
|
|
370
|
+
const profiling = this.createProfiling({
|
|
371
|
+
enabled: profilingEnabled,
|
|
315
372
|
id: input.id,
|
|
316
373
|
method: input.method,
|
|
317
374
|
path: input.path,
|
|
318
375
|
url: input.url,
|
|
319
|
-
capture,
|
|
320
|
-
profilerSessionId: input.profilerSessionId,
|
|
321
376
|
profilerOrigin: input.profilerOrigin,
|
|
322
377
|
profilerParentRequestId: input.profilerParentRequestId,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
378
|
+
});
|
|
379
|
+
if (!profilingEnabled) return profiling;
|
|
380
|
+
|
|
381
|
+
const traceEnabled = this.isDevTraceEnabled();
|
|
382
|
+
const capture = traceEnabled ? this.armedCapture ?? this.config.capture : undefined;
|
|
383
|
+
this.armedCapture = undefined;
|
|
384
|
+
|
|
385
|
+
const trace =
|
|
386
|
+
traceEnabled
|
|
387
|
+
? ({
|
|
388
|
+
id: input.id,
|
|
389
|
+
method: input.method,
|
|
390
|
+
path: input.path,
|
|
391
|
+
url: input.url,
|
|
392
|
+
capture: capture as TTraceCaptureMode,
|
|
393
|
+
profilerSessionId: input.profilerSessionId,
|
|
394
|
+
profilerOrigin: input.profilerOrigin,
|
|
395
|
+
profilerParentRequestId: input.profilerParentRequestId,
|
|
396
|
+
startedAt: profiling.startedAt,
|
|
397
|
+
droppedEvents: 0,
|
|
398
|
+
requestDataJson: serializeCaptureValue(input.data, 'requestData'),
|
|
399
|
+
calls: [],
|
|
400
|
+
sqlQueries: [],
|
|
401
|
+
events: [],
|
|
402
|
+
} satisfies TRequestTrace)
|
|
403
|
+
: undefined;
|
|
404
|
+
|
|
405
|
+
this.requests.set(input.id, {
|
|
406
|
+
profiling,
|
|
407
|
+
trace,
|
|
408
|
+
capture,
|
|
409
|
+
});
|
|
410
|
+
this.activeMeasurements.set(input.id, { cpu: process.cpuUsage(), memory: snapshotMemory() });
|
|
330
411
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
412
|
+
if (trace) {
|
|
413
|
+
this.order.push(input.id);
|
|
414
|
+
this.trimRequestBuffer();
|
|
415
|
+
this.record(input.id, 'request.start', { method: input.method, path: input.path, url: input.url, headers: input.headers, data: input.data });
|
|
416
|
+
}
|
|
335
417
|
|
|
336
|
-
|
|
418
|
+
return profiling;
|
|
337
419
|
}
|
|
338
420
|
|
|
339
421
|
public setRequestUser(requestId: string, user?: string) {
|
|
340
|
-
const
|
|
341
|
-
if (!
|
|
422
|
+
const record = this.getRecord(requestId);
|
|
423
|
+
if (!record) return;
|
|
424
|
+
|
|
425
|
+
record.profiling.user = user;
|
|
426
|
+
if (!record.trace) return;
|
|
342
427
|
|
|
428
|
+
const trace = record.trace;
|
|
343
429
|
trace.user = user;
|
|
344
430
|
if (user) this.record(requestId, 'request.user', { user });
|
|
345
431
|
}
|
|
346
432
|
|
|
347
433
|
public getCapture(requestId: string) {
|
|
348
|
-
return this.
|
|
434
|
+
return this.getRecord(requestId)?.capture;
|
|
349
435
|
}
|
|
350
436
|
|
|
351
437
|
public shouldCapture(requestId: string, minimumCapture: TTraceCaptureMode) {
|
|
@@ -356,7 +442,8 @@ export default class Trace {
|
|
|
356
442
|
}
|
|
357
443
|
|
|
358
444
|
public record(requestId: string, type: TTraceEventType, details: TTraceDetails, minimumCapture: TTraceCaptureMode = 'summary') {
|
|
359
|
-
const
|
|
445
|
+
const record = this.getRecord(requestId);
|
|
446
|
+
const trace = record?.trace;
|
|
360
447
|
if (!trace || !this.shouldCapture(requestId, minimumCapture)) return;
|
|
361
448
|
|
|
362
449
|
if (trace.events.length >= this.config.eventsLimit) {
|
|
@@ -376,28 +463,41 @@ export default class Trace {
|
|
|
376
463
|
}
|
|
377
464
|
|
|
378
465
|
public finishRequest(requestId: string, output: { statusCode: number; user?: string; errorMessage?: string }) {
|
|
379
|
-
const
|
|
380
|
-
if (!
|
|
466
|
+
const record = this.getRecord(requestId);
|
|
467
|
+
if (!record) return;
|
|
468
|
+
|
|
469
|
+
const { profiling, trace } = record;
|
|
470
|
+
if (output.user) profiling.user = output.user;
|
|
471
|
+
profiling.statusCode = output.statusCode;
|
|
472
|
+
profiling.errorMessage = output.errorMessage;
|
|
381
473
|
|
|
382
|
-
if (output.user) trace.user = output.user;
|
|
383
|
-
trace.statusCode = output.statusCode;
|
|
384
|
-
trace.errorMessage = output.errorMessage;
|
|
385
474
|
const measurement = this.activeMeasurements.get(requestId);
|
|
386
475
|
if (measurement) {
|
|
387
|
-
const cpu = process.cpuUsage(measurement.cpu);
|
|
388
|
-
trace.performance = {
|
|
389
|
-
cpu: {
|
|
390
|
-
systemMicros: cpu.system,
|
|
391
|
-
userMicros: cpu.user,
|
|
392
|
-
},
|
|
393
|
-
memory: {
|
|
394
|
-
after: snapshotMemory(),
|
|
395
|
-
before: measurement.memory,
|
|
396
|
-
},
|
|
397
|
-
};
|
|
398
476
|
this.activeMeasurements.delete(requestId);
|
|
477
|
+
|
|
478
|
+
if (trace) {
|
|
479
|
+
const cpu = process.cpuUsage(measurement.cpu);
|
|
480
|
+
trace.performance = {
|
|
481
|
+
cpu: {
|
|
482
|
+
systemMicros: cpu.system,
|
|
483
|
+
userMicros: cpu.user,
|
|
484
|
+
},
|
|
485
|
+
memory: {
|
|
486
|
+
after: snapshotMemory(),
|
|
487
|
+
before: measurement.memory,
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
399
491
|
}
|
|
400
492
|
|
|
493
|
+
profiling.finishedAt = nowIso();
|
|
494
|
+
profiling.durationMs = Math.max(0, Date.parse(profiling.finishedAt) - Date.parse(profiling.startedAt));
|
|
495
|
+
|
|
496
|
+
if (!trace) return;
|
|
497
|
+
|
|
498
|
+
if (output.user) trace.user = output.user;
|
|
499
|
+
trace.statusCode = output.statusCode;
|
|
500
|
+
trace.errorMessage = output.errorMessage;
|
|
401
501
|
this.record(
|
|
402
502
|
requestId,
|
|
403
503
|
'request.finish',
|
|
@@ -405,8 +505,8 @@ export default class Trace {
|
|
|
405
505
|
'summary',
|
|
406
506
|
);
|
|
407
507
|
|
|
408
|
-
trace.finishedAt =
|
|
409
|
-
trace.durationMs =
|
|
508
|
+
trace.finishedAt = profiling.finishedAt;
|
|
509
|
+
trace.durationMs = profiling.durationMs;
|
|
410
510
|
|
|
411
511
|
if (this.config.persistOnError && trace.statusCode >= 500) {
|
|
412
512
|
trace.persistedFilepath = this.exportRequest(requestId);
|
|
@@ -433,13 +533,37 @@ export default class Trace {
|
|
|
433
533
|
requestData?: TTraceInspectable;
|
|
434
534
|
},
|
|
435
535
|
) {
|
|
436
|
-
const
|
|
437
|
-
if (!
|
|
536
|
+
const record = this.getRecord(requestId);
|
|
537
|
+
if (!record) return undefined;
|
|
438
538
|
const channel = this.getContextChannel();
|
|
439
539
|
const inferredServiceLabel = input.serviceLabel || channel?.serviceLabel || this.inferServiceLabelFromStack(new Error().stack);
|
|
540
|
+
const callIndex = record.profiling.apiCalls.length;
|
|
541
|
+
const startedAt = nowIso();
|
|
542
|
+
const callId = `${requestId}:call:${callIndex}`;
|
|
543
|
+
|
|
544
|
+
const profilingCall: TRequestProfilingApiCall = {
|
|
545
|
+
id: callId,
|
|
546
|
+
origin: input.origin,
|
|
547
|
+
label: input.label,
|
|
548
|
+
method: input.method || '',
|
|
549
|
+
path: input.path || '',
|
|
550
|
+
fetcherId: input.fetcherId,
|
|
551
|
+
connectedProjectNamespace: input.connectedProjectNamespace,
|
|
552
|
+
connectedControllerAccessor: input.connectedControllerAccessor,
|
|
553
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
554
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
555
|
+
serviceLabel: inferredServiceLabel,
|
|
556
|
+
startedAt,
|
|
557
|
+
requestBodyJson: input.requestData !== undefined ? serializeRawCaptureValue(input.requestData, 'requestData') : undefined,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
record.profiling.apiCalls.push(profilingCall);
|
|
561
|
+
|
|
562
|
+
const trace = record.trace;
|
|
563
|
+
if (!trace) return callId;
|
|
440
564
|
|
|
441
565
|
const call: TTraceCall = {
|
|
442
|
-
id:
|
|
566
|
+
id: callId,
|
|
443
567
|
parentId: input.parentId,
|
|
444
568
|
origin: input.origin,
|
|
445
569
|
label: input.label,
|
|
@@ -453,7 +577,7 @@ export default class Trace {
|
|
|
453
577
|
serviceLabel: inferredServiceLabel,
|
|
454
578
|
cacheKey: input.cacheKey || channel?.cacheKey,
|
|
455
579
|
cachePhase: input.cachePhase || channel?.cachePhase,
|
|
456
|
-
startedAt
|
|
580
|
+
startedAt,
|
|
457
581
|
requestDataKeys: input.requestDataKeys || [],
|
|
458
582
|
requestData: input.requestData !== undefined ? summarizeCaptureValue(input.requestData, trace.capture, 'requestData') : undefined,
|
|
459
583
|
requestDataJson: input.requestData !== undefined ? serializeCaptureValue(input.requestData, 'requestData') : undefined,
|
|
@@ -461,7 +585,7 @@ export default class Trace {
|
|
|
461
585
|
};
|
|
462
586
|
|
|
463
587
|
trace.calls.push(call);
|
|
464
|
-
return
|
|
588
|
+
return callId;
|
|
465
589
|
}
|
|
466
590
|
|
|
467
591
|
public finishCall(
|
|
@@ -476,12 +600,24 @@ export default class Trace {
|
|
|
476
600
|
) {
|
|
477
601
|
if (!callId) return;
|
|
478
602
|
|
|
479
|
-
const
|
|
480
|
-
const
|
|
481
|
-
if (!
|
|
603
|
+
const record = this.getRecord(requestId);
|
|
604
|
+
const profilingCall = record?.profiling.apiCalls.find((candidate) => candidate.id === callId);
|
|
605
|
+
if (!record || !profilingCall) return;
|
|
606
|
+
|
|
607
|
+
profilingCall.finishedAt = nowIso();
|
|
608
|
+
profilingCall.durationMs = Math.max(0, Date.parse(profilingCall.finishedAt) - Date.parse(profilingCall.startedAt));
|
|
609
|
+
profilingCall.statusCode = output.statusCode;
|
|
610
|
+
profilingCall.errorMessage = output.errorMessage;
|
|
611
|
+
profilingCall.responseBodyJson = output.result !== undefined ? serializeRawCaptureValue(output.result, 'result') : undefined;
|
|
482
612
|
|
|
483
|
-
|
|
484
|
-
|
|
613
|
+
const trace = record.trace;
|
|
614
|
+
if (!trace) return;
|
|
615
|
+
|
|
616
|
+
const call = trace.calls.find((candidate) => candidate.id === callId);
|
|
617
|
+
if (!call) return;
|
|
618
|
+
|
|
619
|
+
call.finishedAt = profilingCall.finishedAt;
|
|
620
|
+
call.durationMs = profilingCall.durationMs;
|
|
485
621
|
call.statusCode = output.statusCode;
|
|
486
622
|
call.errorMessage = output.errorMessage;
|
|
487
623
|
call.resultKeys = output.resultKeys || [];
|
|
@@ -490,7 +626,7 @@ export default class Trace {
|
|
|
490
626
|
}
|
|
491
627
|
|
|
492
628
|
public setRequestResult(requestId: string, result: TTraceInspectable) {
|
|
493
|
-
const trace = this.
|
|
629
|
+
const trace = this.getRecord(requestId)?.trace;
|
|
494
630
|
if (!trace) return;
|
|
495
631
|
|
|
496
632
|
trace.resultJson = serializeCaptureValue(result, 'result');
|
|
@@ -520,8 +656,8 @@ export default class Trace {
|
|
|
520
656
|
target?: string;
|
|
521
657
|
},
|
|
522
658
|
) {
|
|
523
|
-
const
|
|
524
|
-
if (!
|
|
659
|
+
const record = this.getRecord(requestId);
|
|
660
|
+
if (!record) return;
|
|
525
661
|
const channel = this.getContextChannel();
|
|
526
662
|
|
|
527
663
|
const durationMs = Math.max(0, input.durationMs || 0);
|
|
@@ -530,10 +666,41 @@ export default class Trace {
|
|
|
530
666
|
const startedAt =
|
|
531
667
|
Number.isFinite(finishedAtMs) && durationMs > 0 ? new Date(finishedAtMs - durationMs).toISOString() : finishedAt;
|
|
532
668
|
const fingerprint = this.createSqlFingerprint(input.query);
|
|
669
|
+
const normalizedQuery = this.normalizeSqlQuery(input.query) || undefined;
|
|
533
670
|
const inferredServiceLabel = input.serviceLabel || channel?.serviceLabel || this.inferServiceLabelFromStack(new Error().stack);
|
|
534
671
|
|
|
672
|
+
const profilingQuery: TRequestProfilingSqlQuery = {
|
|
673
|
+
id: `${requestId}:sql:${record.profiling.sqlQueries.length}`,
|
|
674
|
+
callerCallId: input.callerCallId,
|
|
675
|
+
callerFetcherId: input.callerFetcherId,
|
|
676
|
+
callerLabel: input.callerLabel,
|
|
677
|
+
callerMethod: input.callerMethod || '',
|
|
678
|
+
callerOrigin: input.callerOrigin || 'request',
|
|
679
|
+
callerPath: input.callerPath || '',
|
|
680
|
+
durationMs,
|
|
681
|
+
finishedAt,
|
|
682
|
+
kind: input.kind,
|
|
683
|
+
model: input.model,
|
|
684
|
+
operation: input.operation,
|
|
685
|
+
fingerprint,
|
|
686
|
+
normalizedQuery,
|
|
687
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
688
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
689
|
+
serviceLabel: inferredServiceLabel,
|
|
690
|
+
connectedNamespace: input.connectedNamespace || channel?.connectedNamespace,
|
|
691
|
+
paramsJson: input.paramsJson,
|
|
692
|
+
paramsText: input.paramsText,
|
|
693
|
+
query: input.query.trim(),
|
|
694
|
+
startedAt,
|
|
695
|
+
target: input.target,
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
record.profiling.sqlQueries.push(profilingQuery);
|
|
699
|
+
const trace = record.trace;
|
|
700
|
+
if (!trace) return;
|
|
701
|
+
|
|
535
702
|
const sqlQuery: TTraceSqlQuery = {
|
|
536
|
-
id:
|
|
703
|
+
id: profilingQuery.id,
|
|
537
704
|
callerCallId: input.callerCallId,
|
|
538
705
|
callerFetcherId: input.callerFetcherId,
|
|
539
706
|
callerLabel: input.callerLabel,
|
|
@@ -546,6 +713,7 @@ export default class Trace {
|
|
|
546
713
|
model: input.model,
|
|
547
714
|
operation: input.operation,
|
|
548
715
|
fingerprint,
|
|
716
|
+
normalizedQuery,
|
|
549
717
|
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
550
718
|
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
551
719
|
serviceLabel: inferredServiceLabel,
|
|
@@ -564,7 +732,7 @@ export default class Trace {
|
|
|
564
732
|
return [...this.order]
|
|
565
733
|
.reverse()
|
|
566
734
|
.slice(0, limit)
|
|
567
|
-
.map((requestId) => this.
|
|
735
|
+
.map((requestId) => this.getRecord(requestId)?.trace)
|
|
568
736
|
.filter((trace): trace is TRequestTrace => trace !== undefined)
|
|
569
737
|
.map((trace) => ({
|
|
570
738
|
id: trace.id,
|
|
@@ -593,21 +761,25 @@ export default class Trace {
|
|
|
593
761
|
return [...this.order]
|
|
594
762
|
.reverse()
|
|
595
763
|
.slice(0, Math.max(1, limit))
|
|
596
|
-
.map((requestId) => this.
|
|
764
|
+
.map((requestId) => this.getRecord(requestId)?.trace)
|
|
597
765
|
.filter((trace): trace is TRequestTrace => trace !== undefined);
|
|
598
766
|
}
|
|
599
767
|
|
|
600
768
|
public getLatestRequest() {
|
|
601
769
|
const latestRequestId = this.order[this.order.length - 1];
|
|
602
|
-
return latestRequestId ? this.
|
|
770
|
+
return latestRequestId ? this.getRecord(latestRequestId)?.trace : undefined;
|
|
603
771
|
}
|
|
604
772
|
|
|
605
773
|
public getRequest(requestId: string) {
|
|
606
|
-
return this.
|
|
774
|
+
return this.getRecord(requestId)?.trace;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
public getProfiling(requestId: string) {
|
|
778
|
+
return this.getRecord(requestId)?.profiling;
|
|
607
779
|
}
|
|
608
780
|
|
|
609
781
|
public exportRequest(requestId: string, filepath?: string) {
|
|
610
|
-
const trace = this.
|
|
782
|
+
const trace = this.getRecord(requestId)?.trace;
|
|
611
783
|
if (!trace) throw new Error(`Trace ${requestId} was not found.`);
|
|
612
784
|
|
|
613
785
|
const outputFilepath =
|
|
@@ -622,6 +794,15 @@ export default class Trace {
|
|
|
622
794
|
return outputFilepath;
|
|
623
795
|
}
|
|
624
796
|
|
|
797
|
+
public releaseRequest(requestId: string) {
|
|
798
|
+
const record = this.getRecord(requestId);
|
|
799
|
+
if (!record) return;
|
|
800
|
+
if (record.trace) return;
|
|
801
|
+
|
|
802
|
+
this.requests.delete(requestId);
|
|
803
|
+
this.activeMeasurements.delete(requestId);
|
|
804
|
+
}
|
|
805
|
+
|
|
625
806
|
private trimRequestBuffer() {
|
|
626
807
|
const overflow = this.order.length - this.config.requestsLimit;
|
|
627
808
|
if (overflow <= 0) return;
|
|
@@ -46,6 +46,12 @@ type TPrismaExtensionOperation = {
|
|
|
46
46
|
query: (args: unknown) => Promise<unknown>;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
declare global {
|
|
50
|
+
interface BigInt {
|
|
51
|
+
toJSON: () => number | string;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
/*----------------------------------
|
|
50
56
|
- HELPERS
|
|
51
57
|
----------------------------------*/
|
|
@@ -173,21 +179,18 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
|
|
|
173
179
|
'DATABASE_URL is required before starting the Models service. Prisma 7 no longer auto-loads runtime env files.',
|
|
174
180
|
);
|
|
175
181
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
log: [{ emit: 'event', level: 'query' }],
|
|
181
|
-
})
|
|
182
|
-
: new PrismaClient({
|
|
183
|
-
adapter: createMariaDbAdapter(databaseUrl),
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
if (!shouldTraceQueries) {
|
|
187
|
-
this.client = prismaClient;
|
|
182
|
+
if (!this.app.container.Trace.shouldInstrumentRequests()) {
|
|
183
|
+
this.client = new PrismaClient({
|
|
184
|
+
adapter: createMariaDbAdapter(databaseUrl),
|
|
185
|
+
});
|
|
188
186
|
return;
|
|
189
187
|
}
|
|
190
188
|
|
|
189
|
+
const prismaClient = new PrismaClient({
|
|
190
|
+
adapter: createMariaDbAdapter(databaseUrl),
|
|
191
|
+
log: [{ emit: 'event', level: 'query' }],
|
|
192
|
+
});
|
|
193
|
+
|
|
191
194
|
prismaClient.$on('query', (event: TPrismaQueryEvent) => this.traceQuery(event));
|
|
192
195
|
|
|
193
196
|
this.client = prismaClient.$extends(
|
|
@@ -510,7 +510,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
510
510
|
private registerDevTraceRoutes(routes: express.Express) {
|
|
511
511
|
if (!__DEV__ || this.app.env.profile !== 'dev') return;
|
|
512
512
|
|
|
513
|
-
if (this.app.container.Trace.
|
|
513
|
+
if (this.app.container.Trace.isDevTraceEnabled()) {
|
|
514
514
|
routes.get('/__proteum/trace/requests', (req, res) => {
|
|
515
515
|
const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
|
|
516
516
|
const parsedLimit = typeof rawLimit === 'string' ? Number.parseInt(rawLimit, 10) : NaN;
|
|
@@ -132,7 +132,13 @@ export type Config<
|
|
|
132
132
|
// Set it as a function, so when we instanciate the services, we can callthis.router to pass the router instance in roiuter services
|
|
133
133
|
type TRouterServicesList = { [serviceName: string]: AnyRouterService };
|
|
134
134
|
|
|
135
|
-
export type Hooks = {
|
|
135
|
+
export type Hooks = {
|
|
136
|
+
request: { args: [request: ServerRequest<TServerRouter>] };
|
|
137
|
+
'request.finished': { args: [request: ServerRequest<TServerRouter>] };
|
|
138
|
+
resolve: { args: [request: ServerRequest<TServerRouter>] };
|
|
139
|
+
resolved: { args: [route: TMatchedRoute, request: ServerRequest<TServerRouter>, response: ServerResponse<TServerRouter>] };
|
|
140
|
+
render: { args: [page: Page<TServerRouter>] };
|
|
141
|
+
};
|
|
136
142
|
|
|
137
143
|
export type TControllerDefinition = {
|
|
138
144
|
path?: string;
|
|
@@ -607,6 +613,32 @@ export default class ServerRouter<
|
|
|
607
613
|
/*----------------------------------
|
|
608
614
|
- RESOLUTION
|
|
609
615
|
----------------------------------*/
|
|
616
|
+
private async finalizeRequest(
|
|
617
|
+
request: ServerRequest<this>,
|
|
618
|
+
output: {
|
|
619
|
+
statusCode: number;
|
|
620
|
+
user?: string;
|
|
621
|
+
errorMessage?: string;
|
|
622
|
+
},
|
|
623
|
+
) {
|
|
624
|
+
this.app.container.Trace.finishRequest(request.id, output);
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
await this.runHook('request.finished', request);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
const typedError =
|
|
630
|
+
error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown request.finished hook error');
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
await this.app.runHook('error', typedError, request);
|
|
634
|
+
} catch (hookError) {
|
|
635
|
+
console.error('request.finished hook error', typedError, 'Error hook failure', hookError);
|
|
636
|
+
}
|
|
637
|
+
} finally {
|
|
638
|
+
this.app.container.Trace.releaseRequest(request.id);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
610
642
|
public async middleware(req: express.Request, res: express.Response) {
|
|
611
643
|
// Create request
|
|
612
644
|
let requestId = uuid();
|
|
@@ -629,7 +661,7 @@ export default class ServerRouter<
|
|
|
629
661
|
this,
|
|
630
662
|
);
|
|
631
663
|
|
|
632
|
-
this.app.container.Trace.startRequest({
|
|
664
|
+
request.profiling = this.app.container.Trace.startRequest({
|
|
633
665
|
id: request.id,
|
|
634
666
|
method: request.method,
|
|
635
667
|
path: request.path,
|
|
@@ -640,7 +672,7 @@ export default class ServerRouter<
|
|
|
640
672
|
profilerOrigin: request.headers[profilerOriginHeader] || undefined,
|
|
641
673
|
profilerParentRequestId: request.headers[profilerParentRequestIdHeader] || undefined,
|
|
642
674
|
});
|
|
643
|
-
if (this.app.container.Trace.
|
|
675
|
+
if (this.app.container.Trace.isDevTraceEnabled()) res.setHeader(profilerTraceRequestIdHeader, request.id);
|
|
644
676
|
if (cachedPage) {
|
|
645
677
|
this.app.container.Trace.record(
|
|
646
678
|
request.id,
|
|
@@ -659,7 +691,7 @@ export default class ServerRouter<
|
|
|
659
691
|
// Bulk API Requests
|
|
660
692
|
if (request.path === '/api' && typeof request.data.fetchers === 'object') {
|
|
661
693
|
await this.resolveApiBatch(request.data.fetchers, request);
|
|
662
|
-
this.
|
|
694
|
+
await this.finalizeRequest(request, {
|
|
663
695
|
statusCode: request.res.statusCode || 200,
|
|
664
696
|
user: request.user?.email,
|
|
665
697
|
});
|
|
@@ -696,7 +728,7 @@ export default class ServerRouter<
|
|
|
696
728
|
},
|
|
697
729
|
'summary',
|
|
698
730
|
);
|
|
699
|
-
this.
|
|
731
|
+
await this.finalizeRequest(request, {
|
|
700
732
|
statusCode: response.statusCode,
|
|
701
733
|
user: request.user?.email,
|
|
702
734
|
});
|
|
@@ -715,7 +747,7 @@ export default class ServerRouter<
|
|
|
715
747
|
'summary',
|
|
716
748
|
);
|
|
717
749
|
res.send(cachedPage.rendered);
|
|
718
|
-
this.
|
|
750
|
+
await this.finalizeRequest(request, {
|
|
719
751
|
statusCode: response.statusCode,
|
|
720
752
|
user: request.user?.email,
|
|
721
753
|
});
|
|
@@ -739,19 +771,19 @@ export default class ServerRouter<
|
|
|
739
771
|
'summary',
|
|
740
772
|
);
|
|
741
773
|
res.send(response.data);
|
|
742
|
-
this.
|
|
774
|
+
await this.finalizeRequest(request, {
|
|
743
775
|
statusCode: response.statusCode,
|
|
744
776
|
user: request.user?.email,
|
|
745
777
|
});
|
|
746
778
|
} else if (response.data !== 'true') {
|
|
747
|
-
this.
|
|
779
|
+
await this.finalizeRequest(request, {
|
|
748
780
|
statusCode: res.statusCode || response.statusCode,
|
|
749
781
|
user: request.user?.email,
|
|
750
782
|
errorMessage: "Can't return data from the controller since response has already been sent via express.",
|
|
751
783
|
});
|
|
752
784
|
throw new Error("Can't return data from the controller since response has already been sent via express.");
|
|
753
785
|
} else {
|
|
754
|
-
this.
|
|
786
|
+
await this.finalizeRequest(request, {
|
|
755
787
|
statusCode: res.statusCode || response.statusCode,
|
|
756
788
|
user: request.user?.email,
|
|
757
789
|
});
|
|
@@ -10,7 +10,7 @@ import Bowser from 'bowser';
|
|
|
10
10
|
|
|
11
11
|
// Core
|
|
12
12
|
import BaseRequest from '@common/router/request';
|
|
13
|
-
import type { TTraceCallOrigin } from '@common/dev/requestTrace';
|
|
13
|
+
import type { TRequestProfiling, TTraceCallOrigin } from '@common/dev/requestTrace';
|
|
14
14
|
|
|
15
15
|
// Specific
|
|
16
16
|
import type { HttpMethod, HttpHeaders } from '..';
|
|
@@ -74,6 +74,7 @@ export default class ServerRequest<TRouter extends TAnyRouter = TAnyRouter> exte
|
|
|
74
74
|
// Services
|
|
75
75
|
public api: ApiClient;
|
|
76
76
|
public traceCall?: TRequestTraceCallContext;
|
|
77
|
+
public profiling!: TRequestProfiling;
|
|
77
78
|
|
|
78
79
|
/*----------------------------------
|
|
79
80
|
- INITIALISATION
|