@iann29/rastro 0.1.0-alpha.9 → 0.1.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 (98) hide show
  1. package/README.md +246 -57
  2. package/agent/integration.md +223 -65
  3. package/agent/manifest.json +34 -13
  4. package/agent/manifest.schema.json +53 -11
  5. package/dist/client/federation.d.ts +196 -11
  6. package/dist/client/federation.d.ts.map +1 -1
  7. package/dist/client/federation.js +160 -6
  8. package/dist/client/federation.js.map +1 -1
  9. package/dist/client/heat.d.ts +37 -0
  10. package/dist/client/heat.d.ts.map +1 -0
  11. package/dist/client/heat.js +43 -0
  12. package/dist/client/heat.js.map +1 -0
  13. package/dist/client/index.d.ts +441 -98
  14. package/dist/client/index.d.ts.map +1 -1
  15. package/dist/client/index.js +207 -17
  16. package/dist/client/index.js.map +1 -1
  17. package/dist/component/_generated/api.d.ts +6 -0
  18. package/dist/component/_generated/api.d.ts.map +1 -1
  19. package/dist/component/_generated/api.js.map +1 -1
  20. package/dist/component/_generated/component.d.ts +50 -13
  21. package/dist/component/_generated/component.d.ts.map +1 -1
  22. package/dist/component/constants.d.ts +3 -0
  23. package/dist/component/constants.d.ts.map +1 -1
  24. package/dist/component/constants.js +6 -0
  25. package/dist/component/constants.js.map +1 -1
  26. package/dist/component/coverage.d.ts +9 -0
  27. package/dist/component/coverage.d.ts.map +1 -1
  28. package/dist/component/coverage.js +36 -6
  29. package/dist/component/coverage.js.map +1 -1
  30. package/dist/component/geo.d.ts +2 -0
  31. package/dist/component/geo.d.ts.map +1 -1
  32. package/dist/component/geo.js +56 -19
  33. package/dist/component/geo.js.map +1 -1
  34. package/dist/component/http.d.ts.map +1 -1
  35. package/dist/component/http.js +48 -27
  36. package/dist/component/http.js.map +1 -1
  37. package/dist/component/ingest.d.ts.map +1 -1
  38. package/dist/component/ingest.js +223 -304
  39. package/dist/component/ingest.js.map +1 -1
  40. package/dist/component/localTime.d.ts +25 -0
  41. package/dist/component/localTime.d.ts.map +1 -0
  42. package/dist/component/localTime.js +126 -0
  43. package/dist/component/localTime.js.map +1 -0
  44. package/dist/component/reports.d.ts +44 -13
  45. package/dist/component/reports.d.ts.map +1 -1
  46. package/dist/component/reports.js +193 -38
  47. package/dist/component/reports.js.map +1 -1
  48. package/dist/component/retention.js +26 -8
  49. package/dist/component/retention.js.map +1 -1
  50. package/dist/component/rollupStore.d.ts +320 -0
  51. package/dist/component/rollupStore.d.ts.map +1 -0
  52. package/dist/component/rollupStore.js +596 -0
  53. package/dist/component/rollupStore.js.map +1 -0
  54. package/dist/component/rollups.d.ts +20 -0
  55. package/dist/component/rollups.d.ts.map +1 -0
  56. package/dist/component/rollups.js +73 -0
  57. package/dist/component/rollups.js.map +1 -0
  58. package/dist/component/sanitize.d.ts +5 -0
  59. package/dist/component/sanitize.d.ts.map +1 -1
  60. package/dist/component/sanitize.js +15 -0
  61. package/dist/component/sanitize.js.map +1 -1
  62. package/dist/component/schema.d.ts +345 -11
  63. package/dist/component/schema.js +25 -0
  64. package/dist/component/schema.js.map +1 -1
  65. package/dist/component/sites.d.ts +12 -0
  66. package/dist/component/sites.d.ts.map +1 -1
  67. package/dist/component/sites.js +30 -0
  68. package/dist/component/sites.js.map +1 -1
  69. package/dist/component/validators.d.ts +34 -13
  70. package/dist/component/validators.d.ts.map +1 -1
  71. package/dist/component/validators.js +23 -2
  72. package/dist/component/validators.js.map +1 -1
  73. package/dist/tracker/generated.d.ts +3 -0
  74. package/dist/tracker/generated.d.ts.map +1 -1
  75. package/dist/tracker/generated.js +3 -0
  76. package/dist/tracker/generated.js.map +1 -1
  77. package/docs/federation-setup.md +104 -27
  78. package/docs/federation.md +168 -31
  79. package/docs/upgrading.md +158 -21
  80. package/llms.txt +8 -6
  81. package/package.json +2 -1
  82. package/src/component/_generated/api.ts +6 -0
  83. package/src/component/_generated/component.ts +47 -12
  84. package/src/component/constants.ts +6 -0
  85. package/src/component/coverage.ts +46 -6
  86. package/src/component/geo.ts +82 -28
  87. package/src/component/http.ts +61 -26
  88. package/src/component/ingest.ts +327 -439
  89. package/src/component/localTime.ts +167 -0
  90. package/src/component/reports.ts +261 -39
  91. package/src/component/retention.ts +25 -7
  92. package/src/component/rollupStore.ts +799 -0
  93. package/src/component/rollups.ts +82 -0
  94. package/src/component/sanitize.ts +14 -0
  95. package/src/component/schema.ts +35 -0
  96. package/src/component/sites.ts +32 -0
  97. package/src/component/validators.ts +27 -1
  98. package/src/tracker/generated.ts +12 -0
package/docs/upgrading.md CHANGED
@@ -1,23 +1,28 @@
1
1
  # Upgrading Amage Rastro
2
2
 
3
- Amage Rastro is pre-stable. Pin exact versions, read the changelog, and rehearse
4
- upgrades against a non-production snapshot before changing a maintained
5
- deployment.
3
+ Pin exact versions, read the changelog, and rehearse upgrades against a
4
+ non-production snapshot before changing a maintained deployment. `0.1.0` is the
5
+ first release without a prerelease tag; the alphas below it stay documented for
6
+ hosts still on them.
6
7
 
7
8
  ## Release matrix
8
9
 
9
- | Current state | Required path | Success condition |
10
- | ----------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
11
- | Fresh deployment | Install the intended release directly | Component deploys and `/rastro/health` succeeds |
12
- | `0.1.0-alpha.0` with maintained data | Install `0.1.0-alpha.1` first and wait for staged visitor-index backfills | Every staged index reports complete before installing `alpha.2` |
13
- | `0.1.0-alpha.1` | Wait for staged visitor-index backfills, then install `alpha.2` | Backfills complete before active readers deploy |
14
- | `0.1.0-alpha.2` with legacy `events` rows | Stay on `alpha.2` and run/resume migration | Durable success plus empty source |
15
- | `0.1.0-alpha.3` fresh/canonical deployment | Rehearse the next alpha normally | Schema, reports, tracker, and retention checks pass |
16
- | `0.1.0-alpha.3` through `alpha.5` with canonical data | Install `0.1.0-alpha.6` directly; Web Vitals tables are additive | Schema deploys and `/rastro/health` reports `webVitals: "optIn"` |
17
- | `0.1.0-alpha.6` | Install `0.1.0-alpha.7` directly; no schema change | `GET /rastro/tracker.js` contains `sentAt` |
18
- | `0.1.0-alpha.7` | Install `0.1.0-alpha.8` directly; site map tables are additive; re-export `siteMap` | `/rastro/health` reports `siteMap: "dailyRollup"` |
19
- | `0.1.0-alpha.8` | Install `0.1.0-alpha.9` directly; no schema change; the tracker changes | `/rastro/health` reports `liveDepartures: "leaveBeacon"` |
20
- | Any release whose packed declarations lack federation | Stop and request an eligible exact registry release | Packed declarations contain the federation exports |
10
+ | Current state | Required path | Success condition |
11
+ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
12
+ | Fresh deployment | Install the intended release directly | Component deploys and `/rastro/health` succeeds |
13
+ | `0.1.0-alpha.0` with maintained data | Install `0.1.0-alpha.1` first and wait for staged visitor-index backfills | Every staged index reports complete before installing `alpha.2` |
14
+ | `0.1.0-alpha.1` | Wait for staged visitor-index backfills, then install `alpha.2` | Backfills complete before active readers deploy |
15
+ | `0.1.0-alpha.2` with legacy `events` rows | Stay on `alpha.2` and run/resume migration | Durable success plus empty source |
16
+ | `0.1.0-alpha.3` fresh/canonical deployment | Rehearse the next alpha normally | Schema, reports, tracker, and retention checks pass |
17
+ | `0.1.0-alpha.3` through `alpha.5` with canonical data | Install `0.1.0-alpha.6` directly; Web Vitals tables are additive | Schema deploys and `/rastro/health` reports `webVitals: "optIn"` |
18
+ | `0.1.0-alpha.6` | Install `0.1.0-alpha.7` directly; no schema change | `GET /rastro/tracker.js` contains `sentAt` |
19
+ | `0.1.0-alpha.7` | Install `0.1.0-alpha.8` directly; site map tables are additive; re-export `siteMap` | `/rastro/health` reports `siteMap: "dailyRollup"` |
20
+ | `0.1.0-alpha.8` | Install `0.1.0-alpha.10` directly; no schema change; the tracker changes | `/rastro/health` reports `liveDepartures: "leaveBeacon"` |
21
+ | `0.1.0-alpha.9` | Install `0.1.0-alpha.10` directly; `alpha.9` is deprecated it rejected its own leave beacons | The final page-close `POST /rastro/events` returns 200 |
22
+ | `0.1.0-alpha.10` | Install `0.1.0-alpha.12` directly; new tables and fields are additive; re-export the configure functions to opt in | `/rastro/health` reports `tracker.version: "0.1.0-alpha.12"` |
23
+ | `0.1.0-alpha.11` | Install `0.1.0-alpha.12` directly; same code, the package now ships `src/tracker/generated.ts` | A `convex/` test importing `@iann29/rastro/test` typechecks on push |
24
+ | `0.1.0-alpha.12` | Install `0.1.0` directly; every schema change is an optional field or a new table; add `analytics:public` to a grant only to allow public links | `/rastro/health` reports `tracker.version: "0.1.0"` |
25
+ | Any release whose packed declarations lack federation | Stop and request an eligible exact registry release | Packed declarations contain the federation exports |
21
26
 
22
27
  Do not upgrade a populated legacy deployment directly to a release that removes
23
28
  the `events` table. Schema acceptance alone does not prove that telemetry was
@@ -171,7 +176,14 @@ Acceptance for this upgrade: `/rastro/health` reports
171
176
  between two pages, `siteMap` returns both routes and the transition between
172
177
  them.
173
178
 
174
- ## Realtime presence and persistent identity (alpha.9)
179
+ ## Realtime presence and persistent identity (alpha.9, fixed in alpha.10)
180
+
181
+ **Install `0.1.0-alpha.10`, never `alpha.9`.** As published, `alpha.9`'s HTTP
182
+ action rejected any batch containing the new `leave` event; the final pagehide
183
+ flush carries the whole remaining queue in one body, so outbound clicks and
184
+ pending events were lost on every page close while departures silently fell back
185
+ to the safety net. `alpha.9` is deprecated on npm; `alpha.10` is the same
186
+ release with the event accepted at the boundary.
175
187
 
176
188
  `0.1.0-alpha.9` changes no schema, but it changes the served tracker twice over,
177
189
  so the cache window is the whole upgrade story.
@@ -195,16 +207,135 @@ contract describes what is stored (origin-scoped, never a cookie, gone when the
195
207
  visitor clears site data) and the consent note to reconcile with local
196
208
  requirements.
197
209
 
198
- **The tracker is cached** (`max-age=300, stale-while-revalidate=86400`): for up
210
+ **The tracker is cached** (`max-age=300, stale-while-revalidate=86400` on hosts
211
+ through `alpha.10`; the stale window shrinks to an hour from `alpha.11`): for up
199
212
  to a day after the deploy, returning visitors and already-open tabs run the old
200
213
  tracker — per-tab identity, no leave beacon. They degrade cleanly to the
201
214
  90-second safety net and to per-tab counting until the cache turns over.
202
- Validate in a private window, not in a tab that was open during the deploy.
215
+ Validate in a private window, not in a tab that was open during the deploy; see
216
+ [Tracker cache](#tracker-cache).
203
217
 
204
218
  Acceptance for this upgrade: `/rastro/health` reports
205
219
  `features.liveDepartures: "leaveBeacon"`, `GET /rastro/tracker.js` contains
206
220
  `leave` and `_rv`, two tabs on the site appear as one active visitor, and
207
- closing both removes the visitor from **Ao vivo** within seconds.
221
+ closing both removes the visitor from **Ao vivo** within seconds — and the final
222
+ `/rastro/events` beacon on the page close returns 200 in the network log,
223
+ not 400.
224
+
225
+ ## Calendar days in the site's timezone (alpha.11)
226
+
227
+ `0.1.0-alpha.11` buckets each event once more, by the calendar day of the site's
228
+ timezone (`aggregateBuckets.granularity: "localDay"`), for sites whose timezone
229
+ ever leaves UTC. Nothing is backfilled: coverage starts at the first ingest
230
+ after the upgrade plus the rollup safety delay, stamped as
231
+ `analyticsCoverage.localDaySince`, and restarts when a site's timezone changes.
232
+ Until the stamp covers a dashboard's range and the one before it, the dashboard
233
+ keeps reading whole UTC days, so the switch is silent — the trend chart caption
234
+ says which days a view follows. The `aggregateDay` retention kind now purges the
235
+ local-day buckets alongside the UTC ones. The connector manifest advertises the
236
+ optional `localDays` capability; no host edit is required, since the capability
237
+ adds no function.
238
+
239
+ ## Configure scope (alpha.11)
240
+
241
+ `0.1.0-alpha.11` changes no component schema. It adds the optional `configure`
242
+ capability to the federated surface: fourteen functions (`siteSettings`,
243
+ `updateSite`, `listGoals`, `upsertGoal`, `removeGoal`, `listFunnels`,
244
+ `upsertFunnel`, `removeFunnel`, `listAffiliates`, `upsertAffiliate`,
245
+ `removeAffiliate`, `retentionStatus`, `setRetentionPolicy`,
246
+ `disableRetentionPolicy`) that let the dashboard's organization owners and
247
+ admins configure a granted site, and the stable error
248
+ `FEDERATION_CONFIGURE_FORBIDDEN`. Nothing runs until the host opts in.
249
+
250
+ Two host edits are involved, both in the federation module and its grant:
251
+
252
+ 1. `convex/rastroFederation.ts` re-exports the new functions from `federated`
253
+ (the runbook shows the complete list). As with `siteMap` in `alpha.8`, the
254
+ connector manifest is static package metadata: it advertises `configure`
255
+ whether or not the module exports the functions, so a host that later grants
256
+ the scope without this export leaves every form in the dashboard failing with
257
+ a missing-function error.
258
+ 2. The `rastroFederationGrants` table gains an optional
259
+ `permissions: v.optional(v.array(v.string()))`, and `provisionConnection`
260
+ accepts `permissions` validated with `federatedAnalyticsPermissionValidator`.
261
+ A grant without it stays read only; the resolver passes `grant.permissions`
262
+ through as `FederatedAnalyticsConnection.permissions`.
263
+
264
+ Reads are unchanged, and a host that upgrades the package without either edit
265
+ keeps working as a read-only connection. `connectionStatus` now returns the
266
+ effective `permissions`; a dashboard talking to an older host treats the absent
267
+ field as read only. After deploying, open **Conexões** and click **Verificar
268
+ novamente** so the stored capability list picks up `configure` and the card
269
+ reports "Somente leitura" or "Leitura e configuração".
270
+
271
+ Acceptance for this upgrade: `rastroFederation:manifest` lists the `configure`
272
+ capability; with a read-only grant, `upsertGoal` under a configure-carrying
273
+ identity fails with `FEDERATION_CONFIGURE_FORBIDDEN`; after reprovisioning with
274
+ `"permissions":["analytics:configure"]`, an owner or admin creates a goal from
275
+ **Metas** and `listGoals` returns it, while a member sees only the generated
276
+ code.
277
+
278
+ ## Leaving alpha (0.1.0)
279
+
280
+ `0.1.0` is the first release without a prerelease tag. An `alpha.12` host
281
+ installs it directly: `sessions` gains an optional `newVisitor`, `liveSessions`
282
+ gains the optional intent fields (`entryPath`, `returning`, `utmCampaign`,
283
+ `affiliateSlug`, `conversionCount`, `funnel`), `aggregateBuckets` slots accept
284
+ the `medium` and `outbound` dimension types, and `analyticsCoverage` gains
285
+ `dimensionsSince`, stamped at each site's first ingest on this version plus the
286
+ rollup delay. No federated function changed its arguments and no re-export is
287
+ needed: `overview` gains `topMediums`, `topOutbound` and
288
+ `metadata.dimensionsSince`, `liveVisitors` gains the intent fields, and
289
+ `listSessions` gains `newVisitor`; a dashboard reading an older host hides what
290
+ it does not receive.
291
+
292
+ What a host may choose to do:
293
+
294
+ - **Public links.** A grant that lists `analytics:public` allows the control
295
+ plane to mint tokens for `#/p/<slug>` pages. Such a token carries
296
+ `rastro_permissions: ["analytics:public"]` alone and the bounded
297
+ `rastro_site_ids` claim, may run only `manifest`, `connectionStatus`,
298
+ `listSites`, `overview`, `liveVisitors` and `dataCoverage`, sees a redacted
299
+ `overview` (no revenue, conversions, goals, campaigns or custom events) and a
300
+ `liveVisitors` whose visitor id is the session id. A grant without the
301
+ permission refuses every public call with `FEDERATION_PUBLIC_FORBIDDEN`; an
302
+ `alpha.12` host refuses the token with `FEDERATION_INVALID_CLAIMS`. After
303
+ changing a grant, click "Verificar novamente" in Conexões so the control plane
304
+ records the permission before it lets a member create a link.
305
+ - **Heat rules.** `visitorHeat` is exported for hosts that want the dashboard's
306
+ hot/warm/cold score on their own side; it is pure and needs no deploy.
307
+
308
+ The dashboard's "Meios" and "Destinos externos" cards, the "Quentes agora"
309
+ figure, the "Reprise" in the journey sheet and the `#/telao` board appear as
310
+ soon as the host runs `0.1.0`; a range that starts before `dimensionsSince`
311
+ captions the two new cards with the date the count began.
312
+
313
+ Control plane deployments: `rastroPublicLinks` is a new table and
314
+ `publicLinks.*` are new functions; deploy `control-plane/convex` before or with
315
+ the dashboard build that links to public pages.
316
+
317
+ ## Tracker cache
318
+
319
+ `GET /rastro/tracker.js` and `GET /rastro/vitals.js` are served with a
320
+ five-minute fresh window followed by a stale-while-revalidate window: up to a
321
+ day on hosts through `alpha.10`, up to an hour from `alpha.11`. Two consequences
322
+ apply to every release that changes the served script:
323
+
324
+ - A tab that was open during the deploy never reloads the script. It keeps the
325
+ previous tracker until the page itself is reloaded.
326
+ - A returning visitor's first page load inside the stale window runs the
327
+ previous tracker while the browser revalidates in the background; the next
328
+ load runs the new one.
329
+
330
+ So a request that still lacks a field the new tracker sends (`sentAt`, `leave`,
331
+ `_rv`) during that window is expected, not evidence of a failed deploy — the
332
+ alpha.5 rehearsal on a real host raised exactly that false alarm. Validate an
333
+ upgrade in a fresh browser context (a private window, or a tab opened after the
334
+ deploy), and inspect `GET /rastro/tracker.js` directly when the served script is
335
+ in doubt. From `alpha.11` both scripts carry a strong `ETag` and answer
336
+ `If-None-Match` with `304`; compare the `ETag` your browser received with
337
+ `tracker.hash` from `/rastro/health`, which the dashboard's **Instalação** view
338
+ also shows.
208
339
 
209
340
  ## Federation releases
210
341
 
@@ -231,14 +362,20 @@ control-plane issuer.
231
362
  - DEV or a snapshot-seeded sandbox accepted the schema.
232
363
  - Legacy migration status is complete when applicable.
233
364
  - Staged indexes completed before their readers were activated.
234
- - `/rastro/health` succeeds on the HTTP-actions URL.
235
- - A real tracker request is accepted from an allowed origin.
365
+ - `/rastro/health` succeeds on the HTTP-actions URL and `tracker.version` names
366
+ the release just installed.
367
+ - A real tracker request is accepted from an allowed origin, observed in a
368
+ browser context opened after the deploy (see [Tracker cache](#tracker-cache)).
236
369
  - When the host opts into Web Vitals: `/rastro/vitals.js` serves and
237
370
  `vitalsReport` reflects a real page load.
238
371
  - On `alpha.8` or later: the federation module re-exports `siteMap` and, after a
239
372
  real two-page visit, `siteMap` returns both routes and their transition.
240
373
  - On `alpha.9` or later: closing the site's last tab removes the visitor from
241
374
  the live view within seconds, and two tabs count as one visitor.
375
+ - On `alpha.11` or later: the federation module re-exports the configure
376
+ functions, a read-only grant refuses `upsertGoal` with
377
+ `FEDERATION_CONFIGURE_FORBIDDEN`, and a grant listing `analytics:configure`
378
+ lets an owner or admin create a goal from the dashboard.
242
379
  - Protected overview, journey, goal, funnel, and affiliate reports used by the
243
380
  host still run.
244
381
  - Trusted conversion idempotency and currency checks still pass.
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Amage Rastro
2
2
 
3
- > Privacy-first, real-time web analytics for Convex. Telemetry stays in each product's Convex or Synapse deployment; the central dashboard reads only an explicitly granted, read-only report surface.
3
+ > Privacy-first, real-time web analytics for Convex. Telemetry stays in each product's Convex or Synapse deployment; the central dashboard reads only an explicitly granted report surface, and configures a site only when its host's grant opts into the configure scope.
4
4
 
5
5
  Canonical documentation: https://www.amagerastro.com/docs/
6
6
  Machine manifest: https://www.amagerastro.com/agent/manifest.json
@@ -30,7 +30,7 @@ Do not infer federation availability from this website, a dist-tag, or a source
30
30
  - Resolve an indexed, authoritative local grant on every federated query.
31
31
  - Grant one through ten explicit site IDs. Never grant by owner-wide enumeration.
32
32
  - Use the canonical module name `convex/rastroFederation.ts`.
33
- - Expose only the manifest, connection status, site summaries, reports, journeys, conversions, affiliates, funnels, goals, web vitals, and coverage listed in the manifest.
33
+ - Expose only the manifest, connection status, site summaries, reports, journeys, conversions, affiliates, funnels, goals, web vitals, coverage, and the configure functions listed in the manifest; the configure functions run only when the token and the local grant both carry `analytics:configure`. A public link's token carries `analytics:public` alone and reaches only the manifest, connection status, site summaries, a redacted overview, redacted live visitors, and coverage, and only when the local grant lists `analytics:public`.
34
34
  - Use the functions URL as JWT audience and the HTTP-actions URL for tracker ingestion.
35
35
  - Rehearse on an explicit development target. Production changes require fresh operator consent.
36
36
  - Validate manifest, authenticated connection status, redacted site list, real ingestion, reactive live data, and local-first revocation.
@@ -57,15 +57,17 @@ Do not infer federation availability from this website, a dist-tag, or a source
57
57
  - Maximum live visitors: 500.
58
58
  - Live window: a `leave` beacon on pagehide retires a visitor about ten seconds after they leave, and a bfcache return revives them; the safety net without a goodbye is 90 seconds since the last event or heartbeat. `POST /rastro/events` re-anchors a batch whose `sentAt` is more than 30 seconds from server time, so a wrong device clock decides neither liveness nor hour buckets.
59
59
  - Maximum journey events: 500.
60
- - Token lifetime: 10 minutes, RS256, permission `analytics:read`.
60
+ - Token lifetime: 10 minutes, RS256. Every member's token carries `analytics:read`; owners and admins also carry `analytics:configure`, which the host honors only when the local grant lists it (`configure` is an optional capability that hosts before `alpha.11` do not advertise). A public link's token carries `analytics:public` alone, plus `rastro_site_ids` naming the link's sites; hosts before `0.1.0` reject it.
61
61
 
62
62
  ## Read semantics
63
63
 
64
64
  - Times are inclusive Unix epoch milliseconds in UTC.
65
- - Goal, funnel, and affiliate reports include read-only definitions plus aggregates; configuration writes are not exposed.
65
+ - Goal, funnel, and affiliate reports include read-only definitions plus aggregates. Configuration writes (goals, funnels, affiliates, a site's name/domains/timezone, retention) exist only in the configure scope and fail with `FEDERATION_CONFIGURE_FORBIDDEN` for a read-only token or grant; `listSites` never returns domains, only `siteSettings` does, to a connection allowed to change them.
66
+ - A public token (`analytics:public`) gets `overview` with zero `conversions` and `revenueCents` and empty `topCampaigns`, `topMediums`, `topEvents`, and `topGoals`, `liveVisitors` with `visitorId` equal to `sessionId` and without `entryPath`, `returning`, `utmCampaign`, `affiliateSlug`, `conversionCount`, or `funnel`, and `FEDERATION_PUBLIC_FORBIDDEN` for sessions, journeys, conversions, goals, funnels, affiliates, vitals, the site map, and the configure scope.
66
67
  - Session and visitor journeys include bounded custom properties. Hosts must never place personal data or secrets in event properties.
67
68
  - Audit retained routes, identity strategy, sessions, live location, campaigns, affiliates, conversions, custom properties, retention, and intended organization membership before granting federation; stop if their sensitivity or audience cannot be established.
68
- - Overview ranges require complete UTC hour or day buckets. Manual pagination honors `numItems` and `cursor` only.
69
+ - Overview ranges require complete UTC hours, complete UTC days, or (hosts from `alpha.11`, optional capability `localDays`) complete calendar days in the timezone every requested site shares; `metadata.range.timezone` names the zone the answer follows. Manual pagination honors `numItems` and `cursor` only.
70
+ - Right after an accepted request, `liveVisitors` and `sessionJourney` reflect it; `listSessions` and the bucketed reports can trail it by seconds, so an empty result from those is not proof of failed ingestion.
69
71
  - `dataCoverage` echoes the authorized site ID but returns no event, visitor, or session IDs.
70
72
  - `vitalsReport` returns p75, averages, and exact good/needs-improvement/poor counts per metric, page, and device — never individual measurements.
71
- - `siteMap` returns per-route totals (pageviews, estimated visitors, entries, derived exits and bounces, time on page) and route-to-route transition counts, with numeric, UUID, and long hexadecimal segments collapsed to `:id` — never sessions, visitor IDs, or query strings. `liveVisitors` adds `previousPath` and `movedAt` when a visitor moves between routes.
73
+ - `siteMap` returns per-route totals (pageviews, estimated visitors, entries, derived exits and bounces, time on page) and route-to-route transition counts, with numeric, UUID, and long hexadecimal segments collapsed to `:id` — never sessions, visitor IDs, or query strings. `liveVisitors` adds `previousPath` and `movedAt` when a visitor moves between routes, and since 0.1.0 the intent facts of the session — `entryPath`, `returning`, `utmCampaign`, `affiliateSlug`, `conversionCount`, and the furthest `funnel` step — which `visitorHeat` from the package turns into the dashboard's hot/warm/cold score.
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "bugs": {
7
7
  "url": "https://github.com/amageweb/amage-rastro/issues"
8
8
  },
9
- "version": "0.1.0-alpha.9",
9
+ "version": "0.1.0",
10
10
  "license": "Apache-2.0",
11
11
  "publishConfig": {
12
12
  "access": "public"
@@ -58,6 +58,7 @@
58
58
  "src/test.ts",
59
59
  "src/component/**/*.ts",
60
60
  "!src/component/**/*.test.ts",
61
+ "src/tracker/generated.ts",
61
62
  "scripts/benchmark-ingest.mjs",
62
63
  "scripts/generate-federation-keys.mjs",
63
64
  "agent/manifest.json",
@@ -23,8 +23,11 @@ import type * as http from "../http.js";
23
23
  import type * as identity from "../identity.js";
24
24
  import type * as ingest from "../ingest.js";
25
25
  import type * as live from "../live.js";
26
+ import type * as localTime from "../localTime.js";
26
27
  import type * as reports from "../reports.js";
27
28
  import type * as retention from "../retention.js";
29
+ import type * as rollupStore from "../rollupStore.js";
30
+ import type * as rollups from "../rollups.js";
28
31
  import type * as sanitize from "../sanitize.js";
29
32
  import type * as sites from "../sites.js";
30
33
  import type * as useragent from "../useragent.js";
@@ -55,8 +58,11 @@ const fullApi: ApiFromModules<{
55
58
  identity: typeof identity;
56
59
  ingest: typeof ingest;
57
60
  live: typeof live;
61
+ localTime: typeof localTime;
58
62
  reports: typeof reports;
59
63
  retention: typeof retention;
64
+ rollupStore: typeof rollupStore;
65
+ rollups: typeof rollups;
60
66
  sanitize: typeof sanitize;
61
67
  sites: typeof sites;
62
68
  useragent: typeof useragent;
@@ -273,7 +273,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
273
273
  range: {
274
274
  boundary: "inclusive";
275
275
  from: number;
276
- timezone: "UTC";
276
+ timezone: string;
277
277
  to: number;
278
278
  };
279
279
  uniqueVisitors: {
@@ -301,6 +301,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
301
301
  dataset:
302
302
  | "overviewHour"
303
303
  | "overviewDay"
304
+ | "overviewLocalDay"
304
305
  | "events"
305
306
  | "sessions"
306
307
  | "conversions"
@@ -339,7 +340,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
339
340
  range: {
340
341
  boundary: "inclusive";
341
342
  from: number;
342
- timezone: "UTC";
343
+ timezone: string;
343
344
  to: number;
344
345
  };
345
346
  };
@@ -392,7 +393,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
392
393
  range: {
393
394
  boundary: "inclusive";
394
395
  from: number;
395
- timezone: "UTC";
396
+ timezone: string;
396
397
  to: number;
397
398
  };
398
399
  };
@@ -482,6 +483,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
482
483
  lastSeenAt: number;
483
484
  latitude?: number;
484
485
  longitude?: number;
486
+ newVisitor?: boolean;
485
487
  os: string;
486
488
  pageviewCount: number;
487
489
  referrer?: string;
@@ -508,12 +510,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
508
510
  Array<{
509
511
  _creationTime: number;
510
512
  _id: string;
513
+ affiliateSlug?: string;
511
514
  browser: string;
512
515
  city?: string;
516
+ conversionCount?: number;
513
517
  country?: string;
514
518
  device: string;
519
+ entryPath?: string;
515
520
  eventCount: number;
516
521
  expiresAt: number;
522
+ funnel?: { funnelId: string; step: number; steps: number };
517
523
  lastSeenAt: number;
518
524
  latitude?: number;
519
525
  longitude?: number;
@@ -522,10 +528,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
522
528
  pageviewCount: number;
523
529
  path: string;
524
530
  previousPath?: string;
531
+ returning?: boolean;
525
532
  sessionId: string;
526
533
  siteId: string;
527
534
  source: string;
528
535
  startedAt: number;
536
+ utmCampaign?: string;
529
537
  visitorId: string;
530
538
  }>,
531
539
  Name
@@ -545,14 +553,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
545
553
  interval: "hour" | "day";
546
554
  metadata: {
547
555
  breakdowns: {
548
- topBrowsers: "eventVolume";
549
- topCampaigns: "eventVolume";
550
- topCountries: "eventVolume";
551
- topDevices: "eventVolume";
556
+ topBrowsers: "pageviews" | "sessions" | "eventVolume";
557
+ topCampaigns: "pageviews" | "sessions" | "eventVolume";
558
+ topCountries: "pageviews" | "sessions" | "eventVolume";
559
+ topDevices: "pageviews" | "sessions" | "eventVolume";
552
560
  topEvents: "eventVolume";
553
561
  topGoals: "goalCompletionVolume";
554
- topPages: "eventVolume";
555
- topSources: "eventVolume";
562
+ topMediums: "pageviews" | "sessions" | "eventVolume";
563
+ topOutbound: "eventVolume";
564
+ topPages: "pageviews" | "sessions" | "eventVolume";
565
+ topSources: "pageviews" | "sessions" | "eventVolume";
556
566
  };
557
567
  coverage: {
558
568
  availableFrom: number | null;
@@ -561,6 +571,8 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
561
571
  retainedBefore: number | null;
562
572
  state: "complete" | "partial" | "retained" | "unavailable";
563
573
  };
574
+ dimensionsSince: number | null;
575
+ engagement: "counted" | "unavailable";
564
576
  features: {
565
577
  botDetection: "userAgent";
566
578
  errorInsights: "unsupported";
@@ -570,7 +582,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
570
582
  range: {
571
583
  boundary: "inclusive";
572
584
  from: number;
573
- timezone: "UTC";
585
+ timezone: string;
574
586
  to: number;
575
587
  };
576
588
  visitors: {
@@ -583,9 +595,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
583
595
  };
584
596
  timeSeries: Array<{
585
597
  clicks: number;
598
+ continuedSessions: number;
586
599
  conversions: number;
587
600
  customEvents: number;
601
+ durationMs: number;
588
602
  events: number;
603
+ newVisitors: number;
589
604
  outboundClicks: number;
590
605
  pageviews: number;
591
606
  revenueCents: number;
@@ -624,6 +639,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
624
639
  revenueCents: number;
625
640
  value: string;
626
641
  }>;
642
+ topMediums: Array<{
643
+ count: number;
644
+ revenueCents: number;
645
+ value: string;
646
+ }>;
647
+ topOutbound: Array<{
648
+ count: number;
649
+ revenueCents: number;
650
+ value: string;
651
+ }>;
627
652
  topPages: Array<{
628
653
  count: number;
629
654
  revenueCents: number;
@@ -636,9 +661,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
636
661
  }>;
637
662
  totals: {
638
663
  clicks: number;
664
+ continuedSessions: number;
639
665
  conversions: number;
640
666
  customEvents: number;
667
+ durationMs: number;
641
668
  events: number;
669
+ newVisitors: number;
642
670
  outboundClicks: number;
643
671
  pageviews: number;
644
672
  revenueCents: number;
@@ -731,7 +759,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
731
759
  range: {
732
760
  boundary: "inclusive";
733
761
  from: number;
734
- timezone: "UTC";
762
+ timezone: string;
735
763
  to: number;
736
764
  };
737
765
  visitors: { basis: "estimatedUnique"; exact: boolean };
@@ -820,7 +848,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
820
848
  range: {
821
849
  boundary: "inclusive";
822
850
  from: number;
823
- timezone: "UTC";
851
+ timezone: string;
824
852
  to: number;
825
853
  };
826
854
  };
@@ -1040,6 +1068,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1040
1068
  }>,
1041
1069
  Name
1042
1070
  >;
1071
+ localDays: FunctionReference<
1072
+ "query",
1073
+ "internal",
1074
+ { siteId: string },
1075
+ { since: number; timezone: string } | null,
1076
+ Name
1077
+ >;
1043
1078
  update: FunctionReference<
1044
1079
  "mutation",
1045
1080
  "internal",
@@ -38,6 +38,12 @@ export const FEATURE_REPORT_DOCUMENT_RESERVE = 500;
38
38
  export const REPORT_ROLLUP_SHARDS = 2;
39
39
  export const ROLLUP_SAFETY_DELAY_MS = 5 * 60_000;
40
40
  export const RETENTION_LEASE_MS = 60_000;
41
+ // Report rollups are folded by one leased job per site (see rollupStore.ts).
42
+ // A run takes at most this many queued batches, stops early once the merged
43
+ // deltas would touch this many rows, and holds its lease for this long.
44
+ export const ROLLUP_FOLD_BATCH_DOCS = 200;
45
+ export const ROLLUP_FOLD_MAX_ROWS = 1_500;
46
+ export const ROLLUP_FOLD_LEASE_MS = 60_000;
41
47
  // Shared Web Vitals histogram edges. Every Google rating threshold (LCP
42
48
  // 2500/4000, CLS×1000 100/250, INP 200/500, FCP 1800/3000, TTFB 800/1800) is
43
49
  // an edge, so rating counts stay exact while one layout serves all metrics.
@@ -13,24 +13,64 @@ export async function ensureAnalyticsControl(
13
13
  .withIndex("by_siteId", (range) => range.eq("siteId", siteId))
14
14
  .unique();
15
15
  if (existing) {
16
- if (existing.siteMapSince !== undefined) return existing;
17
- // A site that predates the site map starts its map coverage at the first
18
- // ingest after the upgrade; the same safety delay applies as for rollups.
19
- const siteMapSince = now + ROLLUP_SAFETY_DELAY_MS;
16
+ // A site that predates a feature starts that feature's coverage at the
17
+ // first ingest after the upgrade, with the same safety delay as rollups:
18
+ // the site map rows, and the alpha.11 counters (pageviews and sessions
19
+ // per dimension slot; duration, continued sessions and new visitors per
20
+ // bucket), and the 0.1.0 dimensions (utm_medium and outbound hosts).
21
+ const stamps: {
22
+ siteMapSince?: number;
23
+ countersSince?: number;
24
+ localDaySince?: number;
25
+ dimensionsSince?: number;
26
+ } = {};
27
+ if (existing.siteMapSince === undefined) {
28
+ stamps.siteMapSince = now + ROLLUP_SAFETY_DELAY_MS;
29
+ }
30
+ if (existing.countersSince === undefined) {
31
+ stamps.countersSince = now + ROLLUP_SAFETY_DELAY_MS;
32
+ }
33
+ if (existing.localDaySince === undefined) {
34
+ stamps.localDaySince = now + ROLLUP_SAFETY_DELAY_MS;
35
+ }
36
+ if (existing.dimensionsSince === undefined) {
37
+ stamps.dimensionsSince = now + ROLLUP_SAFETY_DELAY_MS;
38
+ }
39
+ if (Object.keys(stamps).length === 0) return existing;
20
40
  await ctx.db.patch("analyticsCoverage", existing._id, {
21
- siteMapSince,
41
+ ...stamps,
22
42
  updatedAt: now,
23
43
  });
24
- return { ...existing, siteMapSince, updatedAt: now };
44
+ return { ...existing, ...stamps, updatedAt: now };
25
45
  }
26
46
  const fields = {
27
47
  siteId,
28
48
  generation: 1 as const,
29
49
  rollupCompleteFrom: now + ROLLUP_SAFETY_DELAY_MS,
30
50
  siteMapSince: now + ROLLUP_SAFETY_DELAY_MS,
51
+ countersSince: now + ROLLUP_SAFETY_DELAY_MS,
52
+ localDaySince: now + ROLLUP_SAFETY_DELAY_MS,
53
+ dimensionsSince: now + ROLLUP_SAFETY_DELAY_MS,
31
54
  retained,
32
55
  updatedAt: now,
33
56
  };
34
57
  const _id = await ctx.db.insert("analyticsCoverage", fields);
35
58
  return { ...fields, _id, _creationTime: now };
36
59
  }
60
+
61
+ /**
62
+ * A site's local-day buckets are computed in its timezone at ingest, so a
63
+ * changed timezone restarts their coverage: rows before the change belong to
64
+ * the old zone's days and must not answer for the new one.
65
+ */
66
+ export async function resetLocalDayCoverage(
67
+ ctx: MutationCtx,
68
+ siteId: Id<"sites">,
69
+ now: number,
70
+ ) {
71
+ const control = await ensureAnalyticsControl(ctx, siteId, now);
72
+ await ctx.db.patch("analyticsCoverage", control._id, {
73
+ localDaySince: now + ROLLUP_SAFETY_DELAY_MS,
74
+ updatedAt: now,
75
+ });
76
+ }