@uptimizr/collector-server 2.1.0 → 2.2.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.
Files changed (135) hide show
  1. package/AGENTS.md +179 -6
  2. package/README.md +69 -0
  3. package/dist/__tests__/support/registryRequests.d.ts +2 -1
  4. package/dist/__tests__/support/registryRequests.d.ts.map +1 -1
  5. package/dist/__tests__/support/registryRequests.js +17 -1
  6. package/dist/__tests__/support/registryRequests.js.map +1 -1
  7. package/dist/__tests__/support/subscriptionFixtures.d.ts +63 -0
  8. package/dist/__tests__/support/subscriptionFixtures.d.ts.map +1 -0
  9. package/dist/__tests__/support/subscriptionFixtures.js +17 -0
  10. package/dist/__tests__/support/subscriptionFixtures.js.map +1 -0
  11. package/dist/agentReport.d.ts +256 -0
  12. package/dist/agentReport.d.ts.map +1 -0
  13. package/dist/agentReport.js +861 -0
  14. package/dist/agentReport.js.map +1 -0
  15. package/dist/app.d.ts +13 -0
  16. package/dist/app.d.ts.map +1 -1
  17. package/dist/app.js +112 -5
  18. package/dist/app.js.map +1 -1
  19. package/dist/audit.d.ts +5 -1
  20. package/dist/audit.d.ts.map +1 -1
  21. package/dist/audit.js +24 -4
  22. package/dist/audit.js.map +1 -1
  23. package/dist/auth.d.ts +41 -1
  24. package/dist/auth.d.ts.map +1 -1
  25. package/dist/auth.js +47 -0
  26. package/dist/auth.js.map +1 -1
  27. package/dist/cli.js +99 -2
  28. package/dist/cli.js.map +1 -1
  29. package/dist/clickhouseStore.d.ts.map +1 -1
  30. package/dist/clickhouseStore.js +44 -2
  31. package/dist/clickhouseStore.js.map +1 -1
  32. package/dist/config.d.ts +49 -0
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +21 -0
  35. package/dist/config.js.map +1 -1
  36. package/dist/connectionLimiter.d.ts +23 -0
  37. package/dist/connectionLimiter.d.ts.map +1 -0
  38. package/dist/connectionLimiter.js +24 -0
  39. package/dist/connectionLimiter.js.map +1 -0
  40. package/dist/duckdbStore.d.ts.map +1 -1
  41. package/dist/duckdbStore.js +44 -1
  42. package/dist/duckdbStore.js.map +1 -1
  43. package/dist/internalDispatch.d.ts +36 -0
  44. package/dist/internalDispatch.d.ts.map +1 -0
  45. package/dist/internalDispatch.js +50 -0
  46. package/dist/internalDispatch.js.map +1 -0
  47. package/dist/memoryStore.d.ts.map +1 -1
  48. package/dist/memoryStore.js +314 -1
  49. package/dist/memoryStore.js.map +1 -1
  50. package/dist/mssqlStore.d.ts.map +1 -1
  51. package/dist/mssqlStore.js +44 -2
  52. package/dist/mssqlStore.js.map +1 -1
  53. package/dist/postgresStore.d.ts.map +1 -1
  54. package/dist/postgresStore.js +44 -2
  55. package/dist/postgresStore.js.map +1 -1
  56. package/dist/projectMetadata.d.ts +82 -0
  57. package/dist/projectMetadata.d.ts.map +1 -0
  58. package/dist/projectMetadata.js +50 -0
  59. package/dist/projectMetadata.js.map +1 -0
  60. package/dist/routes/context.d.ts +135 -0
  61. package/dist/routes/context.d.ts.map +1 -0
  62. package/dist/routes/context.js +306 -0
  63. package/dist/routes/context.js.map +1 -0
  64. package/dist/routes/insights.d.ts +36 -0
  65. package/dist/routes/insights.d.ts.map +1 -0
  66. package/dist/routes/insights.js +810 -0
  67. package/dist/routes/insights.js.map +1 -0
  68. package/dist/routes/live.d.ts +8 -0
  69. package/dist/routes/live.d.ts.map +1 -1
  70. package/dist/routes/live.js +8 -7
  71. package/dist/routes/live.js.map +1 -1
  72. package/dist/routes/mcp.d.ts +36 -0
  73. package/dist/routes/mcp.d.ts.map +1 -0
  74. package/dist/routes/mcp.js +262 -0
  75. package/dist/routes/mcp.js.map +1 -0
  76. package/dist/routes/meta.d.ts +16 -2
  77. package/dist/routes/meta.d.ts.map +1 -1
  78. package/dist/routes/meta.js +699 -10
  79. package/dist/routes/meta.js.map +1 -1
  80. package/dist/routes/metadata.d.ts +31 -0
  81. package/dist/routes/metadata.d.ts.map +1 -0
  82. package/dist/routes/metadata.js +163 -0
  83. package/dist/routes/metadata.js.map +1 -0
  84. package/dist/routes/narrative.d.ts +36 -0
  85. package/dist/routes/narrative.d.ts.map +1 -0
  86. package/dist/routes/narrative.js +95 -0
  87. package/dist/routes/narrative.js.map +1 -0
  88. package/dist/routes/panels.d.ts +44 -0
  89. package/dist/routes/panels.d.ts.map +1 -0
  90. package/dist/routes/panels.js +108 -0
  91. package/dist/routes/panels.js.map +1 -0
  92. package/dist/routes/query-dsl.d.ts +50 -0
  93. package/dist/routes/query-dsl.d.ts.map +1 -0
  94. package/dist/routes/query-dsl.js +359 -0
  95. package/dist/routes/query-dsl.js.map +1 -0
  96. package/dist/routes/query.d.ts +36 -0
  97. package/dist/routes/query.d.ts.map +1 -1
  98. package/dist/routes/query.js +101 -13
  99. package/dist/routes/query.js.map +1 -1
  100. package/dist/routes/subscriptions.d.ts +52 -0
  101. package/dist/routes/subscriptions.d.ts.map +1 -0
  102. package/dist/routes/subscriptions.js +290 -0
  103. package/dist/routes/subscriptions.js.map +1 -0
  104. package/dist/store.d.ts +113 -2
  105. package/dist/store.d.ts.map +1 -1
  106. package/dist/subscriptions/cli.d.ts +36 -0
  107. package/dist/subscriptions/cli.d.ts.map +1 -0
  108. package/dist/subscriptions/cli.js +143 -0
  109. package/dist/subscriptions/cli.js.map +1 -0
  110. package/dist/subscriptions/delivery.d.ts +101 -0
  111. package/dist/subscriptions/delivery.d.ts.map +1 -0
  112. package/dist/subscriptions/delivery.js +165 -0
  113. package/dist/subscriptions/delivery.js.map +1 -0
  114. package/dist/subscriptions/evaluate.d.ts +119 -0
  115. package/dist/subscriptions/evaluate.d.ts.map +1 -0
  116. package/dist/subscriptions/evaluate.js +326 -0
  117. package/dist/subscriptions/evaluate.js.map +1 -0
  118. package/dist/subscriptions/scheduler.d.ts +97 -0
  119. package/dist/subscriptions/scheduler.d.ts.map +1 -0
  120. package/dist/subscriptions/scheduler.js +332 -0
  121. package/dist/subscriptions/scheduler.js.map +1 -0
  122. package/dist/subscriptions/stream.d.ts +40 -0
  123. package/dist/subscriptions/stream.d.ts.map +1 -0
  124. package/dist/subscriptions/stream.js +38 -0
  125. package/dist/subscriptions/stream.js.map +1 -0
  126. package/dist/version.d.ts +3 -0
  127. package/dist/version.d.ts.map +1 -0
  128. package/dist/version.js +25 -0
  129. package/dist/version.js.map +1 -0
  130. package/dist/webhookSignature.d.ts +64 -0
  131. package/dist/webhookSignature.d.ts.map +1 -0
  132. package/dist/webhookSignature.js +91 -0
  133. package/dist/webhookSignature.js.map +1 -0
  134. package/llms.txt +38 -1
  135. package/package.json +17 -14
package/AGENTS.md CHANGED
@@ -40,6 +40,7 @@ mint one with `uptimizr new-key <projectId> --capabilities query` instead.
40
40
  | `uptimizr migrate` | Apply store migrations. |
41
41
  | `uptimizr regions set <scene>` | Replace a scene's named regions from `--file <regions.json>`. |
42
42
  | `uptimizr regions get <scene>` | Print a scene's named regions as JSON. |
43
+ | `uptimizr agent report` | Run a read-only agent once and write a Markdown report (see below). |
43
44
  | `uptimizr help` | Usage. |
44
45
 
45
46
  `new-key` flags: `--capabilities <list>` (comma-separated; default `query`), `--label <name>`,
@@ -60,17 +61,54 @@ project you mint is the one the running collector resolves.
60
61
  Installed as a dependency, the package exposes the `uptimizr` CLI plus the legacy
61
62
  `uptimizr-collector` bin (equivalent to `uptimizr serve`).
62
63
 
64
+ ### `uptimizr agent report` — headless scheduled reports (ADR 0051 §6)
65
+
66
+ Runs the headless `runAgent` loop from `@uptimizr/agent-core` **once**, in this process,
67
+ over the generated read-only tool catalog against the collector's query API, and writes
68
+ Markdown to a file, stdout or a signed webhook. The collector gains no in-process LLM loop;
69
+ scheduling is the operator's cron / systemd timer / GitHub Action.
70
+
71
+ | Flag | Meaning |
72
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | `--skill <name>` | Required. A packaged skill: `attention_hotspots` (needs `--scene`), `conversion_investigation`, `performance_regression_triage`, `weekly_scene_health`, `xr_comfort_audit`. |
74
+ | `--list-skills` | Print the skills with their descriptions and the metrics each one reads. |
75
+ | `--scene <id>` | Scope the report to one scene. |
76
+ | `--window <NdNhNw>` | Window back from now (`24h`, `7d` default, `2w`), or `--since` / `--until` in epoch ms. |
77
+ | `--out <file or ->` | Markdown destination (default `-`, stdout). |
78
+ | `--json <file or ->` | Structured report: tool calls with arguments, durations and outcomes, plus token usage when reported. |
79
+ | `--webhook <url>` | `POST {markdown, report}` to an `http(s)` URL. |
80
+ | `--max-steps <n>` | Cap on provider turns (default `8`). |
81
+ | `--dry-run` | Print the prompt and tool list; call no provider. |
82
+
83
+ Environment — read from the environment only and never persisted:
84
+ `UPTIMIZR_COLLECTOR_URL`, `UPTIMIZR_API_KEY` (a `query` key is enough; the command only ever
85
+ reads), `UPTIMIZR_AGENT_PROVIDER` (`anthropic` default | `openai` | `scripted`),
86
+ `UPTIMIZR_AGENT_MODEL`, `UPTIMIZR_AGENT_API_KEY` (falls back to `ANTHROPIC_API_KEY` /
87
+ `OPENAI_API_KEY`), `UPTIMIZR_AGENT_ENDPOINT`, `UPTIMIZR_WEBHOOK_SECRET`. The provider key
88
+ never reaches a log, a report or an error message.
89
+
90
+ The system prompt is the shared analytics guidelines plus the rendered `GET /api/v1/context`
91
+ document, so a run uses the project's real scene ids and custom-event names; a collector
92
+ without that endpoint degrades silently. Webhook bodies carry
93
+ `X-Uptimizr-Signature: sha256=<hex HMAC-SHA-256 of the raw body>` and `X-Uptimizr-Delivery`.
94
+ Exit codes: `0` ok · `1` usage/config · `2` provider or delivery failure · `3` report
95
+ produced but incomplete (a tool call failed, or no answer).
96
+
97
+ `UPTIMIZR_AGENT_PROVIDER=scripted` is a documented, model-free provider: it calls exactly the
98
+ tools the skill names and prints what the collector returned. It is for proving wiring in CI —
99
+ it produces data, not analysis.
100
+
63
101
  ## API keys and capabilities (ADR 0051 §7)
64
102
 
65
103
  A key carries a **set of capabilities**, not a single role. `new-key` defaults to `query`;
66
104
  `init` / `new-project` mint their single key with `query`, `query:raw` and `annotate`.
67
105
 
68
- | Capability | Unlocks |
69
- | ----------- | --------------------------------------------------------------------------------------------------------- |
70
- | `query` | The aggregate analytics API, the scene registry, the live token exchange, and `GET /api/v1/audit`. |
71
- | `query:raw` | Raw per-session streams: `GET /api/v1/sessions/:id/events` and `GET /api/v1/live/sessions/:id`. |
72
- | `annotate` | The project **metadata** write path (annotations, glossary, saved analyses, panel specs). Never events. |
73
- | `ingest` | Reserved for server-side write paths. Public ingestion is keyless, so issued keys are normally read keys. |
106
+ | Capability | Unlocks |
107
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
108
+ | `query` | The aggregate analytics API, the scene registry, the live token exchange, and `GET /api/v1/audit`. |
109
+ | `query:raw` | Raw per-session data: `GET /api/v1/sessions/:id/events`, `GET /api/v1/live/sessions/:id`, and the compacted `GET /api/v1/sessions/:id/narrative`. |
110
+ | `annotate` | The project **metadata** write path (annotations, glossary, saved analyses, panel specs). Never events. |
111
+ | `ingest` | Reserved for server-side write paths. Public ingestion is keyless, so issued keys are normally read keys. |
74
112
 
75
113
  The raw endpoints are gated **twice**: the collector must run with `ENABLE_RAW_SESSION_RETENTION`
76
114
  **and** the key must hold `query:raw` — otherwise `403`. Retention alone is not enough.
@@ -100,8 +138,49 @@ Ingestion keeps its separate `COLLECTOR_INGEST_RATE_LIMIT_*` budget.
100
138
  - The read API: sessions, heatmaps (`pointer`, `camera`, `position`, `world`, `gaze`, `mesh-uv`,
101
139
  `click-rays`, `flow`, `perf`, `errors`), mesh/interaction insights, performance and diagnostics,
102
140
  scene/path/funnel analytics, scene representations and regions, and the live SSE endpoints.
141
+ - **`POST /api/v1/query`** (and `GET /api/v1/query?q=<url-encoded JSON>`) — **the query DSL**
142
+ (ADR 0051 §3): one endpoint that runs any registry metric. See below.
103
143
  - `GET /health` — liveness probe, unauthenticated.
104
144
 
145
+ ### The query DSL: `POST /api/v1/query` (ADR 0051 §3)
146
+
147
+ One endpoint for every metric. Name the `metric`, bound it with a `range`, narrow it with the
148
+ filters that metric declares, cap it, pick the envelope:
149
+
150
+ ```jsonc
151
+ {
152
+ "v": 1,
153
+ "metric": "mesh_sources",
154
+ "range": { "since": 1757000000000, "until": 1757600000000 },
155
+ "filters": { "scene": "lobby", "cameraMode": "first-person" },
156
+ "limit": 20,
157
+ "format": "summary",
158
+ }
159
+ ```
160
+
161
+ `GET /api/v1/query?q=<url-encoded JSON>` takes the same document (8 KiB cap) for GET-only clients.
162
+ Both are reads: same `query` capability, same audit trail, same aggregations.
163
+
164
+ - The grammar is **closed** — no SQL, no expression language, typed filters, bounded output,
165
+ unknown keys rejected. `range` is required; `format` defaults to `table` here, not `full`.
166
+ - A metric, dimension or filter outside the registry's vocabulary is a `400` listing **every**
167
+ objection, each with a stable `code`, the offending `path` and (for a closed list) `accepted`.
168
+ Read `accepted` instead of guessing again.
169
+ - `dimensions` may be any subset a metric declares **when** its measure is a portable count —
170
+ event counts, mesh and interaction tallies, input actions, camera gestures. A spatial heatmap or a
171
+ percentile is computed at one fixed grain and refuses anything else, naming the grain it supports.
172
+ - **`compare`** — another `{ range }` or `{ segment }`; the result comes back joined on the
173
+ dimension key as `{ current, previous, delta, deltaPct }`, with a significance test where the
174
+ measure is a count and both windows clear the metric's minimum. Never subtract two results by hand.
175
+ - **`explain: true`** — the compiled plan instead of the rows: the tier, the SQL with its parameters
176
+ left unbound, `params` by name and type (never value), `rowsScanned`, and `warnings` (a capture
177
+ channel that produced nothing, a sample below the metric's minimum, a truncated result).
178
+ - **`drillQuery`** — every row of a `summary` carries the whole query narrowed to that row, ready to
179
+ send straight back.
180
+ - `order` takes a measure column, and only where the result is a ranked list.
181
+ - `filters.event` (an ADR 0038 step predicate, applied as a **cohort** of sessions) and
182
+ `filters.device` (`os` / `browser` on `session_start`) exist only on the generic tier.
183
+
105
184
  ### Result envelopes: `format=full | table | summary` (ADR 0051 §2)
106
185
 
107
186
  Every aggregate endpoint accepts `format`. It **filters nothing** — it picks the result envelope:
@@ -114,6 +193,23 @@ Every aggregate endpoint accepts `format`. It **filters nothing** — it picks t
114
193
  the registry's `maxSummaryRows`. **This is what makes a 500-bin heatmap affordable for an LLM** —
115
194
  prefer it over `full` when feeding a model.
116
195
 
196
+ ### Session narrative: `GET /api/v1/sessions/:id/narrative` (`query:raw`)
197
+
198
+ The one per-session read worth an agent's time. It compacts the raw stream into an ordered account
199
+ of what the session did — scene changes, per-mesh dwell above `minDwellMs`, interactions, frame
200
+ dips below `fpsThreshold`, errors, capability changes, XR entry/exit, the end reason — with
201
+ timestamps **relative to the session's first event** and a closing `summary` entry of totals.
202
+ Bounded by `maxEntries` (default 200, hard cap 1000).
203
+
204
+ `format` here is `full` | `table` | **`text`**. Prefer `text`: one line per entry, about a third of
205
+ the tokens of the JSON, which is what makes a whole session affordable in a context window. There
206
+ is no `summary` envelope — a narrative is already one.
207
+
208
+ Gated **twice**, like the raw stream: `ENABLE_RAW_SESSION_RETENTION` **and** `query:raw`, either
209
+ missing is `403`; an unknown session is `404`. It is a projection, never the stream: no
210
+ `visitorId`, no URL or page metadata, no positions or rays, no `device` detail beyond the engine,
211
+ and custom-event property **keys** only — never their values.
212
+
117
213
  ### Filters
118
214
 
119
215
  Common query params: `since`, `until` (epoch ms), `bins`, `limit`, `scene`, `session`,
@@ -129,6 +225,25 @@ set is carried **inside the signed token**, so `GET /api/v1/live/sessions/:id` c
129
225
  `query:raw` even though `EventSource` cannot send headers. `GET /api/v1/live/presence` and
130
226
  `/live/stream` use the same `?token=...`.
131
227
 
228
+ ### Hosted MCP (`/mcp`, ADR 0051 §7)
229
+
230
+ `COLLECTOR_MCP_HTTP=1` (off by default) makes the collector serve the **Model Context Protocol**
231
+ over MCP's Streamable HTTP transport: `POST /mcp` for JSON-RPC, `GET /mcp` for the server→client
232
+ SSE stream, `DELETE /mcp` to end a session. It is the same server `@uptimizr/mcp` runs over stdio —
233
+ same tools, resources and prompts, built by the same factory — so a remote agent needs only a URL
234
+ and a key, with nothing installed locally.
235
+
236
+ - Every request is authenticated with `x-api-key` **or** `Authorization: Bearer <key>` (a bearer
237
+ alias accepted on this route only) and needs `query`: `401` without a key, `403` without the
238
+ capability, `403` if a session id is presented by a different key than opened it.
239
+ - Tool calls are dispatched to the collector's own query routes **in process**, so they run the
240
+ same validation, scoping and result envelope as the equivalent `curl`, and cost the caller's
241
+ ordinary per-key rate-limit budget once.
242
+ - Bounded by `COLLECTOR_MCP_MAX_SESSIONS` (default `50`, one too many → `503`) and
243
+ `COLLECTOR_MCP_SESSION_TTL_MS` (default 30 minutes idle). Audited with `surface: "mcp-http"`.
244
+ - Behind a reverse proxy, disable response buffering for `/mcp` (it answers with SSE) and pin a
245
+ session to one instance if you run several.
246
+
132
247
  ## Storage (`COLLECTOR_STORE`)
133
248
 
134
249
  | Value | Store |
@@ -142,6 +257,59 @@ set is carried **inside the signed token**, so `GET /api/v1/live/sessions/:id` c
142
257
  All four return **identical analytics** (the cross-engine parity suite). Aggregations are computed
143
258
  at **query time** in v1 — no materialized views.
144
259
 
260
+ ## Conditional subscriptions (ADR 0051 §6)
261
+
262
+ Standing predicates over a registry metric, delivered over SSE and signed webhooks.
263
+
264
+ - `GET /api/v1/subscriptions` · `GET /api/v1/subscriptions/:id` ·
265
+ `GET /api/v1/subscriptions/:id/events` — need `query`.
266
+ - `POST /api/v1/subscriptions` · `PATCH /api/v1/subscriptions/:id` (`{ enabled }` only) ·
267
+ `DELETE /api/v1/subscriptions/:id` · `POST /api/v1/subscriptions/:id/test[?deliver=true]` —
268
+ need **`annotate`**: creating one is how a caller asks the collector to make an outbound
269
+ request on its behalf.
270
+ - `GET /api/v1/subscriptions/stream?token=…` — SSE, live-token auth (ADR 0032 §7), optional
271
+ `&id=` filter, shares `LIVE_MAX_CONNECTIONS`.
272
+
273
+ Predicates: `threshold` (on the metric's registry headline column only), `anomaly`, `movers`,
274
+ `new_value`, `presence`. `evaluate.every` ≥ 1m, `evaluate.window` ≥ 1h. 100 per project, last
275
+ 100 firings each.
276
+
277
+ **A webhook secret is write-only** — accepted on create, never returned; reads carry a mask.
278
+ **Webhook egress is off until `COLLECTOR_WEBHOOK_ALLOWED_HOSTS` names the hosts**, because a
279
+ subscription URL arrives over HTTP and is therefore request-controlled input to an outbound
280
+ request. Bodies are signed `X-Uptimizr-Signature: sha256=<hex>` over the raw bytes; verify before
281
+ parsing, in constant time.
282
+
283
+ `POST …/test` is a dry run by default and answers with _why_ it did or did not fire.
284
+
285
+ ## Pinned panels (ADR 0051 §7)
286
+
287
+ Declarative panels an agent leaves on the project's dashboard. A spec is a title, a query, a chart
288
+ name, an optional encoding, a span and a one-line note — **data**, which the dashboard draws with
289
+ panel components `@uptimizr/react` already ships. No module is loaded and nothing is evaluated, so
290
+ ADR 0041's remote-panel trust decision is not widened.
291
+
292
+ - `GET /api/v1/panels` (`limit`) — the project's pinned panels, **oldest first**: these are grid
293
+ positions, not a feed. Needs `query`.
294
+ - `POST /api/v1/panels` → `201` · `PUT /api/v1/panels/:id` · `DELETE /api/v1/panels/:id` → `204` —
295
+ need **`annotate`**, and are audited like every other metadata write. `PUT` is a full replacement
296
+ (half a spec is not a panel); the row keeps its id, its place in the grid and its original
297
+ authorship. An unknown id — or one belonging to another project, which is deliberately
298
+ indistinguishable — is `404`.
299
+ - **Validated twice**, for two different questions: `panelSpecV1Schema` (`@uptimizr/schema`) for the
300
+ shape, then `validatePanelSpec` (`@uptimizr/metrics`) for the vocabulary — does the metric exist
301
+ and accept these filters, does the chart suit its grain, do the encoding columns exist in its
302
+ result. A failure is `400 { error, issues }`, the same body `POST /api/v1/query` answers with, so
303
+ a client fixes the spec from the response instead of guessing. The second check runs at **pin**
304
+ time because a pinned panel is read weeks later: a line chart with no axis to walk along does not
305
+ fail, it draws something a reader takes for a trend.
306
+ - `409` when the project already holds `LIMITS.maxProjectPanelSpecs` (50) panels.
307
+ - `query.range` may be the literal `"inherit"`, which the dashboard resolves against its filter bar
308
+ on every load, or an explicit `{ since, until }` to pin one period.
309
+ - OpenAPI operation ids `list_panels`, `pin_panel`, `update_panel` and `unpin_panel`, under a
310
+ `panels` tag. `CollectorStore` gains `createPanelSpec` / `listPanelSpecs` / `updatePanelSpec` /
311
+ `deletePanelSpec`, implemented by all five stores.
312
+
145
313
  ## Other configuration
146
314
 
147
315
  - Server / browser access: `COLLECTOR_HOST` (`0.0.0.0`), `COLLECTOR_PORT` (`4318`),
@@ -151,9 +319,14 @@ at **query time** in v1 — no materialized views.
151
319
  `LIVE_MAX_CONNECTIONS`, `LIVE_PRESENCE_INTERVAL_MS`.
152
320
  - Rate limits: `COLLECTOR_RATE_LIMIT_MAX`, `COLLECTOR_RATE_LIMIT_WINDOW_MS`,
153
321
  `COLLECTOR_INGEST_RATE_LIMIT_MAX`, `COLLECTOR_INGEST_RATE_LIMIT_WINDOW_MS`.
322
+ - Subscriptions: `COLLECTOR_SUBSCRIPTIONS` (default on; `0` keeps the API and runs no timers),
323
+ `COLLECTOR_SUBSCRIPTIONS_MAX_CONCURRENT` (default `4`),
324
+ **`COLLECTOR_WEBHOOK_ALLOWED_HOSTS`** (empty = no webhook egress at all).
154
325
  - Agent audit: **`AUDIT_RETENTION_DAYS`** (default `30`; `0` = keep forever),
155
326
  `AUDIT_DASHBOARD_REQUESTS` (default off — requests carrying `x-uptimizr-client: dashboard` are
156
327
  skipped as a volume filter, **not** a security boundary).
328
+ - Hosted MCP: `COLLECTOR_MCP_HTTP` (off by default), `COLLECTOR_MCP_MAX_SESSIONS` (`50`),
329
+ `COLLECTOR_MCP_SESSION_TTL_MS` (`1800000`).
157
330
  - All-in-one dashboard: `COLLECTOR_DASHBOARD_DIR` (point it at a static dashboard export and one
158
331
  process serves ingestion, queries and the UI), `COLLECTOR_CSP` (`strict` or `off`).
159
332
 
package/README.md CHANGED
@@ -75,6 +75,52 @@ so it needs no API key. Over HTTP the same thing is
75
75
  an `annotate`-capable key, and from a client build `registerRegions` in
76
76
  `@uptimizr/sdk-core`.
77
77
 
78
+ ### Scheduled reports: `uptimizr agent report`
79
+
80
+ Run a read-only analytics agent **once** and write a Markdown report — a weekly
81
+ scene-health digest with nobody in the chair. It is an ordinary CLI process that
82
+ reads this collector's query API with an ordinary key, so the collector itself
83
+ gains no LLM loop and scheduling stays yours (cron, a systemd timer, a GitHub
84
+ Action).
85
+
86
+ ```bash
87
+ # The narrow key a report should hold
88
+ npx -p @uptimizr/collector-server uptimizr new-key <projectId> \
89
+ --capabilities query --label "weekly-report"
90
+
91
+ export UPTIMIZR_COLLECTOR_URL=https://collect.example.com
92
+ export UPTIMIZR_API_KEY=utk_… # the query-only key above
93
+ export UPTIMIZR_AGENT_API_KEY=sk-ant-… # your own provider key
94
+
95
+ npx -p @uptimizr/collector-server uptimizr agent report \
96
+ --skill weekly_scene_health --scene lobby --window 7d \
97
+ --out report.md --json report.json --webhook https://hooks.example.com/uptimizr
98
+ ```
99
+
100
+ - `--list-skills` prints the investigations this release ships
101
+ (`attention_hotspots`, `conversion_investigation`,
102
+ `performance_regression_triage`, `weekly_scene_health`, `xr_comfort_audit`) and
103
+ the metrics each one reads — the same packaged methodologies `@uptimizr/mcp`
104
+ offers as prompts.
105
+ - `--dry-run` prints the exact prompt and tool list and calls no provider;
106
+ `UPTIMIZR_AGENT_PROVIDER=scripted` runs the whole path with no model, no key
107
+ and no egress (useful in CI — it produces data, not analysis).
108
+ - Provider configuration is read from the environment only and never persisted:
109
+ `UPTIMIZR_AGENT_PROVIDER` (`anthropic` | `openai` | `scripted`),
110
+ `UPTIMIZR_AGENT_MODEL`, `UPTIMIZR_AGENT_API_KEY`, `UPTIMIZR_AGENT_ENDPOINT`.
111
+ The key is never logged, echoed or written into a report.
112
+ - Webhook deliveries are signed with
113
+ `X-Uptimizr-Signature: sha256=<hex HMAC-SHA-256 of the raw body>` keyed with
114
+ `UPTIMIZR_WEBHOOK_SECRET`, plus a unique `X-Uptimizr-Delivery` id.
115
+ - Exit codes: `0` success · `1` usage/configuration · `2` provider or delivery
116
+ failure · `3` report produced but incomplete (a tool call failed, or no answer).
117
+
118
+ Every report ends with a **Method** section listing each tool call and its
119
+ arguments, so an unattended, model-written document stays auditable. See
120
+ `uptimizr agent report --help`, or the
121
+ [deploy guide](https://uptimizr.com/docs/deploy/collector/#scheduled-agent-reports)
122
+ for a copy-pasteable weekly GitHub Actions workflow.
123
+
78
124
  ### All-in-one: serve the dashboard too
79
125
 
80
126
  The collector can also serve a pre-built static dashboard from its own origin, so
@@ -207,6 +253,26 @@ what makes a 500-bin heatmap affordable for an LLM. See
207
253
 
208
254
  - `GET /health` — liveness probe.
209
255
 
256
+ ### Hosted MCP (`/mcp`, opt-in)
257
+
258
+ Start the collector with `COLLECTOR_MCP_HTTP=1` and it also speaks the **Model Context Protocol**
259
+ over Streamable HTTP, so a remote AI client connects with a URL and an API key instead of running
260
+ `npx @uptimizr/mcp` locally (ADR 0051 §7). It is off by default — without the variable the route is
261
+ not registered.
262
+
263
+ | Method | Path | Purpose |
264
+ | -------- | ------ | ---------------------------------------------------------------------- |
265
+ | `POST` | `/mcp` | JSON-RPC. Without `Mcp-Session-Id`, only `initialize` opens a session. |
266
+ | `GET` | `/mcp` | The server→client SSE stream for an existing session. |
267
+ | `DELETE` | `/mcp` | End a session and release its slot. |
268
+
269
+ Every request is authenticated with `x-api-key` or `Authorization: Bearer <key>` and needs `query`;
270
+ a session may only be driven by the key that opened it. Tools, resources and prompts are exactly
271
+ `@uptimizr/mcp`'s. Sessions are capped by `COLLECTOR_MCP_MAX_SESSIONS` (default `50`, one too many
272
+ → `503`) and expire after `COLLECTOR_MCP_SESSION_TTL_MS` idle (default 30 minutes); tool calls are
273
+ audited with `surface: "mcp-http"`. Behind a reverse proxy, turn response buffering **off** for
274
+ `/mcp`. See the [MCP guide](https://uptimizr.com/docs/guides/mcp/#hosted-transport-streamable-http).
275
+
210
276
  ## Security
211
277
 
212
278
  `@fastify/helmet`, `@fastify/cors` (restricted to `COLLECTOR_CORS_ORIGINS`), and
@@ -223,6 +289,7 @@ if `VISITOR_HASH_SECRET` is missing.
223
289
  | Live SSE routes (`/api/v1/live/*` `GET`s) | `?token=...` | Browser `EventSource` cannot attach custom headers, so live streams use short-lived bearer tokens. |
224
290
  | `GET /health` | None | Liveness probe. |
225
291
  | `GET /api/v1/openapi.json` | None | API documentation, not data — a client needs it before it has a key. Rate-limited like every other route. |
292
+ | `/mcp` (when `COLLECTOR_MCP_HTTP=1`) | `x-api-key` | Hosted MCP. `Authorization: Bearer <key>` is accepted as an alias on this route only; the session id is never a credential on its own. |
226
293
 
227
294
  ### API keys and capabilities
228
295
 
@@ -306,6 +373,8 @@ Environment-driven (see [`.env.example`](../../../.env.example)):
306
373
  - Rate limits: `COLLECTOR_RATE_LIMIT_MAX`, `COLLECTOR_RATE_LIMIT_WINDOW_MS`,
307
374
  `COLLECTOR_INGEST_RATE_LIMIT_MAX`, `COLLECTOR_INGEST_RATE_LIMIT_WINDOW_MS`
308
375
  (a key's own budget overrides the first pair).
376
+ - Hosted MCP: `COLLECTOR_MCP_HTTP` (off by default), `COLLECTOR_MCP_MAX_SESSIONS`
377
+ (default `50`), `COLLECTOR_MCP_SESSION_TTL_MS` (default `1800000`).
309
378
  - Agent audit: `AUDIT_RETENTION_DAYS` (default `30`, `0` = keep forever),
310
379
  `AUDIT_DASHBOARD_REQUESTS` (default off).
311
380
  - All-in-one dashboard: `COLLECTOR_DASHBOARD_DIR` (optional; see
@@ -34,7 +34,8 @@ export declare const TEST_PROXY: {
34
34
  export declare const PATH_PARAM_VALUES: Readonly<Record<string, string>>;
35
35
  /**
36
36
  * Query parameters an endpoint needs beyond the shared range. Only the genuinely
37
- * required ones: `mesh` for the per-mesh UV heatmap and `steps` for the funnel.
37
+ * required ones: `mesh` for the per-mesh UV heatmap, `steps` for the funnel and
38
+ * `metric` for the baseline.
38
39
  */
39
40
  export declare const REQUIRED_QUERY: Readonly<Record<string, Record<string, string>>>;
40
41
  /** The two resource reads, which legitimately 404 when nothing is registered. */
@@ -1 +1 @@
1
- {"version":3,"file":"registryRequests.d.ts","sourceRoot":"","sources":["../../../src/__tests__/support/registryRequests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,8EAA8E;AAC9E,eAAO,MAAM,WAAW,EAAE,eAqBzB,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,UAAU;;;;YAIW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;cAM5C,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;CAMrF,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAK3E,CAAC;AAEF,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAG/C,CAAC;AAEH;;;GAGG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,gBAAgB,EACxB,KAAK,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,GAC3C,MAAM,CAYR"}
1
+ {"version":3,"file":"registryRequests.d.ts","sourceRoot":"","sources":["../../../src/__tests__/support/registryRequests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,8EAA8E;AAC9E,eAAO,MAAM,WAAW,EAAE,eA6BzB,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,UAAU;;;;YAIW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;cAM5C,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;CAMrF,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI9D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAY3E,CAAC;AAEF,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAG/C,CAAC;AAEH;;;GAGG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,gBAAgB,EACxB,KAAK,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,GAC3C,MAAM,CAYR"}
@@ -33,6 +33,14 @@ export const TEST_CONFIG = {
33
33
  cspMode: "strict",
34
34
  auditRetentionDays: 30,
35
35
  auditDashboardRequests: false,
36
+ mcpHttpEnabled: false,
37
+ mcpMaxSessions: 50,
38
+ mcpSessionTtlMs: 1_800_000,
39
+ // The sweeps build an app per case; leaving the scheduler off keeps them free
40
+ // of timers they never assert on. Webhook egress stays disallowed by default.
41
+ subscriptions: false,
42
+ subscriptionsMaxConcurrent: 4,
43
+ webhookAllowedHosts: [],
36
44
  };
37
45
  /** Fixture scene proxy, so the `scene_representation` resource has a hit. */
38
46
  export const TEST_PROXY = {
@@ -60,13 +68,21 @@ export const PATH_PARAM_VALUES = {
60
68
  };
61
69
  /**
62
70
  * Query parameters an endpoint needs beyond the shared range. Only the genuinely
63
- * required ones: `mesh` for the per-mesh UV heatmap and `steps` for the funnel.
71
+ * required ones: `mesh` for the per-mesh UV heatmap, `steps` for the funnel and
72
+ * `metric` for the baseline.
64
73
  */
65
74
  export const REQUIRED_QUERY = {
66
75
  "/api/v1/heatmaps/mesh-uv": { mesh: "box" },
67
76
  "/api/v1/funnel": {
68
77
  steps: JSON.stringify([{ type: "session_start" }, { type: "pointer_click" }]),
69
78
  },
79
+ // A baseline is a baseline *of* something: the metric is the subject rather
80
+ // than a filter, so it is the one insight parameter with no default.
81
+ "/api/v1/insights/baseline": { metric: "list_sessions" },
82
+ // --- anomalies (#306) --- same subject-is-the-parameter rule.
83
+ "/api/v1/insights/anomalies": { metric: "list_sessions" },
84
+ // --- significance / scene health (#307) ---
85
+ "/api/v1/insights/significance": { metric: "list_sessions" },
70
86
  };
71
87
  /** The two resource reads, which legitimately 404 when nothing is registered. */
72
88
  export const RESOURCE_METRICS = new Set([
@@ -1 +1 @@
1
- {"version":3,"file":"registryRequests.js","sourceRoot":"","sources":["../../../src/__tests__/support/registryRequests.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registryRequests.js","sourceRoot":"","sources":["../../../src/__tests__/support/registryRequests.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Shared constants for the subscription route suite (#311).
3
+ *
4
+ * Re-exported through this module rather than imported directly in the test so
5
+ * the numbers and the mask come from the single definitions in
6
+ * `@uptimizr/schema` and `@uptimizr/db` — a test that hard-codes `100` or
7
+ * `"••••••••"` stops testing the bound the moment the bound moves.
8
+ *
9
+ * Not a test file — the filename has no `.test.` segment, so Vitest does not
10
+ * collect it.
11
+ */
12
+ export declare const LIMITS: {
13
+ readonly maxBatchEvents: 1000;
14
+ readonly maxProjectIdLength: 128;
15
+ readonly maxSessionIdLength: 128;
16
+ readonly maxSdkVersionLength: 64;
17
+ readonly maxUrlLength: 2048;
18
+ readonly maxTitleLength: 512;
19
+ readonly maxReferrerLength: 2048;
20
+ readonly maxLanguageLength: 35;
21
+ readonly maxMeshNameLength: 256;
22
+ readonly maxAssetNameLength: 1024;
23
+ readonly maxCustomNameLength: 128;
24
+ readonly maxCustomPropValueLength: 1024;
25
+ readonly maxCustomPropEntries: 64;
26
+ readonly maxUserIdLength: 128;
27
+ readonly maxUserTraitValueLength: 1024;
28
+ readonly maxUserTraitEntries: 64;
29
+ readonly maxSceneDescriptionLength: 256;
30
+ readonly maxCameraNameLength: 128;
31
+ readonly maxSceneProxyMeshNameLength: 256;
32
+ readonly maxSceneProxyMeshPathLength: 512;
33
+ readonly maxSceneProxyMeshes: 10000;
34
+ readonly maxSceneRegionLabelLength: 120;
35
+ readonly maxSceneRegionDescriptionLength: 500;
36
+ readonly maxSceneRegions: 200;
37
+ readonly maxSubscriptionNameLength: 120;
38
+ readonly maxSubscriptionDeliveries: 4;
39
+ readonly maxSubscriptionsPerProject: 100;
40
+ readonly maxSubscriptionEvents: 100;
41
+ readonly maxNodeIdLength: 128;
42
+ readonly maxBoneIdLength: 128;
43
+ readonly maxChildPathLength: 512;
44
+ readonly maxGraphicsDiagnosticMessageLength: 1024;
45
+ readonly maxGraphicsDiagnosticCodeLength: 64;
46
+ readonly maxAnnotationTextLength: 2000;
47
+ readonly maxAnnotationTargetIdLength: 256;
48
+ readonly maxProjectAnnotations: 500;
49
+ readonly maxGlossaryTermLength: 64;
50
+ readonly maxGlossaryMeaningLength: 500;
51
+ readonly maxProjectGlossaryEntries: 200;
52
+ readonly maxSavedAnalysisTitleLength: 120;
53
+ readonly maxSavedAnalysisConclusionLength: 4000;
54
+ readonly maxSavedAnalysisQueryLength: 8000;
55
+ readonly maxProjectSavedAnalyses: 200;
56
+ readonly maxPanelSpecTitleLength: 120;
57
+ readonly maxPanelSpecNoteLength: 500;
58
+ readonly maxPanelEncodingColumnLength: 64;
59
+ readonly maxProjectPanelSpecs: 50;
60
+ };
61
+ /** What every read path shows in place of a stored webhook secret. */
62
+ export declare const MASKED_SECRET_PLACEHOLDER = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
63
+ //# sourceMappingURL=subscriptionFixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptionFixtures.d.ts","sourceRoot":"","sources":["../../../src/__tests__/support/subscriptionFixtures.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC;AAEpC,sEAAsE;AACtE,eAAO,MAAM,yBAAyB,qDAAgB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { LIMITS as SCHEMA_LIMITS } from "@uptimizr/schema";
2
+ import { MASKED_SECRET } from "@uptimizr/db";
3
+ /**
4
+ * Shared constants for the subscription route suite (#311).
5
+ *
6
+ * Re-exported through this module rather than imported directly in the test so
7
+ * the numbers and the mask come from the single definitions in
8
+ * `@uptimizr/schema` and `@uptimizr/db` — a test that hard-codes `100` or
9
+ * `"••••••••"` stops testing the bound the moment the bound moves.
10
+ *
11
+ * Not a test file — the filename has no `.test.` segment, so Vitest does not
12
+ * collect it.
13
+ */
14
+ export const LIMITS = SCHEMA_LIMITS;
15
+ /** What every read path shows in place of a stored webhook secret. */
16
+ export const MASKED_SECRET_PLACEHOLDER = MASKED_SECRET;
17
+ //# sourceMappingURL=subscriptionFixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptionFixtures.js","sourceRoot":"","sources":["../../../src/__tests__/support/subscriptionFixtures.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,MAAM,GAAG,aAAa,CAAC;AAEpC,sEAAsE;AACtE,MAAM,CAAC,MAAM,yBAAyB,GAAG,aAAa,CAAC"}