@iann29/rastro 0.1.0-alpha.6 → 0.1.0-alpha.8
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 +41 -18
- package/agent/integration.md +15 -7
- package/agent/manifest.json +9 -7
- package/agent/manifest.schema.json +8 -5
- package/dist/client/federation.d.ts +13 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +4 -1
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +412 -10
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +55 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +68 -4
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +6 -1
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +13 -1
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +1 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +13 -2
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +20 -8
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +144 -2
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/reports.d.ts +75 -1
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +275 -3
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts +4 -4
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +12 -2
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts +20 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +45 -1
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +65 -3
- package/dist/component/schema.js +25 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/tracker/generated.d.ts +8 -8
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +8 -8
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.js +3 -1
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker/vitals.js +4 -1
- package/dist/tracker/vitals.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/dist/vitals.min.js +1 -1
- package/docs/federation-setup.md +2 -0
- package/docs/federation.md +4 -0
- package/docs/upgrading.md +89 -8
- package/llms.txt +7 -1
- package/package.json +1 -1
- package/src/component/_generated/component.ts +55 -1
- package/src/component/constants.ts +13 -1
- package/src/component/coverage.ts +12 -1
- package/src/component/http.ts +22 -9
- package/src/component/ingest.ts +227 -0
- package/src/component/reports.ts +364 -2
- package/src/component/retention.ts +16 -1
- package/src/component/sanitize.ts +54 -0
- package/src/component/schema.ts +26 -0
package/docs/federation-setup.md
CHANGED
package/docs/federation.md
CHANGED
|
@@ -48,6 +48,8 @@ The exact generated surface is:
|
|
|
48
48
|
- `funnelsReport`
|
|
49
49
|
- `affiliatesReport`
|
|
50
50
|
- `dataCoverage`
|
|
51
|
+
- `vitalsReport` (optional capability `vitals`, hosts from `alpha.6`)
|
|
52
|
+
- `siteMap` (optional capability `siteMap`, hosts from `alpha.8`)
|
|
51
53
|
|
|
52
54
|
There are no ingestion, configuration-write, retention-write, raw-site, or site
|
|
53
55
|
mutation functions in this surface. The read-only goal, funnel, and affiliate
|
|
@@ -185,6 +187,8 @@ export const {
|
|
|
185
187
|
goalsReport,
|
|
186
188
|
funnelsReport,
|
|
187
189
|
affiliatesReport,
|
|
190
|
+
vitalsReport,
|
|
191
|
+
siteMap,
|
|
188
192
|
dataCoverage,
|
|
189
193
|
} = federated;
|
|
190
194
|
```
|
package/docs/upgrading.md
CHANGED
|
@@ -6,14 +6,17 @@ deployment.
|
|
|
6
6
|
|
|
7
7
|
## Release matrix
|
|
8
8
|
|
|
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
|
-
|
|
|
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
|
+
| Any release whose packed declarations lack federation | Stop and request an eligible exact registry release | Packed declarations contain the federation exports |
|
|
17
20
|
|
|
18
21
|
Do not upgrade a populated legacy deployment directly to a release that removes
|
|
19
22
|
the `events` table. Schema acceptance alone does not prove that telemetry was
|
|
@@ -93,6 +96,80 @@ The visitor-time indexes on legacy and canonical telemetry follow this rule.
|
|
|
93
96
|
Check the release notes for the exact intermediate version before upgrading a
|
|
94
97
|
large deployment.
|
|
95
98
|
|
|
99
|
+
## Web Vitals (alpha.6)
|
|
100
|
+
|
|
101
|
+
`0.1.0-alpha.6` adds field Web Vitals as a purely additive upgrade: a new
|
|
102
|
+
`vitalRollups` component table, the `vital` event type, the `vitalsReport`
|
|
103
|
+
query, the retention kind `vitalRollups` (governed by `reportsDays`), and the
|
|
104
|
+
pre-gzipped `GET /rastro/vitals.js` module. No data migration and no staged
|
|
105
|
+
indexes are involved — install and deploy through the host's normal workflow.
|
|
106
|
+
|
|
107
|
+
Measurement is opt-in per page. Nothing reports until the host adds the second
|
|
108
|
+
script tag documented in the README ("Field Web Vitals (optional)"); the base
|
|
109
|
+
tracker is byte-for-byte unchanged. After deploying, the federation manifest
|
|
110
|
+
advertises the optional `vitals` capability; in the central dashboard, open
|
|
111
|
+
**Conexões** and click **Verificar novamente** on the active connection so the
|
|
112
|
+
stored capability list picks it up and the **Vitais** view replaces its upgrade
|
|
113
|
+
notice. A dashboard talking to an older host shows that notice by design — the
|
|
114
|
+
protocol-v1 baseline does not require the capability.
|
|
115
|
+
|
|
116
|
+
Acceptance for this upgrade: `/rastro/health` reports
|
|
117
|
+
`features.webVitals: "optIn"`, `GET /rastro/vitals.js` serves the module, and
|
|
118
|
+
after one real page load is hidden, `vitalsReport` shows the measurement.
|
|
119
|
+
|
|
120
|
+
## Clock alignment (alpha.7)
|
|
121
|
+
|
|
122
|
+
`0.1.0-alpha.7` changes no schema. The served `tracker.js` and `vitals.js` stamp
|
|
123
|
+
each batch with `sentAt`, and `POST /rastro/events` re-anchors a batch whose
|
|
124
|
+
sender clock is more than 30 seconds from the server's before ingestion. Live
|
|
125
|
+
presence lasts five minutes after the last event or heartbeat instead of one.
|
|
126
|
+
Browsers keep the previous tracker for at most the five-minute cache plus one
|
|
127
|
+
background revalidation; a batch without `sentAt` is ingested exactly as before.
|
|
128
|
+
|
|
129
|
+
Acceptance for this upgrade: `/rastro/health` succeeds, `GET /rastro/tracker.js`
|
|
130
|
+
contains `sentAt`, and a visitor whose device clock is minutes off appears in
|
|
131
|
+
**Ao vivo** within seconds of loading a page.
|
|
132
|
+
|
|
133
|
+
## Site map (alpha.8)
|
|
134
|
+
|
|
135
|
+
`0.1.0-alpha.8` adds the route map as a purely additive upgrade: a new
|
|
136
|
+
`routeDailyRollups` component table fed at ingestion, optional fields on
|
|
137
|
+
existing tables (`liveSessions.previousPath`/`movedAt`,
|
|
138
|
+
`sessions.lastPageviewAt`, `analyticsCoverage.siteMapSince`), the `siteMap`
|
|
139
|
+
query, and the retention kind `siteMapRollups` (governed by `reportsDays`). No
|
|
140
|
+
data migration and no staged indexes are involved — install and deploy through
|
|
141
|
+
the host's normal workflow. Ingestion writes one extra rollup document per
|
|
142
|
+
distinct route in a batch.
|
|
143
|
+
|
|
144
|
+
One federation edit is required. The connector manifest is static package
|
|
145
|
+
metadata: after this upgrade it advertises the optional `siteMap` capability
|
|
146
|
+
whether or not the host module exports the function, and connection verification
|
|
147
|
+
checks the manifest's shape, not each function's existence. A host that upgrades
|
|
148
|
+
the package without re-exporting the query leaves the dashboard's **Mapa** view
|
|
149
|
+
failing with a missing-function error. Add `siteMap` to the destructured export
|
|
150
|
+
in `convex/rastroFederation.ts` before deploying:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
export const {
|
|
154
|
+
manifest,
|
|
155
|
+
// ...the existing exports...
|
|
156
|
+
vitalsReport,
|
|
157
|
+
siteMap,
|
|
158
|
+
dataCoverage,
|
|
159
|
+
} = federated;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The map has no history to backfill: coverage starts at `siteMapSince`, stamped
|
|
163
|
+
by the first ingest after the upgrade, and the dashboard names the date its map
|
|
164
|
+
data begins. In the central dashboard, open **Conexões** and click **Verificar
|
|
165
|
+
novamente** on the active connection so the stored capability list picks up
|
|
166
|
+
`siteMap` and the **Mapa** view replaces its upgrade notice.
|
|
167
|
+
|
|
168
|
+
Acceptance for this upgrade: `/rastro/health` reports
|
|
169
|
+
`features.siteMap: "dailyRollup"`, and after one real visit that navigates
|
|
170
|
+
between two pages, `siteMap` returns both routes and the transition between
|
|
171
|
+
them.
|
|
172
|
+
|
|
96
173
|
## Federation releases
|
|
97
174
|
|
|
98
175
|
Federation is additive to component telemetry but requires host tables,
|
|
@@ -120,6 +197,10 @@ control-plane issuer.
|
|
|
120
197
|
- Staged indexes completed before their readers were activated.
|
|
121
198
|
- `/rastro/health` succeeds on the HTTP-actions URL.
|
|
122
199
|
- A real tracker request is accepted from an allowed origin.
|
|
200
|
+
- When the host opts into Web Vitals: `/rastro/vitals.js` serves and
|
|
201
|
+
`vitalsReport` reflects a real page load.
|
|
202
|
+
- On `alpha.8` or later: the federation module re-exports `siteMap` and, after a
|
|
203
|
+
real two-page visit, `siteMap` returns both routes and their transition.
|
|
123
204
|
- Protected overview, journey, goal, funnel, and affiliate reports used by the
|
|
124
205
|
host still run.
|
|
125
206
|
- Trusted conversion idempotency and currency checks still pass.
|
package/llms.txt
CHANGED
|
@@ -16,6 +16,7 @@ Do not infer federation availability from this website, a dist-tag, or a source
|
|
|
16
16
|
## Architecture
|
|
17
17
|
|
|
18
18
|
- Tracker and telemetry: browser -> customer HTTP-actions URL -> customer Rastro component.
|
|
19
|
+
- Optional field Web Vitals (alpha.6+): a second browser module at `/rastro/vitals.js` reports LCP, CLS, INP, FCP, and TTFB to the same events endpoint, into bounded daily histograms.
|
|
19
20
|
- Reactive dashboard reads: amagerastro.com browser -> customer functions URL -> `rastroFederation:*`.
|
|
20
21
|
- Control plane: account, organization, connection metadata, short-lived token issuance, and revocation.
|
|
21
22
|
- The control plane does not proxy or persist customer analytics events.
|
|
@@ -29,7 +30,7 @@ Do not infer federation availability from this website, a dist-tag, or a source
|
|
|
29
30
|
- Resolve an indexed, authoritative local grant on every federated query.
|
|
30
31
|
- Grant one through ten explicit site IDs. Never grant by owner-wide enumeration.
|
|
31
32
|
- Use the canonical module name `convex/rastroFederation.ts`.
|
|
32
|
-
- Expose only the manifest, connection status, site summaries, reports, journeys, conversions, affiliates, funnels, goals, and coverage listed in the manifest.
|
|
33
|
+
- Expose only the manifest, connection status, site summaries, reports, journeys, conversions, affiliates, funnels, goals, web vitals, and coverage listed in the manifest.
|
|
33
34
|
- Use the functions URL as JWT audience and the HTTP-actions URL for tracker ingestion.
|
|
34
35
|
- Rehearse on an explicit development target. Production changes require fresh operator consent.
|
|
35
36
|
- Validate manifest, authenticated connection status, redacted site list, real ingestion, reactive live data, and local-first revocation.
|
|
@@ -51,7 +52,10 @@ Do not infer federation availability from this website, a dist-tag, or a source
|
|
|
51
52
|
- Hourly overview maximum: 24 hours.
|
|
52
53
|
- Funnel report maximum: 90 complete UTC days.
|
|
53
54
|
- Affiliate reports require complete UTC-day boundaries.
|
|
55
|
+
- Vitals report maximum: 90 complete UTC days; `vitals` is an optional capability that hosts before `alpha.6` do not advertise.
|
|
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.
|
|
54
57
|
- Maximum live visitors: 500.
|
|
58
|
+
- Live window: five minutes since the visitor's 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.
|
|
55
59
|
- Maximum journey events: 500.
|
|
56
60
|
- Token lifetime: 10 minutes, RS256, permission `analytics:read`.
|
|
57
61
|
|
|
@@ -63,3 +67,5 @@ Do not infer federation availability from this website, a dist-tag, or a source
|
|
|
63
67
|
- 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.
|
|
64
68
|
- Overview ranges require complete UTC hour or day buckets. Manual pagination honors `numItems` and `cursor` only.
|
|
65
69
|
- `dataCoverage` echoes the authorized site ID but returns no event, visitor, or session IDs.
|
|
70
|
+
- `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.
|
package/package.json
CHANGED
|
@@ -306,7 +306,8 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
306
306
|
| "goals"
|
|
307
307
|
| "funnels"
|
|
308
308
|
| "affiliates"
|
|
309
|
-
| "vitals"
|
|
309
|
+
| "vitals"
|
|
310
|
+
| "siteMap";
|
|
310
311
|
}>;
|
|
311
312
|
from: number;
|
|
312
313
|
siteId: string;
|
|
@@ -515,9 +516,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
515
516
|
lastSeenAt: number;
|
|
516
517
|
latitude?: number;
|
|
517
518
|
longitude?: number;
|
|
519
|
+
movedAt?: number;
|
|
518
520
|
os: string;
|
|
519
521
|
pageviewCount: number;
|
|
520
522
|
path: string;
|
|
523
|
+
previousPath?: string;
|
|
521
524
|
sessionId: string;
|
|
522
525
|
siteId: string;
|
|
523
526
|
source: string;
|
|
@@ -703,6 +706,54 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
703
706
|
},
|
|
704
707
|
Name
|
|
705
708
|
>;
|
|
709
|
+
siteMap: FunctionReference<
|
|
710
|
+
"query",
|
|
711
|
+
"internal",
|
|
712
|
+
{ from: number; siteIds: Array<string>; to: number },
|
|
713
|
+
{
|
|
714
|
+
from: number;
|
|
715
|
+
metadata: {
|
|
716
|
+
basis: "dailyRollup" | "boundedLegacy";
|
|
717
|
+
coverage: {
|
|
718
|
+
availableFrom: number | null;
|
|
719
|
+
availableThrough: number | null;
|
|
720
|
+
generation: number | null;
|
|
721
|
+
retainedBefore: number | null;
|
|
722
|
+
state: "complete" | "partial" | "retained" | "unavailable";
|
|
723
|
+
};
|
|
724
|
+
limits: {
|
|
725
|
+
routesPerDay: number;
|
|
726
|
+
rows: number;
|
|
727
|
+
transitionSlots: number;
|
|
728
|
+
};
|
|
729
|
+
range: {
|
|
730
|
+
boundary: "inclusive";
|
|
731
|
+
from: number;
|
|
732
|
+
timezone: "UTC";
|
|
733
|
+
to: number;
|
|
734
|
+
};
|
|
735
|
+
visitors: { basis: "estimatedUnique"; exact: boolean };
|
|
736
|
+
};
|
|
737
|
+
routes: Array<{
|
|
738
|
+
bounces: number;
|
|
739
|
+
daily: Array<{ bucketStart: number; pageviews: number }>;
|
|
740
|
+
dwellMs: number;
|
|
741
|
+
dwellSamples: number;
|
|
742
|
+
entries: number;
|
|
743
|
+
exits: number;
|
|
744
|
+
pageviews: number;
|
|
745
|
+
route: string;
|
|
746
|
+
transitionsIn: number;
|
|
747
|
+
transitionsOut: number;
|
|
748
|
+
visitors: number;
|
|
749
|
+
}>;
|
|
750
|
+
to: number;
|
|
751
|
+
transitions: Array<{ count: number; from: string; to: string }>;
|
|
752
|
+
truncated: { routes: boolean; transitions: boolean };
|
|
753
|
+
window: { from: number; to: number } | null;
|
|
754
|
+
},
|
|
755
|
+
Name
|
|
756
|
+
>;
|
|
706
757
|
visitorJourney: FunctionReference<
|
|
707
758
|
"query",
|
|
708
759
|
"internal",
|
|
@@ -828,6 +879,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
828
879
|
| "funnelRollups"
|
|
829
880
|
| "affiliateRollups"
|
|
830
881
|
| "vitalRollups"
|
|
882
|
+
| "siteMapRollups"
|
|
831
883
|
| "attributions"
|
|
832
884
|
| "liveSessions"
|
|
833
885
|
| "ingestWindows"
|
|
@@ -863,6 +915,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
863
915
|
| "funnelRollups"
|
|
864
916
|
| "affiliateRollups"
|
|
865
917
|
| "vitalRollups"
|
|
918
|
+
| "siteMapRollups"
|
|
866
919
|
| "attributions"
|
|
867
920
|
| "liveSessions"
|
|
868
921
|
| "ingestWindows"
|
|
@@ -909,6 +962,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
909
962
|
| "funnelRollups"
|
|
910
963
|
| "affiliateRollups"
|
|
911
964
|
| "vitalRollups"
|
|
965
|
+
| "siteMapRollups"
|
|
912
966
|
| "attributions"
|
|
913
967
|
| "liveSessions"
|
|
914
968
|
| "ingestWindows"
|
|
@@ -7,8 +7,9 @@ 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 = 60_000;
|
|
10
|
+
export const LIVE_SESSION_TTL_MS = 5 * 60_000;
|
|
11
11
|
export const LIVE_SWEEP_INTERVAL_MS = 5_000;
|
|
12
|
+
export const CLOCK_SKEW_TOLERANCE_MS = 30_000;
|
|
12
13
|
export const AGGREGATE_SHARDS = 128;
|
|
13
14
|
export const HOURLY_AGGREGATE_SHARDS = 256;
|
|
14
15
|
export const SECONDARY_AGGREGATE_SHARDS = 16;
|
|
@@ -53,3 +54,14 @@ export const HOUR_MS = 3_600_000;
|
|
|
53
54
|
export const MINUTE_MS = 60_000;
|
|
54
55
|
export const RETENTION_POLICY_INTERVAL_MS = DAY_MS;
|
|
55
56
|
export const MAX_VISITOR_ALIASES = 10;
|
|
57
|
+
// Site map rollups: one row per (site, route, UTC day, shard). A day keeps at
|
|
58
|
+
// most this many route rows per shard before new routes fold into "(other)";
|
|
59
|
+
// each row keeps this many outgoing targets before they fold the same way. A
|
|
60
|
+
// report reads at most MAX_SITE_MAP_ROWS rows, newest day first — about
|
|
61
|
+
// 5 MiB of rows, under the 8 MiB a function may read once the feature-report
|
|
62
|
+
// byte reserve is kept — and declares the older days it could not read as
|
|
63
|
+
// partial coverage: 20 days at the daily cap, 40 for a 100-route site.
|
|
64
|
+
export const MAX_SITE_MAP_ROUTES_PER_DAY = 200;
|
|
65
|
+
export const ROUTE_TRANSITION_SLOTS = 24;
|
|
66
|
+
export const MAX_SITE_MAP_ROWS = 8_000;
|
|
67
|
+
export const SITE_MAP_OTHER_ROUTE = "(other)";
|
|
@@ -12,11 +12,22 @@ export async function ensureAnalyticsControl(
|
|
|
12
12
|
.query("analyticsCoverage")
|
|
13
13
|
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
14
14
|
.unique();
|
|
15
|
-
if (existing)
|
|
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;
|
|
20
|
+
await ctx.db.patch("analyticsCoverage", existing._id, {
|
|
21
|
+
siteMapSince,
|
|
22
|
+
updatedAt: now,
|
|
23
|
+
});
|
|
24
|
+
return { ...existing, siteMapSince, updatedAt: now };
|
|
25
|
+
}
|
|
16
26
|
const fields = {
|
|
17
27
|
siteId,
|
|
18
28
|
generation: 1 as const,
|
|
19
29
|
rollupCompleteFrom: now + ROLLUP_SAFETY_DELAY_MS,
|
|
30
|
+
siteMapSince: now + ROLLUP_SAFETY_DELAY_MS,
|
|
20
31
|
retained,
|
|
21
32
|
updatedAt: now,
|
|
22
33
|
};
|
package/src/component/http.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
resolveGeoIp,
|
|
20
20
|
} from "./geo.js";
|
|
21
21
|
import { isPlainRecord } from "./guards.js";
|
|
22
|
-
import { sanitizeEvent } from "./sanitize.js";
|
|
22
|
+
import { alignClock, sanitizeEvent } from "./sanitize.js";
|
|
23
23
|
import { deriveVisitorKey, randomSecret } from "./identity.js";
|
|
24
24
|
import { classifyClient, isKnownBot } from "./useragent.js";
|
|
25
25
|
import type { TrackerEvent } from "./validators.js";
|
|
@@ -62,7 +62,7 @@ http.route({
|
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
let payload: { siteId: string; events: TrackerEvent[] };
|
|
65
|
+
let payload: { siteId: string; events: TrackerEvent[]; sentAt?: number };
|
|
66
66
|
try {
|
|
67
67
|
const bytes = await readLimitedBody(request, MAX_BATCH_BYTES);
|
|
68
68
|
const parsed: unknown = JSON.parse(new TextDecoder().decode(bytes));
|
|
@@ -87,6 +87,12 @@ http.route({
|
|
|
87
87
|
headers,
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
|
+
// The sender's clock says when its events happened, but a clock minutes
|
|
91
|
+
// off would keep the visitor out of the live view and file the traffic
|
|
92
|
+
// under another hour. The batch also says when it was sent, and that offset
|
|
93
|
+
// re-anchors every event here, before the transaction and the geo
|
|
94
|
+
// enrichment read them, so both see the same timestamps.
|
|
95
|
+
const events = alignClock(payload.events, payload.sentAt, Date.now());
|
|
90
96
|
|
|
91
97
|
const trustProxy = env.RASTRO_TRUST_PROXY === "true";
|
|
92
98
|
const metadata = await readRequestMetadata(ctx);
|
|
@@ -99,14 +105,14 @@ http.route({
|
|
|
99
105
|
// counter shape the tracker expects and names the reason in a header.
|
|
100
106
|
if (isKnownBot(userAgent)) {
|
|
101
107
|
return json(
|
|
102
|
-
{ accepted: 0, duplicates: 0, rejected:
|
|
108
|
+
{ accepted: 0, duplicates: 0, rejected: events.length },
|
|
103
109
|
200,
|
|
104
110
|
{ ...headers, "X-Rastro-Rejected": "bot" },
|
|
105
111
|
);
|
|
106
112
|
}
|
|
107
113
|
// Anonymous batches (visitorId === sessionId) get a salted daily key so
|
|
108
114
|
// repeat visits count once; the IP itself stops here.
|
|
109
|
-
const anonymous =
|
|
115
|
+
const anonymous = events.some(
|
|
110
116
|
(event) => event.visitorId === event.sessionId,
|
|
111
117
|
);
|
|
112
118
|
const visitorKey =
|
|
@@ -129,7 +135,7 @@ http.route({
|
|
|
129
135
|
const result = await ctx.runMutation(api.ingest.ingestBatch, {
|
|
130
136
|
siteId: payload.siteId as Id<"sites">,
|
|
131
137
|
origin,
|
|
132
|
-
events
|
|
138
|
+
events,
|
|
133
139
|
context,
|
|
134
140
|
});
|
|
135
141
|
if (
|
|
@@ -138,13 +144,13 @@ http.route({
|
|
|
138
144
|
geoIpStatus(env.RASTRO_GEOIP_PROVIDER, env.RASTRO_GEOIP_TOKEN) ===
|
|
139
145
|
"ready"
|
|
140
146
|
) {
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
147
|
+
const candidates = enrichmentCandidateEvents(events);
|
|
148
|
+
if (candidates) {
|
|
143
149
|
let stage: GeoIpFailureStage = "claim";
|
|
144
150
|
try {
|
|
145
151
|
const claim = await ctx.runMutation(internal.geo.claimEnrichment, {
|
|
146
152
|
siteId: payload.siteId as Id<"sites">,
|
|
147
|
-
events,
|
|
153
|
+
events: candidates,
|
|
148
154
|
dailyLimit: geoIpDailyLimit(env.RASTRO_GEOIP_DAILY_LIMIT),
|
|
149
155
|
});
|
|
150
156
|
if (claim.sessionIds.length > 0) {
|
|
@@ -292,6 +298,7 @@ http.route({
|
|
|
292
298
|
? "dailyHash"
|
|
293
299
|
: "session",
|
|
294
300
|
webVitals: "optIn",
|
|
301
|
+
siteMap: "dailyRollup",
|
|
295
302
|
},
|
|
296
303
|
},
|
|
297
304
|
200,
|
|
@@ -306,6 +313,7 @@ http.route({
|
|
|
306
313
|
function parsePayload(value: unknown): {
|
|
307
314
|
siteId: string;
|
|
308
315
|
events: TrackerEvent[];
|
|
316
|
+
sentAt?: number;
|
|
309
317
|
} {
|
|
310
318
|
if (
|
|
311
319
|
!isPlainRecord(value) ||
|
|
@@ -314,7 +322,12 @@ function parsePayload(value: unknown): {
|
|
|
314
322
|
) {
|
|
315
323
|
throw new Error("body must contain siteId and events");
|
|
316
324
|
}
|
|
317
|
-
|
|
325
|
+
const sentAt = optionalNumber(value.sentAt, "sentAt");
|
|
326
|
+
return {
|
|
327
|
+
siteId: value.siteId,
|
|
328
|
+
events: value.events.map(parseEvent),
|
|
329
|
+
...(sentAt === undefined ? {} : { sentAt }),
|
|
330
|
+
};
|
|
318
331
|
}
|
|
319
332
|
|
|
320
333
|
class PayloadTooLargeError extends Error {}
|