@iann29/rastro 0.1.0-alpha.8 → 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.
- package/README.md +274 -75
- package/agent/integration.md +223 -65
- package/agent/manifest.json +34 -13
- package/agent/manifest.schema.json +53 -11
- package/dist/client/federation.d.ts +196 -11
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +160 -6
- package/dist/client/federation.js.map +1 -1
- package/dist/client/heat.d.ts +37 -0
- package/dist/client/heat.d.ts.map +1 -0
- package/dist/client/heat.js +43 -0
- package/dist/client/heat.js.map +1 -0
- package/dist/client/index.d.ts +448 -105
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +207 -17
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +6 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +53 -16
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +5 -1
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +8 -1
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +9 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +36 -6
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/eventStore.d.ts +1 -1
- package/dist/component/geo.d.ts +2 -0
- package/dist/component/geo.d.ts.map +1 -1
- package/dist/component/geo.js +56 -19
- package/dist/component/geo.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +49 -27
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +252 -305
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/localTime.d.ts +25 -0
- package/dist/component/localTime.d.ts.map +1 -0
- package/dist/component/localTime.js +126 -0
- package/dist/component/localTime.js.map +1 -0
- package/dist/component/reports.d.ts +46 -15
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +193 -38
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.js +26 -8
- package/dist/component/retention.js.map +1 -1
- package/dist/component/rollupStore.d.ts +320 -0
- package/dist/component/rollupStore.d.ts.map +1 -0
- package/dist/component/rollupStore.js +596 -0
- package/dist/component/rollupStore.js.map +1 -0
- package/dist/component/rollups.d.ts +20 -0
- package/dist/component/rollups.d.ts.map +1 -0
- package/dist/component/rollups.js +73 -0
- package/dist/component/rollups.js.map +1 -0
- package/dist/component/sanitize.d.ts +5 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +15 -0
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +349 -15
- package/dist/component/schema.js +25 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts +12 -0
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +30 -0
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +41 -20
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +24 -3
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +7 -4
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +7 -4
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.d.ts +1 -1
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +24 -2
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/federation-setup.md +104 -27
- package/docs/federation.md +168 -31
- package/docs/upgrading.md +191 -16
- package/llms.txt +9 -7
- package/package.json +2 -1
- package/src/component/_generated/api.ts +6 -0
- package/src/component/_generated/component.ts +50 -12
- package/src/component/constants.ts +8 -1
- package/src/component/coverage.ts +46 -6
- package/src/component/geo.ts +82 -28
- package/src/component/http.ts +62 -26
- package/src/component/ingest.ts +378 -440
- package/src/component/localTime.ts +167 -0
- package/src/component/reports.ts +261 -39
- package/src/component/retention.ts +25 -7
- package/src/component/rollupStore.ts +799 -0
- package/src/component/rollups.ts +82 -0
- package/src/component/sanitize.ts +14 -0
- package/src/component/schema.ts +35 -0
- package/src/component/sites.ts +32 -0
- package/src/component/validators.ts +28 -1
- package/src/tracker/generated.ts +12 -0
package/docs/upgrading.md
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
# Upgrading Amage Rastro
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
10
|
-
| ----------------------------------------------------- |
|
|
11
|
-
| Fresh deployment | Install the intended release directly
|
|
12
|
-
| `0.1.0-alpha.0` with maintained data | Install `0.1.0-alpha.1` first and wait for staged visitor-index backfills
|
|
13
|
-
| `0.1.0-alpha.1` | Wait for staged visitor-index backfills, then install `alpha.2`
|
|
14
|
-
| `0.1.0-alpha.2` with legacy `events` rows | Stay on `alpha.2` and run/resume migration
|
|
15
|
-
| `0.1.0-alpha.3` fresh/canonical deployment | Rehearse the next alpha normally
|
|
16
|
-
| `0.1.0-alpha.3` through `alpha.5` with canonical data | Install `0.1.0-alpha.6` directly; Web Vitals tables are additive
|
|
17
|
-
| `0.1.0-alpha.6` | Install `0.1.0-alpha.7` directly; no schema change
|
|
18
|
-
| `0.1.0-alpha.7` | Install `0.1.0-alpha.8` directly; site map tables are additive; re-export `siteMap`
|
|
19
|
-
|
|
|
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 |
|
|
20
26
|
|
|
21
27
|
Do not upgrade a populated legacy deployment directly to a release that removes
|
|
22
28
|
the `events` table. Schema acceptance alone does not prove that telemetry was
|
|
@@ -170,6 +176,167 @@ Acceptance for this upgrade: `/rastro/health` reports
|
|
|
170
176
|
between two pages, `siteMap` returns both routes and the transition between
|
|
171
177
|
them.
|
|
172
178
|
|
|
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.
|
|
187
|
+
|
|
188
|
+
`0.1.0-alpha.9` changes no schema, but it changes the served tracker twice over,
|
|
189
|
+
so the cache window is the whole upgrade story.
|
|
190
|
+
|
|
191
|
+
Live presence becomes realtime: the tracker announces a `leave` on `pagehide`
|
|
192
|
+
and the live session retires about ten seconds later, a back/forward-cache
|
|
193
|
+
return revives it immediately, and the safety net for a browser that never says
|
|
194
|
+
goodbye is 90 seconds (down from five minutes). A leave stores no telemetry and
|
|
195
|
+
never extends presence, so replays are inert.
|
|
196
|
+
|
|
197
|
+
Anonymous identity becomes persistent: the tracker keeps one pseudonymous id per
|
|
198
|
+
browser and site origin in `localStorage` and sends it as `visitorId`, so one
|
|
199
|
+
person in several tabs counts once, returning visitors count once across days,
|
|
200
|
+
and journeys join across sessions. A host-supplied `data-visitor` id still wins
|
|
201
|
+
and never touches the stored id; blocked storage falls back to the per-tab id
|
|
202
|
+
and the salted daily hash exactly as before.
|
|
203
|
+
|
|
204
|
+
**Review the host's privacy disclosures before deploying.** The persistent
|
|
205
|
+
visitor ID is an identifier under most privacy laws; the README's privacy
|
|
206
|
+
contract describes what is stored (origin-scoped, never a cookie, gone when the
|
|
207
|
+
visitor clears site data) and the consent note to reconcile with local
|
|
208
|
+
requirements.
|
|
209
|
+
|
|
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
|
|
212
|
+
to a day after the deploy, returning visitors and already-open tabs run the old
|
|
213
|
+
tracker — per-tab identity, no leave beacon. They degrade cleanly to the
|
|
214
|
+
90-second safety net and to per-tab counting until the cache turns over.
|
|
215
|
+
Validate in a private window, not in a tab that was open during the deploy; see
|
|
216
|
+
[Tracker cache](#tracker-cache).
|
|
217
|
+
|
|
218
|
+
Acceptance for this upgrade: `/rastro/health` reports
|
|
219
|
+
`features.liveDepartures: "leaveBeacon"`, `GET /rastro/tracker.js` contains
|
|
220
|
+
`leave` and `_rv`, two tabs on the site appear as one active visitor, and
|
|
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.
|
|
339
|
+
|
|
173
340
|
## Federation releases
|
|
174
341
|
|
|
175
342
|
Federation is additive to component telemetry but requires host tables,
|
|
@@ -195,12 +362,20 @@ control-plane issuer.
|
|
|
195
362
|
- DEV or a snapshot-seeded sandbox accepted the schema.
|
|
196
363
|
- Legacy migration status is complete when applicable.
|
|
197
364
|
- Staged indexes completed before their readers were activated.
|
|
198
|
-
- `/rastro/health` succeeds on the HTTP-actions URL.
|
|
199
|
-
|
|
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)).
|
|
200
369
|
- When the host opts into Web Vitals: `/rastro/vitals.js` serves and
|
|
201
370
|
`vitalsReport` reflects a real page load.
|
|
202
371
|
- On `alpha.8` or later: the federation module re-exports `siteMap` and, after a
|
|
203
372
|
real two-page visit, `siteMap` returns both routes and their transition.
|
|
373
|
+
- On `alpha.9` or later: closing the site's last tab removes the visitor from
|
|
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.
|
|
204
379
|
- Protected overview, journey, goal, funnel, and affiliate reports used by the
|
|
205
380
|
host still run.
|
|
206
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,
|
|
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
|
|
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.
|
|
@@ -55,17 +55,19 @@ Do not infer federation availability from this website, a dist-tag, or a source
|
|
|
55
55
|
- Vitals report maximum: 90 complete UTC days; `vitals` is an optional capability that hosts before `alpha.6` do not advertise.
|
|
56
56
|
- Site map maximum: 90 complete UTC days, read newest day first within 8,000 route rows; older days it could not read are declared as partial coverage. `siteMap` is an optional capability that hosts before `alpha.8` do not advertise, and its data starts at the first ingest after that upgrade.
|
|
57
57
|
- Maximum live visitors: 500.
|
|
58
|
-
- Live window:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
@@ -207,6 +207,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
207
207
|
| "custom"
|
|
208
208
|
| "conversion"
|
|
209
209
|
| "heartbeat"
|
|
210
|
+
| "leave"
|
|
210
211
|
| "outbound"
|
|
211
212
|
| "vital";
|
|
212
213
|
value?: number;
|
|
@@ -272,7 +273,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
272
273
|
range: {
|
|
273
274
|
boundary: "inclusive";
|
|
274
275
|
from: number;
|
|
275
|
-
timezone:
|
|
276
|
+
timezone: string;
|
|
276
277
|
to: number;
|
|
277
278
|
};
|
|
278
279
|
uniqueVisitors: {
|
|
@@ -300,6 +301,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
300
301
|
dataset:
|
|
301
302
|
| "overviewHour"
|
|
302
303
|
| "overviewDay"
|
|
304
|
+
| "overviewLocalDay"
|
|
303
305
|
| "events"
|
|
304
306
|
| "sessions"
|
|
305
307
|
| "conversions"
|
|
@@ -338,7 +340,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
338
340
|
range: {
|
|
339
341
|
boundary: "inclusive";
|
|
340
342
|
from: number;
|
|
341
|
-
timezone:
|
|
343
|
+
timezone: string;
|
|
342
344
|
to: number;
|
|
343
345
|
};
|
|
344
346
|
};
|
|
@@ -391,7 +393,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
391
393
|
range: {
|
|
392
394
|
boundary: "inclusive";
|
|
393
395
|
from: number;
|
|
394
|
-
timezone:
|
|
396
|
+
timezone: string;
|
|
395
397
|
to: number;
|
|
396
398
|
};
|
|
397
399
|
};
|
|
@@ -481,6 +483,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
481
483
|
lastSeenAt: number;
|
|
482
484
|
latitude?: number;
|
|
483
485
|
longitude?: number;
|
|
486
|
+
newVisitor?: boolean;
|
|
484
487
|
os: string;
|
|
485
488
|
pageviewCount: number;
|
|
486
489
|
referrer?: string;
|
|
@@ -507,12 +510,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
507
510
|
Array<{
|
|
508
511
|
_creationTime: number;
|
|
509
512
|
_id: string;
|
|
513
|
+
affiliateSlug?: string;
|
|
510
514
|
browser: string;
|
|
511
515
|
city?: string;
|
|
516
|
+
conversionCount?: number;
|
|
512
517
|
country?: string;
|
|
513
518
|
device: string;
|
|
519
|
+
entryPath?: string;
|
|
514
520
|
eventCount: number;
|
|
515
521
|
expiresAt: number;
|
|
522
|
+
funnel?: { funnelId: string; step: number; steps: number };
|
|
516
523
|
lastSeenAt: number;
|
|
517
524
|
latitude?: number;
|
|
518
525
|
longitude?: number;
|
|
@@ -521,10 +528,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
521
528
|
pageviewCount: number;
|
|
522
529
|
path: string;
|
|
523
530
|
previousPath?: string;
|
|
531
|
+
returning?: boolean;
|
|
524
532
|
sessionId: string;
|
|
525
533
|
siteId: string;
|
|
526
534
|
source: string;
|
|
527
535
|
startedAt: number;
|
|
536
|
+
utmCampaign?: string;
|
|
528
537
|
visitorId: string;
|
|
529
538
|
}>,
|
|
530
539
|
Name
|
|
@@ -544,14 +553,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
544
553
|
interval: "hour" | "day";
|
|
545
554
|
metadata: {
|
|
546
555
|
breakdowns: {
|
|
547
|
-
topBrowsers: "eventVolume";
|
|
548
|
-
topCampaigns: "eventVolume";
|
|
549
|
-
topCountries: "eventVolume";
|
|
550
|
-
topDevices: "eventVolume";
|
|
556
|
+
topBrowsers: "pageviews" | "sessions" | "eventVolume";
|
|
557
|
+
topCampaigns: "pageviews" | "sessions" | "eventVolume";
|
|
558
|
+
topCountries: "pageviews" | "sessions" | "eventVolume";
|
|
559
|
+
topDevices: "pageviews" | "sessions" | "eventVolume";
|
|
551
560
|
topEvents: "eventVolume";
|
|
552
561
|
topGoals: "goalCompletionVolume";
|
|
553
|
-
|
|
554
|
-
|
|
562
|
+
topMediums: "pageviews" | "sessions" | "eventVolume";
|
|
563
|
+
topOutbound: "eventVolume";
|
|
564
|
+
topPages: "pageviews" | "sessions" | "eventVolume";
|
|
565
|
+
topSources: "pageviews" | "sessions" | "eventVolume";
|
|
555
566
|
};
|
|
556
567
|
coverage: {
|
|
557
568
|
availableFrom: number | null;
|
|
@@ -560,6 +571,8 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
560
571
|
retainedBefore: number | null;
|
|
561
572
|
state: "complete" | "partial" | "retained" | "unavailable";
|
|
562
573
|
};
|
|
574
|
+
dimensionsSince: number | null;
|
|
575
|
+
engagement: "counted" | "unavailable";
|
|
563
576
|
features: {
|
|
564
577
|
botDetection: "userAgent";
|
|
565
578
|
errorInsights: "unsupported";
|
|
@@ -569,7 +582,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
569
582
|
range: {
|
|
570
583
|
boundary: "inclusive";
|
|
571
584
|
from: number;
|
|
572
|
-
timezone:
|
|
585
|
+
timezone: string;
|
|
573
586
|
to: number;
|
|
574
587
|
};
|
|
575
588
|
visitors: {
|
|
@@ -582,9 +595,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
582
595
|
};
|
|
583
596
|
timeSeries: Array<{
|
|
584
597
|
clicks: number;
|
|
598
|
+
continuedSessions: number;
|
|
585
599
|
conversions: number;
|
|
586
600
|
customEvents: number;
|
|
601
|
+
durationMs: number;
|
|
587
602
|
events: number;
|
|
603
|
+
newVisitors: number;
|
|
588
604
|
outboundClicks: number;
|
|
589
605
|
pageviews: number;
|
|
590
606
|
revenueCents: number;
|
|
@@ -623,6 +639,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
623
639
|
revenueCents: number;
|
|
624
640
|
value: string;
|
|
625
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
|
+
}>;
|
|
626
652
|
topPages: Array<{
|
|
627
653
|
count: number;
|
|
628
654
|
revenueCents: number;
|
|
@@ -635,9 +661,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
635
661
|
}>;
|
|
636
662
|
totals: {
|
|
637
663
|
clicks: number;
|
|
664
|
+
continuedSessions: number;
|
|
638
665
|
conversions: number;
|
|
639
666
|
customEvents: number;
|
|
667
|
+
durationMs: number;
|
|
640
668
|
events: number;
|
|
669
|
+
newVisitors: number;
|
|
641
670
|
outboundClicks: number;
|
|
642
671
|
pageviews: number;
|
|
643
672
|
revenueCents: number;
|
|
@@ -697,6 +726,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
697
726
|
| "custom"
|
|
698
727
|
| "conversion"
|
|
699
728
|
| "heartbeat"
|
|
729
|
+
| "leave"
|
|
700
730
|
| "outbound"
|
|
701
731
|
| "vital";
|
|
702
732
|
visitorId: string;
|
|
@@ -729,7 +759,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
729
759
|
range: {
|
|
730
760
|
boundary: "inclusive";
|
|
731
761
|
from: number;
|
|
732
|
-
timezone:
|
|
762
|
+
timezone: string;
|
|
733
763
|
to: number;
|
|
734
764
|
};
|
|
735
765
|
visitors: { basis: "estimatedUnique"; exact: boolean };
|
|
@@ -794,6 +824,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
794
824
|
| "custom"
|
|
795
825
|
| "conversion"
|
|
796
826
|
| "heartbeat"
|
|
827
|
+
| "leave"
|
|
797
828
|
| "outbound"
|
|
798
829
|
| "vital";
|
|
799
830
|
visitorId: string;
|
|
@@ -817,7 +848,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
817
848
|
range: {
|
|
818
849
|
boundary: "inclusive";
|
|
819
850
|
from: number;
|
|
820
|
-
timezone:
|
|
851
|
+
timezone: string;
|
|
821
852
|
to: number;
|
|
822
853
|
};
|
|
823
854
|
};
|
|
@@ -1037,6 +1068,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1037
1068
|
}>,
|
|
1038
1069
|
Name
|
|
1039
1070
|
>;
|
|
1071
|
+
localDays: FunctionReference<
|
|
1072
|
+
"query",
|
|
1073
|
+
"internal",
|
|
1074
|
+
{ siteId: string },
|
|
1075
|
+
{ since: number; timezone: string } | null,
|
|
1076
|
+
Name
|
|
1077
|
+
>;
|
|
1040
1078
|
update: FunctionReference<
|
|
1041
1079
|
"mutation",
|
|
1042
1080
|
"internal",
|
|
@@ -7,7 +7,8 @@ export const MAX_BYTES_PER_SESSION_WINDOW = 256 * 1024;
|
|
|
7
7
|
export const SITE_INGEST_SHARDS = 4_096;
|
|
8
8
|
export const MAX_SITE_EVENTS_PER_SHARD_WINDOW = 300;
|
|
9
9
|
export const MAX_SITE_BYTES_PER_SHARD_WINDOW = 512 * 1024;
|
|
10
|
-
export const LIVE_SESSION_TTL_MS =
|
|
10
|
+
export const LIVE_SESSION_TTL_MS = 90_000;
|
|
11
|
+
export const LIVE_LEAVE_GRACE_MS = 10_000;
|
|
11
12
|
export const LIVE_SWEEP_INTERVAL_MS = 5_000;
|
|
12
13
|
export const CLOCK_SKEW_TOLERANCE_MS = 30_000;
|
|
13
14
|
export const AGGREGATE_SHARDS = 128;
|
|
@@ -37,6 +38,12 @@ export const FEATURE_REPORT_DOCUMENT_RESERVE = 500;
|
|
|
37
38
|
export const REPORT_ROLLUP_SHARDS = 2;
|
|
38
39
|
export const ROLLUP_SAFETY_DELAY_MS = 5 * 60_000;
|
|
39
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;
|
|
40
47
|
// Shared Web Vitals histogram edges. Every Google rating threshold (LCP
|
|
41
48
|
// 2500/4000, CLS×1000 100/250, INP 200/500, FCP 1800/3000, TTFB 800/1800) is
|
|
42
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
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
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
|
-
|
|
41
|
+
...stamps,
|
|
22
42
|
updatedAt: now,
|
|
23
43
|
});
|
|
24
|
-
return { ...existing,
|
|
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
|
+
}
|