@zerotal/monitor 1.9.0 → 1.11.0

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/CHANGELOG.md CHANGED
@@ -8,6 +8,21 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.10.0] — 2026-08-30
12
+
13
+ ### Fixed
14
+
15
+ - **`MonitorStore` options no longer overwrite their own defaults with `undefined`.**
16
+ The constructor applied `?? 100`-style defaults and then spread `...opts` **after**
17
+ them, and object spread copies own properties even when their value is `undefined`.
18
+ So `new MonitorStore({ retentionDays: cfg.retentionDays })` with an unset config put
19
+ `undefined` straight back over the 7, and `prune()` computed a cutoff of
20
+ `Date.now() - undefined * DAY_MS` — `NaN`, which prunes nothing and reports nothing.
21
+ Spreading last is what made every `??` in that constructor decorative.
22
+
23
+ The caller's options now go first and the defaults fill the gaps, which is the
24
+ ordering `Socket`'s constructor already documents at length for the same reason.
25
+
11
26
  ## [1.9.0] — 2026-08-29
12
27
 
13
28
  ### Added
package/api-surface.md CHANGED
@@ -286,32 +286,32 @@ interface ModelStat = {
286
286
  }
287
287
 
288
288
  interface MonitorConfigShape = {
289
- accent?: string
290
- alertCooldownMs?: number
291
- alertThresholds?: AlertThresholds
292
- alertWebhook?: string
293
- alerts?: boolean
294
- apdexTargetMs?: number
295
- auth?: (user: unknown) => boolean | Promise<boolean>
296
- capturePayloads?: boolean
297
- deploy?: string
298
- metrics?: boolean
299
- metricsPath?: string
300
- path?: string
301
- payloadMaxBytes?: number
302
- record?: boolean
303
- refreshMs?: number
304
- region?: string
305
- retentionDays?: number
306
- retentionMode?: RetentionMode
307
- sections?: Record<string, boolean>
308
- slowQueryMs?: number
309
- slowRequestMs?: number
310
- snapshotCacheMs?: number
311
- storage?: string
312
- subtitle?: string
313
- title?: string
314
- zerotalVersion?: string
289
+ accent?: string | undefined
290
+ alertCooldownMs?: number | undefined
291
+ alertThresholds?: AlertThresholds | undefined
292
+ alertWebhook?: string | undefined
293
+ alerts?: boolean | undefined
294
+ apdexTargetMs?: number | undefined
295
+ auth?: ((user: unknown) => boolean | Promise<boolean>) | undefined
296
+ capturePayloads?: boolean | undefined
297
+ deploy?: string | undefined
298
+ metrics?: boolean | undefined
299
+ metricsPath?: string | undefined
300
+ path?: string | undefined
301
+ payloadMaxBytes?: number | undefined
302
+ record?: boolean | undefined
303
+ refreshMs?: number | undefined
304
+ region?: string | undefined
305
+ retentionDays?: number | undefined
306
+ retentionMode?: RetentionMode | undefined
307
+ sections?: Record<string, boolean> | undefined
308
+ slowQueryMs?: number | undefined
309
+ slowRequestMs?: number | undefined
310
+ snapshotCacheMs?: number | undefined
311
+ storage?: string | undefined
312
+ subtitle?: string | undefined
313
+ title?: string | undefined
314
+ zerotalVersion?: string | undefined
315
315
  }
316
316
 
317
317
  interface MonitorSection = {
@@ -386,16 +386,16 @@ interface MonitorStat = {
386
386
  }
387
387
 
388
388
  interface MonitorStoreOptions = {
389
- apdexTargetMs?: number
390
- deploy?: string
391
- region?: string
392
- retentionDays?: number
393
- retentionMode?: RetentionMode
394
- slowQueryMs?: number
395
- slowRequestMs?: number
396
- snapshotCacheMs?: number
397
- storage?: string
398
- zerotalVersion?: string
389
+ apdexTargetMs?: number | undefined
390
+ deploy?: string | undefined
391
+ region?: string | undefined
392
+ retentionDays?: number | undefined
393
+ retentionMode?: RetentionMode | undefined
394
+ slowQueryMs?: number | undefined
395
+ slowRequestMs?: number | undefined
396
+ snapshotCacheMs?: number | undefined
397
+ storage?: string | undefined
398
+ zerotalVersion?: string | undefined
399
399
  }
400
400
 
401
401
  interface MonitorTable = {
@@ -525,30 +525,30 @@ interface RequestSpan = {
525
525
  interface ResolvedMonitorConfig = {
526
526
  accent: string
527
527
  alertCooldownMs: number
528
- alertThresholds?: AlertThresholds
529
- alertWebhook?: string
528
+ alertThresholds?: AlertThresholds | undefined
529
+ alertWebhook?: string | undefined
530
530
  alerts: boolean
531
531
  apdexTargetMs: number
532
532
  auth: (user: unknown) => boolean | Promise<boolean>
533
533
  capturePayloads: boolean
534
- deploy?: string
534
+ deploy?: string | undefined
535
535
  metrics: boolean
536
536
  metricsPath: string
537
537
  path: string
538
538
  payloadMaxBytes: number
539
539
  record: boolean
540
540
  refreshMs: number
541
- region?: string
541
+ region?: string | undefined
542
542
  retentionDays: number
543
543
  retentionMode: RetentionMode
544
- sections?: Record<string, boolean>
544
+ sections?: Record<string, boolean> | undefined
545
545
  slowQueryMs: number
546
546
  slowRequestMs: number
547
547
  snapshotCacheMs: number
548
548
  storage: string
549
549
  subtitle: string
550
550
  title: string
551
- zerotalVersion?: string
551
+ zerotalVersion?: string | undefined
552
552
  }
553
553
 
554
554
  interface RouteDetail = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/monitor",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -30,14 +30,14 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@zerotal/core": "1.9.0",
34
- "@zerotal/flow-ui": "1.9.0",
35
- "@zerotal/flow": "1.9.0"
33
+ "@zerotal/core": "1.11.0",
34
+ "@zerotal/flow-ui": "1.11.0",
35
+ "@zerotal/flow": "1.11.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@zerotal/queue": "1.9.0",
39
- "@zerotal/scheduler": "1.9.0",
40
- "@zerotal/telemetry": "1.9.0"
38
+ "@zerotal/queue": "1.11.0",
39
+ "@zerotal/scheduler": "1.11.0",
40
+ "@zerotal/telemetry": "1.11.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@zerotal/queue": "^1.0.0",
@@ -12,6 +12,7 @@
12
12
  * A retention policy (`prune`) deletes or archives data past the configured
13
13
  * window, and `wipe` clears everything on demand from the panel.
14
14
  */
15
+ import type { Resolved } from "@zerotal/core";
15
16
  import { percentile } from "./store/RingBuffer.ts";
16
17
  import { MonitorDb } from "./store/MonitorDb.ts";
17
18
  import {
@@ -107,25 +108,25 @@ interface ReqSample {
107
108
  }
108
109
 
109
110
  export interface MonitorStoreOptions {
110
- apdexTargetMs?: number;
111
- slowQueryMs?: number;
112
- zerotalVersion?: string;
113
- region?: string;
114
- deploy?: string;
111
+ apdexTargetMs?: number | undefined;
112
+ slowQueryMs?: number | undefined;
113
+ zerotalVersion?: string | undefined;
114
+ region?: string | undefined;
115
+ deploy?: string | undefined;
115
116
  /** SQLite path. `:memory:` (default) for tests; a file path for persistence. */
116
- storage?: string;
117
+ storage?: string | undefined;
117
118
  /** Days of history to keep before pruning. Default: 7. */
118
- retentionDays?: number;
119
+ retentionDays?: number | undefined;
119
120
  /** What to do with data past retention: delete it or move it to the archive. Default: `delete`. */
120
- retentionMode?: RetentionMode;
121
+ retentionMode?: RetentionMode | undefined;
121
122
  /** Requests at/above this many ms count as "slow". Default: 1000. */
122
- slowRequestMs?: number;
123
+ slowRequestMs?: number | undefined;
123
124
  /**
124
125
  * Cache built snapshots for this many ms, keyed by range. De-dupes overlapping
125
126
  * builds (panel poll + Prometheus scrape + alert loop all hit "live"). Mutating
126
127
  * actions invalidate it. Default: 0 (off) — the provider enables it in production.
127
128
  */
128
- snapshotCacheMs?: number;
129
+ snapshotCacheMs?: number | undefined;
129
130
  }
130
131
 
131
132
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -139,22 +140,32 @@ export class MonitorStore {
139
140
  private readonly _snapTtlMs: number;
140
141
  private readonly _snapCache = new Map<MonitorRange, { at: number; snap: MonitorSnapshot }>();
141
142
 
142
- private readonly _opts: Required<
143
+ private readonly _opts: Resolved<
143
144
  Pick<
144
145
  MonitorStoreOptions,
145
146
  "apdexTargetMs" | "slowQueryMs" | "slowRequestMs" | "retentionDays" | "retentionMode"
146
147
  >
147
148
  > &
148
- MonitorStoreOptions;
149
+ Omit<
150
+ MonitorStoreOptions,
151
+ "apdexTargetMs" | "slowQueryMs" | "slowRequestMs" | "retentionDays" | "retentionMode"
152
+ >;
149
153
 
150
154
  constructor(opts: MonitorStoreOptions = {}) {
155
+ // The caller's options go FIRST, then the defaults fill the gaps — the same
156
+ // ordering, for the same reason, as `Socket`'s constructor documents at length.
157
+ // Object spread copies own properties even when their value is `undefined`, so
158
+ // `new MonitorStore({ retentionDays: cfg.retentionDays })` with an unset config
159
+ // put `undefined` straight back over the 7 and `prune()` then computed a cutoff
160
+ // of `Date.now() - undefined * DAY_MS` — NaN, which deletes nothing and reports
161
+ // nothing. Spreading last is what made the `??` above decorative.
151
162
  this._opts = {
163
+ ...opts,
152
164
  apdexTargetMs: opts.apdexTargetMs ?? 100,
153
165
  slowQueryMs: opts.slowQueryMs ?? 100,
154
166
  slowRequestMs: opts.slowRequestMs ?? 1000,
155
167
  retentionDays: opts.retentionDays ?? 7,
156
168
  retentionMode: opts.retentionMode ?? "delete",
157
- ...opts,
158
169
  };
159
170
  this._snapTtlMs = Math.max(0, opts.snapshotCacheMs ?? 0);
160
171
  this._db = new MonitorDb(opts.storage ?? ":memory:");
package/src/config.ts CHANGED
@@ -13,91 +13,91 @@ import type { AlertThresholds } from "./alerting.ts";
13
13
 
14
14
  export interface MonitorConfigShape {
15
15
  /** URL prefix the panel mounts at. Default: `/monitor`. */
16
- path?: string;
16
+ path?: string | undefined;
17
17
  /** Browser tab title / sidebar heading. Default: `Super Panel`. */
18
- title?: string;
18
+ title?: string | undefined;
19
19
  /** Sub-label under the title. Default: `Zerotal Ops`. */
20
- subtitle?: string;
20
+ subtitle?: string | undefined;
21
21
  /**
22
22
  * Authorization gate. Receives the authenticated user (or undefined) and
23
23
  * must return true to allow access. Default: allow only outside production.
24
24
  */
25
- auth?: (user: unknown) => boolean | Promise<boolean>;
25
+ auth?: ((user: unknown) => boolean | Promise<boolean>) | undefined;
26
26
  /** Install the recorder middleware to capture live request data. Default: true. */
27
- record?: boolean;
27
+ record?: boolean | undefined;
28
28
  /** Auto-refresh cadence in milliseconds while "Live" is on. Default: 3000. */
29
- refreshMs?: number;
29
+ refreshMs?: number | undefined;
30
30
  /** Apdex satisfaction threshold (T) in ms. Default: 100. */
31
- apdexTargetMs?: number;
31
+ apdexTargetMs?: number | undefined;
32
32
  /** Queries at/above this many ms count as "slow". Default: 100. */
33
- slowQueryMs?: number;
33
+ slowQueryMs?: number | undefined;
34
34
  /** Requests at/above this many ms count as "slow" (Slow Requests widget). Default: 1000. */
35
- slowRequestMs?: number;
35
+ slowRequestMs?: number | undefined;
36
36
  /**
37
37
  * Capture request/response headers and bodies on each request (Telescope-style),
38
38
  * shown in the request trace. Off by default — it buffers bodies and is
39
39
  * privacy-sensitive. Sensitive headers (authorization/cookie) and body keys
40
40
  * (password/token/secret/…) are redacted automatically.
41
41
  */
42
- capturePayloads?: boolean;
42
+ capturePayloads?: boolean | undefined;
43
43
  /** Truncate captured request/response bodies to this many bytes. Default: 65536. */
44
- payloadMaxBytes?: number;
44
+ payloadMaxBytes?: number | undefined;
45
45
  /**
46
46
  * Cache built snapshots for this many ms, keyed by range, so overlapping reads
47
47
  * (panel poll + Prometheus scrape + alert loop) share one build. Mutating actions
48
48
  * invalidate it. Default: 1000. Set `0` to always build fresh.
49
49
  */
50
- snapshotCacheMs?: number;
50
+ snapshotCacheMs?: number | undefined;
51
51
  /**
52
52
  * SQLite file the panel persists to, so history survives restarts and the
53
53
  * 1h/24h/7d ranges trace real data. Default: `storage/monitor.sqlite`. Use
54
54
  * `:memory:` for an ephemeral, in-process store.
55
55
  */
56
- storage?: string;
56
+ storage?: string | undefined;
57
57
  /** Days of history to keep before pruning. Default: 7. */
58
- retentionDays?: number;
58
+ retentionDays?: number | undefined;
59
59
  /** What to do with data past retention: `delete` it or `archive` it. Default: `delete`. */
60
- retentionMode?: RetentionMode;
60
+ retentionMode?: RetentionMode | undefined;
61
61
  /**
62
62
  * Expose a Prometheus text-exposition endpoint. Default: `false` (opt-in).
63
63
  * The endpoint is unauthenticated (a scraper can't satisfy user-auth), so it
64
64
  * ships off; enable it only when you protect `metricsPath` at the network
65
65
  * layer (firewall/ingress) or scope it to a private interface.
66
66
  */
67
- metrics?: boolean;
67
+ metrics?: boolean | undefined;
68
68
  /** Path for the Prometheus endpoint. Default: `/metrics`. Protect it at the network layer. */
69
- metricsPath?: string;
69
+ metricsPath?: string | undefined;
70
70
  /** Evaluate threshold alerts on a short interval. Default: `true`. */
71
- alerts?: boolean;
71
+ alerts?: boolean | undefined;
72
72
  /** Alert thresholds (error rate, queue backlog, p95, rollbacks). */
73
- alertThresholds?: AlertThresholds;
73
+ alertThresholds?: AlertThresholds | undefined;
74
74
  /**
75
75
  * Minimum gap (ms) before the same alert can fire again after it recovers and
76
76
  * re-breaches. Stops an oscillating metric from re-paging / re-flooding the feed
77
77
  * for one ongoing issue. Default: 30 min. Set `0` to fire on every fresh breach.
78
78
  */
79
- alertCooldownMs?: number;
79
+ alertCooldownMs?: number | undefined;
80
80
  /**
81
81
  * Slack-compatible webhook URL. When set, every newly-firing alert is POSTed to
82
82
  * it as JSON (`{ text, level, title, detail }`) so alerts page someone instead of
83
83
  * only lighting up the panel. Dependency-free (uses `fetch`); delivery is
84
84
  * best-effort. For richer routing, register a handler with `onAlert()` instead.
85
85
  */
86
- alertWebhook?: string;
86
+ alertWebhook?: string | undefined;
87
87
  /** Accent colour (hex) for the panel chrome. Default: Zerotal orange. */
88
- accent?: string;
88
+ accent?: string | undefined;
89
89
  /** Version string shown in the System footer. */
90
- zerotalVersion?: string;
90
+ zerotalVersion?: string | undefined;
91
91
  /** Region label shown in the System footer. */
92
- region?: string;
92
+ region?: string | undefined;
93
93
  /** Deploy SHA shown in the System footer. */
94
- deploy?: string;
94
+ deploy?: string | undefined;
95
95
  /**
96
96
  * Switch contributed sections off by id — `{ scheduler: false }` keeps the
97
97
  * scheduler installed but drops its section from the panel. Anything absent
98
98
  * here is on.
99
99
  */
100
- sections?: Record<string, boolean>;
100
+ sections?: Record<string, boolean> | undefined;
101
101
  }
102
102
 
103
103
  export interface ResolvedMonitorConfig extends MonitorConfigShape {