@finesoft/front 0.5.1 → 0.5.3
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/README.md +4 -4
- package/dist/Outlet.svelte +39 -0
- package/dist/Outlet.svelte.d.ts +7 -0
- package/dist/browser-CR5vhgXg.mjs +1317 -0
- package/dist/browser-kFMjlLGT.d.mts +262 -0
- package/dist/browser.d.mts +7 -2
- package/dist/browser.mjs +9 -1
- package/dist/controller-types-CgmJ6-le.d.mts +16 -0
- package/dist/cookies-BiRlUanX.d.mts +786 -0
- package/dist/fetch-policy-BHT8RtrL.mjs +82 -0
- package/dist/host-guard-DDWxLpFL.mjs +222 -0
- package/dist/http-B6CJqDyf.d.mts +46 -0
- package/dist/http-CaxrMD1A.d.mts +1 -0
- package/dist/http-D70PL72H.mjs +257 -0
- package/dist/http.d.mts +3 -0
- package/dist/http.mjs +2 -0
- package/dist/index-node.d.mts +15 -0
- package/dist/index-node.mjs +15 -0
- package/dist/index.d.mts +48 -697
- package/dist/index.mjs +44 -261
- package/dist/load-node.d.mts +5 -0
- package/dist/load-node.mjs +10 -0
- package/dist/load-portable.d.mts +5 -0
- package/dist/load-portable.mjs +8 -0
- package/dist/lru-map-BKoUAySU.mjs +50 -0
- package/dist/messages-CAt2QdGr.mjs +140 -0
- package/dist/native-contract-DuR25hYB.d.mts +14 -0
- package/dist/native-contract.d.mts +2 -0
- package/dist/native-contract.mjs +1 -0
- package/dist/node-D9hB4dsz.d.mts +35 -0
- package/dist/node.d.mts +2 -0
- package/dist/node.mjs +59 -0
- package/dist/path-CGFl2w7D.mjs +113 -0
- package/dist/path-CXT6xGPO.d.mts +261 -0
- package/dist/portable-CaxrMD1A.d.mts +1 -0
- package/dist/portable.d.mts +11 -0
- package/dist/portable.mjs +12 -0
- package/dist/proxy-2dSWO-Xw.d.mts +53 -0
- package/dist/proxy-z02VvGIj.mjs +7520 -0
- package/dist/public-types-BcJM-AYc.mjs +835 -0
- package/dist/react-DhwBRw01.d.mts +16 -0
- package/dist/react.d.mts +3 -0
- package/dist/react.mjs +29 -0
- package/dist/rolldown-runtime-B4iAMlE-.mjs +35 -0
- package/dist/secure-fetch-Xlht2jd7.d.mts +30 -0
- package/dist/server-controller-proxy-BkVuVWVD.d.mts +38 -0
- package/dist/session-DnB4ZC3x.d.mts +1279 -0
- package/dist/src-BQBfaaPO.mjs +3825 -0
- package/dist/src-Ftl_0rhu.mjs +28 -0
- package/dist/ssr-BLzYP4wU.d.mts +207 -0
- package/dist/ssr-Tn4YkuxM.mjs +357 -0
- package/dist/ssr.d.mts +3 -0
- package/dist/ssr.mjs +3 -0
- package/dist/svelte-Dr5to3SE.d.mts +16 -0
- package/dist/svelte.d.mts +3 -0
- package/dist/svelte.mjs +13 -0
- package/dist/typegen-C-WeJCtf.d.mts +12 -0
- package/dist/typegen-cli.d.mts +1 -0
- package/dist/typegen-cli.mjs +11 -0
- package/dist/typegen.d.mts +3 -0
- package/dist/typegen.mjs +2 -0
- package/dist/types-B1BT0N3t.mjs +402 -0
- package/dist/undici-CPfL25Hr.mjs +22262 -0
- package/dist/vite-Cj4SPA8D.d.mts +277 -0
- package/dist/vite.d.mts +4 -0
- package/dist/vite.mjs +2354 -0
- package/dist/vue-DGmzuKho.d.mts +32 -0
- package/dist/vue.d.mts +3 -0
- package/dist/vue.mjs +56 -0
- package/dist/web.d.mts +6 -0
- package/dist/web.mjs +8 -0
- package/dist/worker.d.mts +2 -0
- package/dist/worker.mjs +2 -0
- package/docs/01-getting-started.md +67 -199
- package/docs/02-routing-and-controllers.md +163 -241
- package/docs/03-middleware.md +10 -212
- package/docs/04-rendering-and-hydration.md +7 -332
- package/docs/05-i18n.md +7 -237
- package/docs/06-http-client.md +20 -263
- package/docs/07-di-container.md +23 -257
- package/docs/08-observability.md +6 -286
- package/docs/09-server-and-deployment.md +59 -219
- package/docs/10-features-platform-pwa.md +7 -231
- package/docs/11-navigation.md +143 -288
- package/docs/12-session-restoration.md +6 -214
- package/docs/README.md +7 -7
- package/docs/advanced/custom-action-handler.md +29 -229
- package/docs/advanced/custom-adapter.md +7 -259
- package/docs/advanced/custom-event-recorder.md +7 -312
- package/docs/advanced/inline-proxy-codegen.md +6 -185
- package/docs/advanced/multi-tenant-scopes.md +10 -323
- package/docs/engineering/ci-release-flow.md +35 -222
- package/docs/engineering/project-structure.md +34 -277
- package/docs/engineering/testing.md +8 -310
- package/docs/pitfalls/container-scope-leak.md +2 -214
- package/docs/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/pitfalls/ssr-hydration-mismatch.md +9 -155
- package/docs/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/docs/zh/01-getting-started.md +67 -199
- package/docs/zh/02-routing-and-controllers.md +166 -244
- package/docs/zh/03-middleware.md +10 -212
- package/docs/zh/04-rendering-and-hydration.md +7 -332
- package/docs/zh/05-i18n.md +7 -237
- package/docs/zh/06-http-client.md +20 -263
- package/docs/zh/07-di-container.md +23 -257
- package/docs/zh/08-observability.md +6 -283
- package/docs/zh/09-server-and-deployment.md +59 -219
- package/docs/zh/10-features-platform-pwa.md +7 -231
- package/docs/zh/11-navigation.md +130 -290
- package/docs/zh/12-session-restoration.md +6 -214
- package/docs/zh/README.md +4 -4
- package/docs/zh/advanced/custom-action-handler.md +29 -229
- package/docs/zh/advanced/custom-adapter.md +7 -259
- package/docs/zh/advanced/custom-event-recorder.md +7 -312
- package/docs/zh/advanced/inline-proxy-codegen.md +6 -185
- package/docs/zh/advanced/multi-tenant-scopes.md +10 -323
- package/docs/zh/engineering/ci-release-flow.md +35 -222
- package/docs/zh/engineering/project-structure.md +34 -277
- package/docs/zh/engineering/testing.md +8 -310
- package/docs/zh/pitfalls/container-scope-leak.md +2 -214
- package/docs/zh/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/zh/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/zh/pitfalls/ssr-hydration-mismatch.md +9 -155
- package/docs/zh/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/package.json +118 -20
- package/dist/browser-BHhVWXik.mjs +0 -2
- package/dist/browser-BV2BBXm7.d.mts +0 -2811
package/docs/07-di-container.md
CHANGED
|
@@ -1,264 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dependencies and resource ownership
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
3
|
+
Declare typed providers on the app/runtime. Ordinary calls use runtime.execute; it owns the invocation scope. Scope providers share one initialization within that invocation; runtime providers live until runtime.dispose. Transient values are created per acquisition and tracked for cleanup. Created values are owned by default; supplied values are external unless owned is true. Async initialization and disposal are awaited in dependency order. Runtime providers cannot capture request-only bindings. The legacy named Container remains available for existing controller dependencies; new asynchronous services should use context.get(token).
|
|
73
4
|
|
|
74
5
|
```ts
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
/* ... */
|
|
6
|
+
import { createToken, provide, defineApp, defineOperation, createRuntime } from "@finesoft/front";
|
|
7
|
+
const tenant = createToken<string>("tenant");
|
|
8
|
+
const who = defineOperation({
|
|
9
|
+
id: "who",
|
|
10
|
+
kind: "query",
|
|
11
|
+
handler: async (_: undefined, context) => context.get(tenant),
|
|
131
12
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
/* ... */
|
|
13
|
+
const app = defineApp({
|
|
14
|
+
id: "example",
|
|
15
|
+
operations: [who],
|
|
16
|
+
providers: [
|
|
17
|
+
provide({
|
|
18
|
+
token: tenant,
|
|
19
|
+
lifetime: "scope",
|
|
20
|
+
create: ({ bindings }) => String(bindings.tenant),
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
200
23
|
});
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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);
|
|
24
|
+
const runtime = createRuntime({ app });
|
|
25
|
+
try {
|
|
26
|
+
await runtime.execute(who, undefined, { bindings: { tenant: "one" } });
|
|
27
|
+
} finally {
|
|
28
|
+
await runtime.dispose();
|
|
231
29
|
}
|
|
232
30
|
```
|
|
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
|
package/docs/08-observability.md
CHANGED
|
@@ -1,290 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Observability
|
|
2
2
|
|
|
3
|
-
|
|
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:
|
|
3
|
+
Use a runtime recorder for portable structured events. Web logging configuration belongs to app.configuration; do not create another framework instance solely to register diagnostics. ReportingLoggerFactory can forward warnings/errors through an application callback. Do not include private inputs, credentials or arbitrary upstream errors in public diagnostics. Browser impression observation is selected from the browser entry.
|
|
136
4
|
|
|
137
5
|
```ts
|
|
138
|
-
import {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
}
|
|
6
|
+
import { ConsoleEventRecorder, createRuntime } from "@finesoft/front";
|
|
7
|
+
const runtime = createRuntime({ app, recorder: new ConsoleEventRecorder() });
|
|
8
|
+
// In an operation: context.record("checkout.completed", { itemCount: 2 });
|
|
9
|
+
// In defineWebApp: configuration: { eventRecorder: recorder, reportCallback }
|
|
222
10
|
```
|
|
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
|