@finesoft/front 0.1.75 → 0.1.77

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 (58) hide show
  1. package/README.md +2 -411
  2. package/dist/browser.d.mts +2 -0
  3. package/dist/browser.mjs +1 -0
  4. package/dist/index.d.mts +2 -1248
  5. package/dist/index.mjs +54 -3557
  6. package/dist/server-data-DGbiKzMS.d.mts +1249 -0
  7. package/dist/start-app-BdXBCcor.mjs +2 -0
  8. package/docs/01-getting-started.md +230 -0
  9. package/docs/02-routing-and-controllers.md +197 -0
  10. package/docs/03-middleware.md +214 -0
  11. package/docs/04-rendering-and-hydration.md +271 -0
  12. package/docs/05-i18n.md +243 -0
  13. package/docs/06-http-client.md +286 -0
  14. package/docs/07-di-container.md +264 -0
  15. package/docs/08-observability.md +290 -0
  16. package/docs/09-server-and-deployment.md +242 -0
  17. package/docs/10-features-platform-pwa.md +238 -0
  18. package/docs/README.md +72 -0
  19. package/docs/advanced/custom-action-handler.md +248 -0
  20. package/docs/advanced/custom-adapter.md +264 -0
  21. package/docs/advanced/custom-event-recorder.md +318 -0
  22. package/docs/advanced/inline-proxy-codegen.md +200 -0
  23. package/docs/advanced/multi-tenant-scopes.md +330 -0
  24. package/docs/engineering/ci-release-flow.md +244 -0
  25. package/docs/engineering/project-structure.md +296 -0
  26. package/docs/engineering/testing.md +317 -0
  27. package/docs/pitfalls/container-scope-leak.md +215 -0
  28. package/docs/pitfalls/i18n-bundle-size.md +182 -0
  29. package/docs/pitfalls/proxy-binary-payloads.md +133 -0
  30. package/docs/pitfalls/redirect-vs-rewrite.md +147 -0
  31. package/docs/pitfalls/ssr-hydration-mismatch.md +163 -0
  32. package/docs/pitfalls/ssr-vs-csr-globals.md +176 -0
  33. package/docs/zh/01-getting-started.md +230 -0
  34. package/docs/zh/02-routing-and-controllers.md +197 -0
  35. package/docs/zh/03-middleware.md +214 -0
  36. package/docs/zh/04-rendering-and-hydration.md +271 -0
  37. package/docs/zh/05-i18n.md +243 -0
  38. package/docs/zh/06-http-client.md +286 -0
  39. package/docs/zh/07-di-container.md +264 -0
  40. package/docs/zh/08-observability.md +287 -0
  41. package/docs/zh/09-server-and-deployment.md +242 -0
  42. package/docs/zh/10-features-platform-pwa.md +238 -0
  43. package/docs/zh/README.md +72 -0
  44. package/docs/zh/advanced/custom-action-handler.md +248 -0
  45. package/docs/zh/advanced/custom-adapter.md +264 -0
  46. package/docs/zh/advanced/custom-event-recorder.md +318 -0
  47. package/docs/zh/advanced/inline-proxy-codegen.md +200 -0
  48. package/docs/zh/advanced/multi-tenant-scopes.md +330 -0
  49. package/docs/zh/engineering/ci-release-flow.md +244 -0
  50. package/docs/zh/engineering/project-structure.md +296 -0
  51. package/docs/zh/engineering/testing.md +317 -0
  52. package/docs/zh/pitfalls/container-scope-leak.md +215 -0
  53. package/docs/zh/pitfalls/i18n-bundle-size.md +182 -0
  54. package/docs/zh/pitfalls/proxy-binary-payloads.md +133 -0
  55. package/docs/zh/pitfalls/redirect-vs-rewrite.md +147 -0
  56. package/docs/zh/pitfalls/ssr-hydration-mismatch.md +163 -0
  57. package/docs/zh/pitfalls/ssr-vs-csr-globals.md +176 -0
  58. package/package.json +12 -3
@@ -0,0 +1,264 @@
1
+ # 7. DI container
2
+
3
+ The framework uses a small dependency-injection container with parent/child scopes. It exists for two reasons:
4
+
5
+ 1. **Request isolation on the server** — every SSR request gets its own scope, so per-request state (auth, request id) doesn't leak across requests.
6
+ 2. **Decoupled testing** — controllers resolve their dependencies from the container, so tests can swap any of them.
7
+
8
+ The container is intentionally small. There are no decorators, no annotations, no auto-wiring. You register factories, you resolve by key.
9
+
10
+ ## Registering
11
+
12
+ ```ts
13
+ import { Container } from "@finesoft/front";
14
+
15
+ const container = new Container();
16
+
17
+ container.register("userApi", () => new UserApi({ baseUrl: "/api" }));
18
+ container.register("logger", () => new ConsoleLogger());
19
+ ```
20
+
21
+ The factory is invoked **once per container** by default. The result is cached.
22
+
23
+ For per-resolve construction (non-singleton):
24
+
25
+ ```ts
26
+ container.register("requestId", () => crypto.randomUUID(), false);
27
+ container.resolve("requestId"); // new id every time
28
+ ```
29
+
30
+ ## Resolving
31
+
32
+ ```ts
33
+ const api = container.resolve<UserApi>("userApi");
34
+ const logger = container.resolve<Logger>("logger");
35
+ ```
36
+
37
+ The generic parameter is for TypeScript only — there is no runtime type check.
38
+
39
+ Resolving an unregistered key throws:
40
+
41
+ ```ts
42
+ container.resolve("missing"); // Error: Dependency "missing" not registered
43
+ ```
44
+
45
+ ## Scopes — the core feature
46
+
47
+ ```ts
48
+ const requestScope = framework.container.createScope();
49
+ requestScope.register("currentUser", () => loadUserFromSession(request));
50
+
51
+ // Falls back to parent for keys not in the scope:
52
+ requestScope.resolve("userApi"); // parent
53
+ requestScope.resolve("currentUser"); // scope
54
+
55
+ requestScope.dispose();
56
+ ```
57
+
58
+ A child scope:
59
+
60
+ - **Inherits** all parent keys via fallback resolution
61
+ - **Overrides** any key by registering its own factory
62
+ - **Cleans up** on `dispose()` — children are recursively disposed; the scope removes itself from the parent's children set
63
+
64
+ The framework creates a request-scoped container automatically per SSR request and passes it to your guards and controllers as `ctx.container` / the second argument of `execute()`. You should not need to manually create scopes for normal request handling.
65
+
66
+ ## When to create your own scope
67
+
68
+ - Multi-tenant apps where each tenant has its own config / API client (see [advanced/multi-tenant-scopes](./advanced/multi-tenant-scopes.md))
69
+ - Long-running operations that need their own short-lived dependencies
70
+ - Test setup where you want to layer overrides on top of a base container
71
+
72
+ ## Disposal and leaks
73
+
74
+ ```ts
75
+ const scope = container.createScope();
76
+ // ... use scope ...
77
+ scope.dispose();
78
+ ```
79
+
80
+ `dispose()`:
81
+
82
+ 1. Recursively disposes any unfinished child scopes
83
+ 2. Calls `destroy()` on any registered factory whose result implements it (loggers, recorders)
84
+ 3. Removes itself from the parent's children set
85
+ 4. Is idempotent — calling twice is safe
86
+
87
+ **Not disposing a scope leaks every cached value in it.** Request scopes that survive past the response will hold onto:
88
+
89
+ - HTTP clients (and their pending request state)
90
+ - Loggers / recorders
91
+ - Anything else the controllers resolved
92
+
93
+ The framework handles disposal for request scopes it creates. Scopes **you** create are yours to dispose.
94
+
95
+ See [pitfalls: container scope leak](./pitfalls/container-scope-leak.md) for the symptoms when you forget.
96
+
97
+ ## Standard DI keys
98
+
99
+ Use `DEP_KEYS` constants instead of string literals to catch typos at type-check time:
100
+
101
+ ```ts
102
+ import { DEP_KEYS } from "@finesoft/front";
103
+
104
+ container.register(DEP_KEYS.LOGGER, () => new ConsoleLogger());
105
+ container.register(DEP_KEYS.EVENT_RECORDER, () => myRecorder);
106
+
107
+ const logger = container.resolve(DEP_KEYS.LOGGER);
108
+ ```
109
+
110
+ The constants:
111
+
112
+ | Key | Standard type | Used by |
113
+ | ------------------------- | ------------------ | --------------------------------------------- |
114
+ | `DEP_KEYS.LOGGER` | `Logger` | Framework logging |
115
+ | `DEP_KEYS.LOGGER_FACTORY` | `LoggerFactory` | Per-category loggers (`logger.scope("auth")`) |
116
+ | `DEP_KEYS.NET` | `Net` | Network state checks (offline / metered) |
117
+ | `DEP_KEYS.STORAGE` | `Storage` | Key-value persistence (localStorage / memory) |
118
+ | `DEP_KEYS.FEATURE_FLAGS` | `FeatureFlags` | Feature flag reads |
119
+ | `DEP_KEYS.METRICS` | `MetricsClient` | Counter / gauge / timing |
120
+ | `DEP_KEYS.FETCH` | `typeof fetch` | `HttpClient`'s underlying fetch (mockable) |
121
+ | `DEP_KEYS.EVENT_RECORDER` | `EventRecorder` | Structured event recording |
122
+ | `DEP_KEYS.LOCALE` | `LocaleAttributes` | Resolved locale (lang + dir) |
123
+ | `DEP_KEYS.PLATFORM` | `PlatformInfo` | Detected user-agent platform info |
124
+ | `DEP_KEYS.TRANSLATOR` | `Translator` | Translation function |
125
+
126
+ The framework registers default implementations for these during `Framework.create()`. Override them by registering after framework creation:
127
+
128
+ ```ts
129
+ const framework = Framework.create({
130
+ /* ... */
131
+ });
132
+ framework.container.register(DEP_KEYS.LOGGER, () => myCustomLogger);
133
+ ```
134
+
135
+ ## Custom keys
136
+
137
+ For your own services, use string keys directly:
138
+
139
+ ```ts
140
+ container.register("userApi", () => new UserApi({ baseUrl: "/api" }));
141
+ container.register("session", () => new SessionService());
142
+ container.register("featureBucketing", () => new BucketingService());
143
+ ```
144
+
145
+ To get type safety for your own keys, define your own const map:
146
+
147
+ ```ts
148
+ // src/lib/di-keys.ts
149
+ export const APP_KEYS = {
150
+ USER_API: "userApi",
151
+ SESSION: "session",
152
+ FEATURE_BUCKETING: "featureBucketing",
153
+ } as const;
154
+
155
+ // Usage
156
+ container.register(APP_KEYS.USER_API, () => new UserApi({ baseUrl: "/api" }));
157
+ const api = container.resolve<UserApi>(APP_KEYS.USER_API);
158
+ ```
159
+
160
+ ## Lifecycle ordering
161
+
162
+ ```
163
+ Framework.create({ ... })
164
+
165
+
166
+ default DEP_KEYS registered (logger, locale, platform, ...)
167
+
168
+
169
+ your custom registrations (in onBeforeStart or bootstrap)
170
+
171
+
172
+ ─── per request ───────────────────────────────────
173
+ framework.container.createScope() ← request scope
174
+
175
+
176
+ beforeLoad guards (ctx.container = scope)
177
+
178
+
179
+ controller.execute(params, scope)
180
+
181
+
182
+ afterLoad guards (ctx.container = scope)
183
+
184
+
185
+ renderApp() / response
186
+
187
+
188
+ scope.dispose() ← framework cleans up
189
+ ```
190
+
191
+ ## Testing with the container
192
+
193
+ Inject mocks at the scope level:
194
+
195
+ ```ts
196
+ import { Framework } from "@finesoft/front";
197
+
198
+ const framework = Framework.create({
199
+ /* ... */
200
+ });
201
+ const testScope = framework.container.createScope();
202
+ testScope.register("userApi", () => mockUserApi);
203
+
204
+ const controller = new UserListController();
205
+ const page = await controller.execute({}, testScope);
206
+
207
+ testScope.dispose();
208
+ ```
209
+
210
+ The full pattern is in [engineering/testing](./engineering/testing.md).
211
+
212
+ ## Antipatterns
213
+
214
+ ### Don't resolve in module top-level
215
+
216
+ ```ts
217
+ // BAD — runs at import time, before framework.create()
218
+ const logger = container.resolve(DEP_KEYS.LOGGER);
219
+ export function log(msg: string) {
220
+ logger.info(msg);
221
+ }
222
+ ```
223
+
224
+ The `container` you'd reach here isn't the request scope; you'd get the parent and lose request isolation.
225
+
226
+ Instead, resolve inside the function that has access to the scope:
227
+
228
+ ```ts
229
+ export function logFromController(container: Container, msg: string) {
230
+ container.resolve<Logger>(DEP_KEYS.LOGGER).info(msg);
231
+ }
232
+ ```
233
+
234
+ ### Don't keep refs to scoped instances after dispose
235
+
236
+ ```ts
237
+ // BAD
238
+ let api: UserApi;
239
+ beforeLoad: (ctx) => {
240
+ api = ctx.container.resolve("userApi");
241
+ return next();
242
+ };
243
+ // `api` now points at an instance whose scope was disposed
244
+ ```
245
+
246
+ If you need to share state across requests, register it on the parent container, not the request scope.
247
+
248
+ ### Don't register inside a guard
249
+
250
+ ```ts
251
+ // BAD — runs per request
252
+ beforeLoad: (ctx) => {
253
+ ctx.container.register("userApi", () => new UserApi(/*...*/));
254
+ return next();
255
+ };
256
+ ```
257
+
258
+ This creates a fresh factory closure per request. Register once at framework setup; the scope inherits.
259
+
260
+ ## Next
261
+
262
+ - [Observability](./08-observability.md) — wiring Logger / EventRecorder / ReportCallback via DI
263
+ - [Engineering: testing](./engineering/testing.md) — using scopes to isolate tests
264
+ - [Pitfalls: container scope leak](./pitfalls/container-scope-leak.md) — what happens when you forget to dispose
@@ -0,0 +1,290 @@
1
+ # 8. Observability
2
+
3
+ Three primitives, each composable, each replaceable:
4
+
5
+ - **`Logger`** — line-oriented (debug/info/warn/error)
6
+ - **`EventRecorder`** — structured event records (`{ name, fields }`)
7
+ - **`ReportCallback`** — fan-out of `warn`/`error` logs to external monitoring
8
+
9
+ Plus impression tracking via `IntersectionImpressionObserver`.
10
+
11
+ ## Logger
12
+
13
+ The base `ConsoleLogger` writes to `console.{debug,info,warn,error}` with a category prefix. Scoped loggers add nested prefixes.
14
+
15
+ ```ts
16
+ import { ConsoleLogger } from "@finesoft/front";
17
+
18
+ const logger = new ConsoleLogger("app");
19
+ logger.info("hello"); // [app] hello
20
+
21
+ const auth = logger.scope("auth");
22
+ auth.warn("token expired"); // [app:auth] token expired
23
+ ```
24
+
25
+ Levels: `debug`, `info`, `warn`, `error`. The framework calls `warn`/`error` on its own logger for internal failures (failed guards, dispatch errors).
26
+
27
+ ### Replacing the framework logger
28
+
29
+ ```ts
30
+ import { DEP_KEYS } from "@finesoft/front";
31
+
32
+ framework.container.register(DEP_KEYS.LOGGER, () => myLogger);
33
+ framework.container.register(DEP_KEYS.LOGGER_FACTORY, () => ({
34
+ create(category) {
35
+ return myLoggerImpl.scope(category);
36
+ },
37
+ }));
38
+ ```
39
+
40
+ ## `ReportCallback` — forwarding logs to monitoring
41
+
42
+ The cleanest way to ship `warn`/`error` logs to Sentry / Datadog / your own collector:
43
+
44
+ ```ts
45
+ import { Framework, type ReportCallback } from "@finesoft/front";
46
+
47
+ const reportCallback: ReportCallback = (level, category, args) => {
48
+ Sentry.captureMessage(`[${category}] ${args.join(" ")}`, level);
49
+ };
50
+
51
+ const framework = Framework.create({
52
+ reportCallback,
53
+ // ...
54
+ });
55
+ ```
56
+
57
+ How it works:
58
+
59
+ - `Framework.create({ reportCallback })` installs `ReportingLoggerFactory` over `ConsoleLogger`
60
+ - All `logger.warn(...)` and `logger.error(...)` calls fire the callback **and** continue to the console
61
+ - `debug` and `info` are not forwarded (avoid burying signal in volume)
62
+ - The callback runs inside `try/catch` — a failure in the reporter cannot crash the framework
63
+
64
+ ### Categories
65
+
66
+ Each `logger.scope("auth")` becomes a category passed to the callback. Use them for routing in your monitoring system:
67
+
68
+ ```ts
69
+ reportCallback(level, category, args) {
70
+ if (category.startsWith("auth")) {
71
+ sentry.captureMessage(/* go to auth project */);
72
+ } else {
73
+ sentry.captureMessage(/* default project */);
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### Anti-spam
79
+
80
+ `ReportCallback` runs every time. If you have a hot loop logging the same warning a thousand times, you'll send a thousand events. Add deduplication in the callback itself:
81
+
82
+ ```ts
83
+ const seen = new Map<string, number>();
84
+ const reportCallback: ReportCallback = (level, category, args) => {
85
+ const key = `${category}:${args[0]}`;
86
+ const now = Date.now();
87
+ const last = seen.get(key) ?? 0;
88
+ if (now - last < 60_000) return; // 1 per minute per (category, message)
89
+ seen.set(key, now);
90
+ Sentry.captureMessage(/* ... */);
91
+ };
92
+ ```
93
+
94
+ ## `EventRecorder` — structured events
95
+
96
+ When you want events with fields, not free-form log lines, use `EventRecorder`.
97
+
98
+ ```ts
99
+ import { Framework, ConsoleEventRecorder, type EventRecorder } from "@finesoft/front";
100
+
101
+ const recorder: EventRecorder = new ConsoleEventRecorder();
102
+ const framework = Framework.create({ eventRecorder: recorder });
103
+
104
+ // Anywhere:
105
+ recorder.record({
106
+ name: "PageView",
107
+ fields: { url: "/products/42", referrer: "/search?q=widget" },
108
+ });
109
+ ```
110
+
111
+ ### Built-in events
112
+
113
+ The framework records `PageView` automatically via `framework.didEnterPage(page)`. This fires:
114
+
115
+ - After every successful navigation (SSR + CSR)
116
+ - With fields: `{ intentId, url, renderMode }`
117
+
118
+ ### Composing recorders
119
+
120
+ `CompositeEventRecorder` fans events to multiple sinks:
121
+
122
+ ```ts
123
+ import { ConsoleEventRecorder, CompositeEventRecorder } from "@finesoft/front";
124
+
125
+ const recorder = new CompositeEventRecorder([
126
+ new ConsoleEventRecorder(), // dev visibility
127
+ productionAnalyticsRecorder, // ship to backend
128
+ ]);
129
+ ```
130
+
131
+ If one recorder throws, the others still receive the event.
132
+
133
+ ### Adding common fields
134
+
135
+ `WithFieldsRecorder` decorates another recorder, prepending fields to every event:
136
+
137
+ ```ts
138
+ import { WithFieldsRecorder, ConsoleEventRecorder } from "@finesoft/front";
139
+
140
+ const recorder = new WithFieldsRecorder(new ConsoleEventRecorder(), [
141
+ { getFields: () => ({ app: "myApp", version: "1.0.0" }) },
142
+ { getFields: () => ({ userId: getCurrentUserId() }) },
143
+ ]);
144
+
145
+ recorder.record({ name: "Click", fields: { id: "buy" } });
146
+ // → { app, version, userId, id }
147
+ ```
148
+
149
+ Use this for session-scoped fields you don't want to repeat in every `record()` call.
150
+
151
+ ### Building your own recorder
152
+
153
+ Implement the `EventRecorder` interface:
154
+
155
+ ```ts
156
+ import type { EventRecorder, EventRecord } from "@finesoft/front";
157
+
158
+ class HttpEventRecorder implements EventRecorder {
159
+ constructor(private endpoint: string) {}
160
+ record(event: EventRecord): void {
161
+ // fire-and-forget; do NOT await — record() shouldn't block UI
162
+ navigator.sendBeacon(this.endpoint, JSON.stringify(event));
163
+ }
164
+ destroy(): void {
165
+ // flush, close connections, etc.
166
+ }
167
+ }
168
+ ```
169
+
170
+ See [advanced/custom-event-recorder](./advanced/custom-event-recorder.md) for batching, retries, and lifecycle handling.
171
+
172
+ ## Impression tracking
173
+
174
+ Track when elements enter the viewport using `IntersectionObserver` plumbing:
175
+
176
+ ```ts
177
+ import { IntersectionImpressionObserver, type EventRecorder } from "@finesoft/front";
178
+
179
+ const observer = new IntersectionImpressionObserver((entries) => {
180
+ for (const entry of entries) {
181
+ recorder.record({
182
+ name: "Impression",
183
+ fields: { id: entry.id, ...entry.metadata },
184
+ });
185
+ }
186
+ });
187
+
188
+ // Attach to an element when it mounts
189
+ observer.observe(productCardElement, "product-card-123", {
190
+ category: "featured",
191
+ position: 3,
192
+ });
193
+
194
+ // Detach when it unmounts
195
+ observer.unobserve(productCardElement);
196
+
197
+ // On app teardown
198
+ observer.destroy();
199
+ ```
200
+
201
+ The observer fires the callback once per element per visibility transition. It does not deduplicate across observe/unobserve cycles — your callback should decide whether to record again.
202
+
203
+ ## Error handling via `fallback`
204
+
205
+ A controller error becomes a recorded event automatically:
206
+
207
+ ```ts
208
+ class HomeController extends BaseController<{}, HomePage> {
209
+ readonly intentId = "home";
210
+
211
+ async execute(_params, container) {
212
+ const api = container.resolve<UserApi>("userApi");
213
+ return { kind: "home", users: await api.list() };
214
+ }
215
+
216
+ fallback(_params, error) {
217
+ // 1. framework logs the error via its logger (→ reportCallback)
218
+ // 2. you return a degraded page
219
+ return { kind: "home", users: [], degraded: true };
220
+ }
221
+ }
222
+ ```
223
+
224
+ Order of operations on error:
225
+
226
+ 1. `execute()` throws.
227
+ 2. `BaseController` catches, calls `framework.logger.error("[controller:home]", error)`.
228
+ 3. Logger fires `reportCallback("error", "controller:home", [error])` → Sentry.
229
+ 4. `fallback()` returns a `Page` of your design.
230
+ 5. Render proceeds with the fallback page (HTTP 200 by default).
231
+
232
+ To set a non-200 status, return a page with an "error" marker and check it in an `afterLoad` guard:
233
+
234
+ ```ts
235
+ afterLoad: [
236
+ (ctx) => {
237
+ if ("degraded" in ctx.page && ctx.page.degraded) {
238
+ return deny(503, "Service degraded");
239
+ }
240
+ return next();
241
+ },
242
+ ],
243
+ ```
244
+
245
+ ## Metrics (counters / gauges / timing)
246
+
247
+ The framework's `MetricsClient` is minimal:
248
+
249
+ ```ts
250
+ import { DEP_KEYS } from "@finesoft/front";
251
+
252
+ const metrics = framework.container.resolve(DEP_KEYS.METRICS);
253
+
254
+ metrics.increment("requests.total");
255
+ metrics.increment("login.failed", { reason: "wrong_password" });
256
+ metrics.gauge("queue.depth", 17);
257
+ metrics.timing("render.duration", 142);
258
+ ```
259
+
260
+ The default implementation is a no-op. Register your own:
261
+
262
+ ```ts
263
+ framework.container.register(DEP_KEYS.METRICS, () => new StatsdMetrics(/* ... */));
264
+ ```
265
+
266
+ For most apps, `EventRecorder` is enough — your analytics backend already aggregates events into metrics.
267
+
268
+ ## Trace IDs and request correlation
269
+
270
+ A common pattern: tag every log and event with a per-request trace id.
271
+
272
+ ```ts
273
+ // beforeLoad guard
274
+ function traceIdGuard(ctx) {
275
+ const traceId = ctx.getHeader("x-request-id") ?? crypto.randomUUID();
276
+ ctx.container.register("traceId", () => traceId);
277
+ ctx.container.register(
278
+ DEP_KEYS.EVENT_RECORDER,
279
+ () => new WithFieldsRecorder(baseRecorder, [{ getFields: () => ({ traceId }) }]),
280
+ );
281
+ return next();
282
+ }
283
+ ```
284
+
285
+ Register the scoped recorder via `WithFieldsRecorder` so every event in this request automatically carries the trace id.
286
+
287
+ ## Next
288
+
289
+ - [Server & deployment](./09-server-and-deployment.md) — where these primitives meet HTTP
290
+ - [Advanced: custom event recorder](./advanced/custom-event-recorder.md) — production-grade implementation