@iann29/rastro 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +374 -62
- package/agent/integration.md +651 -0
- package/agent/manifest.json +183 -0
- package/agent/manifest.schema.json +405 -0
- package/dist/client/federation.d.ts +205 -0
- package/dist/client/federation.d.ts.map +1 -0
- package/dist/client/federation.js +179 -0
- package/dist/client/federation.js.map +1 -0
- package/dist/client/index.d.ts +1658 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +228 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +10 -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 +169 -1
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/cardinality.d.ts +12 -0
- package/dist/component/cardinality.d.ts.map +1 -0
- package/dist/component/cardinality.js +94 -0
- package/dist/component/cardinality.js.map +1 -0
- package/dist/component/constants.d.ts +11 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +11 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +18 -0
- package/dist/component/coverage.d.ts.map +1 -0
- package/dist/component/coverage.js +19 -0
- package/dist/component/coverage.js.map +1 -0
- package/dist/component/errors.d.ts +1 -1
- package/dist/component/errors.d.ts.map +1 -1
- package/dist/component/errors.js.map +1 -1
- package/dist/component/eventStore.d.ts +1 -0
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +85 -36
- package/dist/component/http.js.map +1 -1
- package/dist/component/identity.d.ts +13 -0
- package/dist/component/identity.d.ts.map +1 -0
- package/dist/component/identity.js +58 -0
- package/dist/component/identity.js.map +1 -0
- package/dist/component/ingest.d.ts +1 -0
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +258 -19
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +6 -3
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +120 -2
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +659 -91
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts +75 -1
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +517 -54
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts +4 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +13 -5
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +224 -7
- package/dist/component/schema.js +113 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +5 -1
- package/dist/component/sites.js.map +1 -1
- package/dist/component/useragent.d.ts +9 -0
- package/dist/component/useragent.d.ts.map +1 -0
- package/dist/component/useragent.js +152 -0
- package/dist/component/useragent.js.map +1 -0
- package/dist/component/validators.d.ts +42 -11
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +10 -1
- package/dist/component/validators.js.map +1 -1
- package/dist/component/visitors.d.ts +19 -0
- package/dist/component/visitors.d.ts.map +1 -0
- package/dist/component/visitors.js +86 -0
- package/dist/component/visitors.js.map +1 -0
- package/dist/tracker/generated.d.ts +4 -4
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +4 -4
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.js +23 -11
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/benchmarks/2026-08-20-realistic.md +4 -4
- package/docs/benchmarks/2026-08-21-formal-certification.md +333 -0
- package/docs/federation-setup.md +395 -0
- package/docs/federation.md +258 -0
- package/docs/upgrading.md +130 -0
- package/llms.txt +65 -0
- package/package.json +41 -11
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +10 -0
- package/src/component/_generated/component.ts +228 -1
- package/src/component/cardinality.ts +116 -0
- package/src/component/constants.ts +11 -0
- package/src/component/coverage.ts +25 -0
- package/src/component/errors.ts +2 -1
- package/src/component/http.ts +129 -38
- package/src/component/identity.ts +74 -0
- package/src/component/ingest.ts +414 -13
- package/src/component/live.ts +8 -3
- package/src/component/reports.ts +910 -98
- package/src/component/retention.ts +624 -88
- package/src/component/sanitize.ts +19 -5
- package/src/component/schema.ts +125 -0
- package/src/component/sites.ts +5 -1
- package/src/component/useragent.ts +171 -0
- package/src/component/validators.ts +11 -0
- package/src/component/visitors.ts +105 -0
package/README.md
CHANGED
|
@@ -4,35 +4,80 @@
|
|
|
4
4
|
|
|
5
5
|
Privacy-first, real-time web analytics built as an isolated Convex component.
|
|
6
6
|
|
|
7
|
-
Amage Rastro combines a 1
|
|
7
|
+
Amage Rastro combines a 1.1 KB gzip browser tracker with reactive traffic
|
|
8
8
|
metrics, a live visitor atlas, journey timelines, trusted payment attribution,
|
|
9
9
|
goals, funnels, affiliates, and multi-site reports. Data and functions stay
|
|
10
10
|
inside your Convex deployment.
|
|
11
11
|
|
|
12
12
|

|
|
13
13
|
|
|
14
|
+
## Project status
|
|
15
|
+
|
|
16
|
+
Amage Rastro is an actively developed alpha with a canonical batched event
|
|
17
|
+
store, protected CI, exact run-scoped benchmark reconciliation, and published
|
|
18
|
+
npm prereleases. The untagged `latest` channel intentionally remains on the
|
|
19
|
+
first public alpha. Federation consumers must statically inspect an
|
|
20
|
+
operator-approved exact registry artifact for every required runtime and type
|
|
21
|
+
export before changing a host; follow the gate in the
|
|
22
|
+
[federation setup runbook](docs/federation-setup.md). Never substitute a mutable
|
|
23
|
+
branch or website-supplied source tarball.
|
|
24
|
+
|
|
25
|
+
Public integration documentation is available at
|
|
26
|
+
[`www.amagerastro.com/docs/`](https://www.amagerastro.com/docs/). Coding agents
|
|
27
|
+
should start with [`llms.txt`](https://www.amagerastro.com/llms.txt) and verify
|
|
28
|
+
the [machine manifest](https://www.amagerastro.com/agent/manifest.json).
|
|
29
|
+
|
|
30
|
+
Current evidence supports more than 120 million stored events/day for realistic
|
|
31
|
+
traffic. Near-limit heavy payloads are certified at 76.62 million/day, and the
|
|
32
|
+
feature-saturated profile at 33.22 million/day. Workload-specific results and
|
|
33
|
+
caveats are reported separately rather than collapsed into one capacity claim.
|
|
34
|
+
|
|
14
35
|
## What ships
|
|
15
36
|
|
|
16
|
-
| Capability | Implementation
|
|
17
|
-
| --------------------------- |
|
|
18
|
-
| Live visitor map | Reactive live sessions with coarse city-level coordinates and TTL expiry
|
|
19
|
-
| Traffic analytics | Sharded hour/day rollups, time series, top pages, sources, countries, devices, browsers, and events
|
|
20
|
-
| Journey replay | Bounded pageview/click/custom-event timelines; no DOM, keystroke, or form-value recording
|
|
21
|
-
| Payment conversion tracking | Trusted server-side ledger with idempotent event IDs and canonical site currency
|
|
22
|
-
| Goals and funnels | Exact pageview/custom-event goals and ordered conversion funnels
|
|
23
|
-
| Affiliates | Referral attribution, trusted revenue, and commission accounting
|
|
24
|
-
| Cross-site analytics | Reports over up to ten authorized site IDs with a preloaded pseudonymous visitor ID
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
| Capability | Implementation |
|
|
38
|
+
| --------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| Live visitor map | Reactive live sessions with coarse city-level coordinates and TTL expiry |
|
|
40
|
+
| Traffic analytics | Sharded hour/day rollups, time series, top pages, sources, countries, devices, browsers, and events |
|
|
41
|
+
| Journey replay | Bounded pageview/click/custom-event timelines; no DOM, keystroke, or form-value recording |
|
|
42
|
+
| Payment conversion tracking | Trusted server-side ledger with idempotent event IDs and canonical site currency |
|
|
43
|
+
| Goals and funnels | Exact pageview/custom-event goals and ordered conversion funnels |
|
|
44
|
+
| Affiliates | Referral attribution, trusted revenue, and commission accounting |
|
|
45
|
+
| Cross-site analytics | Reports over up to ten authorized site IDs with a preloaded pseudonymous visitor ID |
|
|
46
|
+
| Automatic GeoIP | Opt-in native request metadata plus IPinfo/IPWhois fallback; bounded quota and no raw-IP persistence |
|
|
47
|
+
| Auto-captured clicks | Anchors, buttons, and `data-rastro-event` controls with a bounded visible label; outbound origin+path |
|
|
48
|
+
| Bot filtering | Self-declared crawlers, unfurlers, monitors, HTTP libraries, and headless browsers rejected at ingestion |
|
|
49
|
+
| Cookieless mode | Session-scoped pseudonymous IDs in `sessionStorage`; no cookies |
|
|
50
|
+
| Tiny tracker | 2,132 bytes raw, 1,127 bytes gzip; the test suite enforces `< 1,280` bytes gzip |
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
### Prerequisites
|
|
55
|
+
|
|
56
|
+
- Convex `^1.43.0` or a compatible Synapse deployment.
|
|
57
|
+
- An existing host authentication provider for administrative and local report
|
|
58
|
+
functions. The browser ingestion route itself is public and origin-checked.
|
|
59
|
+
- Two deployment origins recorded separately:
|
|
60
|
+
|
|
61
|
+
| Purpose | Convex Cloud example | Synapse example |
|
|
62
|
+
| ------------------------------ | ------------------------------ | -------------------------------------- |
|
|
63
|
+
| Functions and reactive queries | `https://project.convex.cloud` | `https://project.synapse.example` |
|
|
64
|
+
| HTTP actions and tracker | `https://project.convex.site` | `https://project.site.synapse.example` |
|
|
65
|
+
|
|
66
|
+
Never append `/rastro/tracker.js` to the functions URL.
|
|
67
|
+
|
|
68
|
+
### 1. Install
|
|
30
69
|
|
|
31
70
|
```bash
|
|
32
|
-
npm install @iann29/rastro
|
|
71
|
+
npm install @iann29/rastro@alpha
|
|
33
72
|
```
|
|
34
73
|
|
|
35
|
-
|
|
74
|
+
The untagged npm `latest` channel intentionally remains on the first public
|
|
75
|
+
alpha. Use `@alpha` until a stable release is promoted.
|
|
76
|
+
|
|
77
|
+
### 2. Mount the component
|
|
78
|
+
|
|
79
|
+
Merge the component into the project's existing configuration; preserve every
|
|
80
|
+
component and environment binding already registered there.
|
|
36
81
|
|
|
37
82
|
```ts
|
|
38
83
|
// convex/convex.config.ts
|
|
@@ -52,6 +97,31 @@ This creates isolated component tables and four HTTP routes:
|
|
|
52
97
|
- `GET /rastro/tracker.js` — the pre-gzipped tracker
|
|
53
98
|
- `GET /rastro/health` — integration health
|
|
54
99
|
|
|
100
|
+
Push the component and regenerate host bindings before importing
|
|
101
|
+
`components.rastroAnalytics`:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Convex Cloud development deployment
|
|
105
|
+
npx convex dev
|
|
106
|
+
|
|
107
|
+
# Synapse development deployment
|
|
108
|
+
synapse dev --once
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For production, use the product's existing release workflow. On an uncoordinated
|
|
112
|
+
Convex project that is `npx convex deploy`; on Synapse use
|
|
113
|
+
`synapse release --execute` when the project has a release manifest, otherwise
|
|
114
|
+
`synapse deploy`. Never deploy before confirming the target.
|
|
115
|
+
|
|
116
|
+
Verify the HTTP origin independently from the functions origin:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
curl -fsS https://YOUR_HTTP_ORIGIN/rastro/health
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A successful health response proves that the component HTTP routes are mounted;
|
|
123
|
+
it does not prove that a browser event was accepted.
|
|
124
|
+
|
|
55
125
|
### Automatic GeoIP
|
|
56
126
|
|
|
57
127
|
Rastro uses Convex request metadata as the canonical client IP and can resolve
|
|
@@ -80,9 +150,9 @@ export default app;
|
|
|
80
150
|
`ipwhois` is also supported. Its token is optional for the documented
|
|
81
151
|
1,000-request/day free endpoint and recommended for production. Bind it as
|
|
82
152
|
`RASTRO_GEOIP_TOKEN` in the same way as the IPinfo example.
|
|
83
|
-
`RASTRO_GEOIP_DAILY_LIMIT` defaults to 1,000 lookups per site per UTC day
|
|
84
|
-
|
|
85
|
-
|
|
153
|
+
`RASTRO_GEOIP_DAILY_LIMIT` defaults to 1,000 lookups per site per UTC day and
|
|
154
|
+
may be set from `0` through `1000000` to match the provider plan. Admission is
|
|
155
|
+
transactional, so concurrent first-session requests cannot exceed the cap.
|
|
86
156
|
|
|
87
157
|
When Rastro runs behind a reverse proxy that supplies `X-Forwarded-For` or
|
|
88
158
|
Vercel/Cloudflare geographic headers, explicitly bind
|
|
@@ -100,13 +170,47 @@ sanitized country, city, and coordinates rounded to one decimal place are
|
|
|
100
170
|
persisted.
|
|
101
171
|
|
|
102
172
|
`GET /rastro/health` reports whether the runtime exposes a usable client IP and
|
|
103
|
-
whether GeoIP is configured and the daily cap, without returning the address or
|
|
104
|
-
administrators can exercise the configured provider through
|
|
105
|
-
`geo:probe` action. If health reports no usable client IP, use
|
|
106
|
-
Vercel/Cloudflare geographic headers with `RASTRO_TRUST_PROXY` or fix
|
|
107
|
-
deployment's proxy forwarding.
|
|
108
|
-
|
|
109
|
-
|
|
173
|
+
whether GeoIP is configured and the daily cap, without returning the address or
|
|
174
|
+
token. Deployment administrators can exercise the configured provider through
|
|
175
|
+
the internal `geo:probe` action. If health reports no usable client IP, use
|
|
176
|
+
trusted Vercel/Cloudflare geographic headers with `RASTRO_TRUST_PROXY` or fix
|
|
177
|
+
the deployment's proxy forwarding.
|
|
178
|
+
|
|
179
|
+
### Browser and device classification
|
|
180
|
+
|
|
181
|
+
The tracker sends no client identification. Ingestion derives `browser`, `os`,
|
|
182
|
+
and `device` from headers the browser attaches itself, and persists only those
|
|
183
|
+
three normalized values; the raw `User-Agent` never reaches a mutation or a
|
|
184
|
+
component table.
|
|
185
|
+
|
|
186
|
+
Chromium attaches the low-entropy `Sec-CH-UA`, `Sec-CH-UA-Mobile`, and
|
|
187
|
+
`Sec-CH-UA-Platform` client hints to cross-origin requests from secure origins,
|
|
188
|
+
with no `Accept-CH` opt-in required of the tracked site. They are preferred when
|
|
189
|
+
present, because they are the only signal separating Brave, Opera, Vivaldi,
|
|
190
|
+
Samsung Internet, Yandex, and bare Chromium from Chrome: Brave ships Chrome's
|
|
191
|
+
`User-Agent` verbatim by design. Hint brands are allowlisted, so an unrecognized
|
|
192
|
+
or forged brand falls back to the `User-Agent` instead of entering the browser
|
|
193
|
+
dimension verbatim.
|
|
194
|
+
|
|
195
|
+
The `User-Agent` remains the fallback for Firefox and Safari, which implement no
|
|
196
|
+
client hints, and for insecure origins, which receive none. Known limits of that
|
|
197
|
+
path:
|
|
198
|
+
|
|
199
|
+
- iPadOS 13+ requests sites with a Macintosh `User-Agent` by default and Safari
|
|
200
|
+
sends no hints, so those sessions are reported as desktop macOS.
|
|
201
|
+
- Without hints every Android client is reported as mobile;
|
|
202
|
+
`Sec-CH-UA-Mobile: ?0` on the Android platform is the only available tablet
|
|
203
|
+
signal, because the explicit `Sec-CH-UA-Form-Factors` hint is high entropy and
|
|
204
|
+
would require an `Accept-CH` opt-in and a permissions-policy delegation from
|
|
205
|
+
every tracked site.
|
|
206
|
+
- Known bots are rejected at ingestion by `User-Agent`: self-declared crawlers,
|
|
207
|
+
link unfurlers, uptime monitors, HTTP libraries, headless browsers, and any
|
|
208
|
+
agent carrying a URL. The response keeps the usual counters with `rejected`
|
|
209
|
+
set and adds `X-Rastro-Rejected: bot`; overview metadata reports
|
|
210
|
+
`botDetection` as `userAgent`. A scripted browser that spoofs a stock
|
|
211
|
+
`User-Agent` is still counted as an ordinary session.
|
|
212
|
+
|
|
213
|
+
### 3. Create a site
|
|
110
214
|
|
|
111
215
|
Components cannot read the host app's authentication state. The host must
|
|
112
216
|
authenticate the caller and pass an opaque owner ID.
|
|
@@ -132,7 +236,7 @@ export const createSite = mutation({
|
|
|
132
236
|
if (!identity) throw new Error("Not authenticated");
|
|
133
237
|
|
|
134
238
|
return await analytics.createSite(ctx, {
|
|
135
|
-
ownerId: identity.
|
|
239
|
+
ownerId: identity.tokenIdentifier,
|
|
136
240
|
name: args.name,
|
|
137
241
|
domains: args.domains,
|
|
138
242
|
currency: args.currency,
|
|
@@ -142,12 +246,30 @@ export const createSite = mutation({
|
|
|
142
246
|
});
|
|
143
247
|
```
|
|
144
248
|
|
|
249
|
+
Call this mutation from the host application's authenticated administration UI
|
|
250
|
+
and persist or display the returned `siteId`:
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
const createSite = useMutation(api.analyticsAdmin.createSite);
|
|
254
|
+
const siteId = await createSite({
|
|
255
|
+
name: "Amage Product",
|
|
256
|
+
domains: ["product.example.com", "localhost"],
|
|
257
|
+
currency: "BRL",
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Do not call the authenticated example with a bare CLI command: without an
|
|
262
|
+
explicit test identity, `ctx.auth.getUserIdentity()` correctly returns `null`.
|
|
263
|
+
|
|
145
264
|
Allowed domains are checked at ingestion. Exact hosts and wildcard subdomains
|
|
146
265
|
such as `*.example.com` are supported. `Origin` is an abuse signal, not
|
|
147
266
|
authentication; Amage Rastro also enforces per-session and fixed-shard per-site
|
|
148
267
|
event/byte budgets.
|
|
149
268
|
|
|
150
|
-
|
|
269
|
+
Domain entries are hostnames, not host-and-port pairs. Use `localhost` or a full
|
|
270
|
+
origin such as `http://localhost:3000`; both normalize to the `localhost` host.
|
|
271
|
+
|
|
272
|
+
### 4. Add the tracker
|
|
151
273
|
|
|
152
274
|
Use the HTTP-actions URL for your deployment, not the Convex function URL.
|
|
153
275
|
|
|
@@ -179,6 +301,9 @@ serves the script and ingestion from different origins:
|
|
|
179
301
|
|
|
180
302
|
### React
|
|
181
303
|
|
|
304
|
+
React is an optional peer dependency. It is required only when importing the
|
|
305
|
+
`@iann29/rastro/react` entry point; backend-only consumers do not need it.
|
|
306
|
+
|
|
182
307
|
```tsx
|
|
183
308
|
import { useState } from "react";
|
|
184
309
|
import { RastroScript, useRastro } from "@iann29/rastro/react";
|
|
@@ -208,6 +333,26 @@ the same value on authorized sites to connect their journeys. Amage Rastro
|
|
|
208
333
|
intentionally has no mutable post-load `identify` command: changing identity
|
|
209
334
|
after a session starts would make the session ambiguous.
|
|
210
335
|
|
|
336
|
+
### 5. Verify ingestion
|
|
337
|
+
|
|
338
|
+
1. Open an allowed site origin in a fresh browser tab.
|
|
339
|
+
2. In browser developer tools, filter Network requests by `rastro/events`.
|
|
340
|
+
3. Wait for the normal tracker flush, which is debounced by approximately 1.2
|
|
341
|
+
seconds.
|
|
342
|
+
4. Confirm `POST /rastro/events` returns HTTP 200 and a response containing
|
|
343
|
+
`accepted`, `duplicates`, and `rejected` counters.
|
|
344
|
+
5. Run one authenticated local report, such as `analytics:overview`, for the
|
|
345
|
+
returned `siteId` and a complete UTC hour/day range.
|
|
346
|
+
|
|
347
|
+
The tracker intentionally does not interrupt the product UI when transport
|
|
348
|
+
fails. A loaded `tracker.js` or successful `/health` response alone is not proof
|
|
349
|
+
of ingestion. The usual causes of a rejected or missing request are:
|
|
350
|
+
|
|
351
|
+
- using the functions URL instead of the HTTP-actions URL;
|
|
352
|
+
- omitting the exact browser host from the site's `domains`;
|
|
353
|
+
- including query strings or paths in a configured base URL;
|
|
354
|
+
- querying a partial UTC bucket or a site the authenticated user does not own.
|
|
355
|
+
|
|
211
356
|
## Track custom events
|
|
212
357
|
|
|
213
358
|
```js
|
|
@@ -222,6 +367,12 @@ For declarative click capture:
|
|
|
222
367
|
</button>
|
|
223
368
|
```
|
|
224
369
|
|
|
370
|
+
Auto-captured clicks record the control's visible text, bounded to 64 characters
|
|
371
|
+
and whitespace-normalized, so journeys read "Clicked «View pricing»" without
|
|
372
|
+
annotation. `data-rastro-label` replaces that text; use it on controls whose
|
|
373
|
+
copy may contain personal data. Input values are never read. Outbound links keep
|
|
374
|
+
their origin and pathname; internal links keep the pathname.
|
|
375
|
+
|
|
225
376
|
Properties accept bounded string, number, boolean, or null values. Never send
|
|
226
377
|
emails, names, form values, secrets, or other personal data as analytics
|
|
227
378
|
properties.
|
|
@@ -232,6 +383,46 @@ session or with a timestamp in another minute is allowed. The tracker preserves
|
|
|
232
383
|
timestamps when retrying. Trusted payment event IDs remain separately
|
|
233
384
|
payload-bound by the site's financial ledger.
|
|
234
385
|
|
|
386
|
+
## Attribute campaigns
|
|
387
|
+
|
|
388
|
+
The tracker reads `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, and
|
|
389
|
+
`utm_content` from the landing URL, keeps them in `sessionStorage` for the rest
|
|
390
|
+
of the session, and sends them as properties of every pageview; no other query
|
|
391
|
+
parameter ever leaves the page. A session's `source` is the lowercase
|
|
392
|
+
`utm_source` when present and the referrer host otherwise, so campaign traffic
|
|
393
|
+
no longer reads as `direct`. `utmSource`, `utmMedium`, and `utmCampaign` stay on
|
|
394
|
+
the session for journeys, and `overview.topCampaigns` ranks `utm_campaign` by
|
|
395
|
+
event volume next to `topSources`.
|
|
396
|
+
|
|
397
|
+
## Identify visitors after signup
|
|
398
|
+
|
|
399
|
+
Anonymous sessions carry a random `visitorId` until the host supplies a
|
|
400
|
+
pseudonymous one through `data-visitor` or the `RastroScript` `visitorId` prop.
|
|
401
|
+
To keep the anonymous history when a visitor creates an account or signs in,
|
|
402
|
+
link the two ids from the trusted server path, never from the browser:
|
|
403
|
+
|
|
404
|
+
```js
|
|
405
|
+
const { visitorId: anonymousVisitorId } = window.rastro("context");
|
|
406
|
+
// Send anonymousVisitorId with the signup or sign-in request.
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
await analytics.linkVisitor(ctx, {
|
|
411
|
+
siteId,
|
|
412
|
+
visitorId: user.analyticsId, // the value later passed to the tracker
|
|
413
|
+
previousVisitorId: anonymousVisitorId,
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
`visitorJourney` for either id then returns one chronological journey; every
|
|
418
|
+
event keeps the id it was recorded with. Links are idempotent, an anonymous id
|
|
419
|
+
belongs to at most one identity, a linked id cannot be linked again, chains and
|
|
420
|
+
identity merges are rejected, and an identity holds at most ten aliases. The
|
|
421
|
+
tracker still sets no cookie: the host's own session identifies the account and
|
|
422
|
+
the anonymous id lives only in `sessionStorage`. Linking anonymous history to an
|
|
423
|
+
account turns it into personal data under the host's privacy policy; disclose it
|
|
424
|
+
and offer an opt-out.
|
|
425
|
+
|
|
235
426
|
## Attribute trusted payments
|
|
236
427
|
|
|
237
428
|
Browser-created conversion events are untrusted telemetry and never affect
|
|
@@ -300,12 +491,12 @@ export const {
|
|
|
300
491
|
|
|
301
492
|
for (const siteId of request.siteIds) {
|
|
302
493
|
const site = await analytics.getSite(ctx, siteId);
|
|
303
|
-
if (!site || site.ownerId !== identity.
|
|
494
|
+
if (!site || site.ownerId !== identity.tokenIdentifier) {
|
|
304
495
|
throw new Error("Not authorized for analytics site");
|
|
305
496
|
}
|
|
306
497
|
}
|
|
307
498
|
|
|
308
|
-
if (request.ownerId && request.ownerId !== identity.
|
|
499
|
+
if (request.ownerId && request.ownerId !== identity.tokenIdentifier) {
|
|
309
500
|
throw new Error("Not authorized for analytics owner");
|
|
310
501
|
}
|
|
311
502
|
},
|
|
@@ -316,25 +507,83 @@ Do not copy the unauthenticated demo boundary into production. The included demo
|
|
|
316
507
|
exposes only fixed-scope read reports plus bounded, idempotent seed/live-refresh
|
|
317
508
|
functions.
|
|
318
509
|
|
|
510
|
+
### Federated dashboard backend
|
|
511
|
+
|
|
512
|
+
The reference control plane in `control-plane/` can connect a central dashboard
|
|
513
|
+
while telemetry remains in the customer's deployment. It uses
|
|
514
|
+
`exposeFederatedAnalyticsApi` with issuer and connection-resolver options. The
|
|
515
|
+
helper constructs its authorization boundary internally; it cannot accept a
|
|
516
|
+
permissive host authorizer. Its surface contains a public protocol v1 manifest,
|
|
517
|
+
authenticated redacted connection status, redacted site summaries, and only the
|
|
518
|
+
reactive dashboard report queries.
|
|
519
|
+
|
|
520
|
+
The JWT only identifies the requested connection. Site access remains
|
|
521
|
+
authoritative in a local host record resolved on every query, so revocation is
|
|
522
|
+
immediate and a token cannot grant itself another site. `listSites` accepts no
|
|
523
|
+
site arguments and omits owner, domain, and network configuration. Stable
|
|
524
|
+
manifest names, capabilities, limits, and authorization error codes are exported
|
|
525
|
+
for connector clients. The host must already have Convex authentication and a
|
|
526
|
+
local authoritative grant. Follow
|
|
527
|
+
[`docs/federation-setup.md`](docs/federation-setup.md) for the executable setup
|
|
528
|
+
and verification sequence; use [`docs/federation.md`](docs/federation.md) as the
|
|
529
|
+
protocol reference. The production control plane is available at
|
|
530
|
+
`https://www.amagerastro.com`; before connecting a customer deployment, verify
|
|
531
|
+
the selected package exports the federation helper as shown in the setup
|
|
532
|
+
runbook.
|
|
533
|
+
|
|
319
534
|
## Backend API
|
|
320
535
|
|
|
321
536
|
The `Rastro` class wraps the component boundary for host functions:
|
|
322
537
|
|
|
323
538
|
- Sites: `createSite`, `updateSite`, `getSite`, `listSites`
|
|
324
|
-
- Ingestion: `ingest`, `trackConversion`
|
|
539
|
+
- Ingestion: `ingest`, `trackConversion`, `linkVisitor`
|
|
325
540
|
- Reports: `overview`, `liveVisitors`, `listSessions`, `sessionJourney`,
|
|
326
541
|
`listConversions`, `visitorJourney`, `goalsReport`, `funnelsReport`,
|
|
327
|
-
`affiliatesReport`
|
|
542
|
+
`affiliatesReport`, `dataCoverage`
|
|
328
543
|
- Configuration: `upsertGoal`, `listGoals`, `removeGoal`, `upsertFunnel`,
|
|
329
544
|
`listFunnels`, `removeFunnel`, `upsertAffiliate`, `listAffiliates`,
|
|
330
545
|
`removeAffiliate`
|
|
331
|
-
- Retention: `cleanup`
|
|
546
|
+
- Retention: `cleanup`, `setRetentionPolicy`, `disableRetentionPolicy`,
|
|
547
|
+
`runRetentionPolicy`, `retentionStatus`
|
|
332
548
|
|
|
333
549
|
Every public function has argument and return validators. Growing reads are
|
|
334
550
|
indexed and bounded. Component pagination uses scope-bound opaque keyset cursors
|
|
335
551
|
because native `.paginate()` is not supported inside Convex components; page
|
|
336
552
|
cost stays constant instead of increasing with an offset.
|
|
337
553
|
|
|
554
|
+
Overview responses label visitor values as either HLL-estimated unique visitors
|
|
555
|
+
or legacy `bucketOccurrences`; legacy aggregate rows are never presented as
|
|
556
|
+
exact uniques. Anonymous sessions from the same device and network within one
|
|
557
|
+
UTC day count as one visitor through a salted daily key, so cookieless visitor
|
|
558
|
+
counts no longer collapse into session counts; runtimes without a client IP fall
|
|
559
|
+
back to one visitor per session, which `/rastro/health` reports as
|
|
560
|
+
`uniqueVisitors: "session"` instead of `"dailyHash"`. Breakdown metadata
|
|
561
|
+
likewise identifies page, source, country, device, browser, and event rankings
|
|
562
|
+
as event-volume counts. Feature status is explicit: outbound-link event volume
|
|
563
|
+
is supported, bot detection rejects self-declared agents by `User-Agent`, and
|
|
564
|
+
session replay and error insights are currently unsupported.
|
|
565
|
+
|
|
566
|
+
All report ranges use integer Unix milliseconds with inclusive `from` and `to`
|
|
567
|
+
boundaries. Range metadata reports `boundary: "inclusive"` and
|
|
568
|
+
`timezone: "UTC"`; the site's timezone is presentation metadata and never
|
|
569
|
+
changes report bucket boundaries. Overview queries must contain complete UTC
|
|
570
|
+
hours or days, and funnel and affiliate reports must contain complete UTC days.
|
|
571
|
+
Those aggregate-backed reports fail with `REPORT_INCOMPLETE` instead of silently
|
|
572
|
+
including the rest of a partial bucket. Goal reports retain exact millisecond
|
|
573
|
+
filtering while bounded goal-completion rows are available.
|
|
574
|
+
|
|
575
|
+
Goal, funnel, and affiliate reports use definition-ID keyed daily rollups when
|
|
576
|
+
the low-churn rollout control proves that generation is available. Each loader
|
|
577
|
+
enforces day, row, document, and byte headroom before returning a complete
|
|
578
|
+
result; it never returns a silently truncated report. Ordinary ingestion never
|
|
579
|
+
patches or schedules work against a shared coverage document. `dataCoverage`
|
|
580
|
+
derives availability through bounded indexed reads of source and rollup records,
|
|
581
|
+
while the control document stores only rollout generation and durable retention
|
|
582
|
+
watermarks. Heartbeats can advance session availability but do not claim event
|
|
583
|
+
or aggregate availability. Hourly and daily overview availability are reported
|
|
584
|
+
separately as `overviewHour` and `overviewDay` because their retention
|
|
585
|
+
boundaries use different bucket alignment.
|
|
586
|
+
|
|
338
587
|
## Operational limits
|
|
339
588
|
|
|
340
589
|
- HTTP body: 64 KiB
|
|
@@ -344,16 +593,25 @@ cost stays constant instead of increasing with an offset.
|
|
|
344
593
|
- Primary aggregate shards: 256 hourly and 128 daily, derived from stable
|
|
345
594
|
session identity
|
|
346
595
|
- Funnel and affiliate aggregate shards: 16
|
|
347
|
-
- Site ingress budget: 4,096 deterministic per-minute shards, each capped at
|
|
348
|
-
|
|
596
|
+
- Site ingress budget: 4,096 deterministic per-minute shards, each capped at 300
|
|
597
|
+
events and 512 KiB (1,228,800 admitted events/minute at uniform load)
|
|
349
598
|
- Rate-window storage: one reusable row per active session and site shard
|
|
350
599
|
- Live session timeout: 60 seconds with one bounded five-second expiry sweep per
|
|
351
600
|
site; legacy per-session jobs hand off to that sweep during the cutover
|
|
352
601
|
- Dashboard overview: up to ten sites and bounded report ranges
|
|
353
602
|
- Hourly overview: up to 24 hours; wider ranges use daily buckets
|
|
354
|
-
- Retention cleanup: bounded batches
|
|
355
|
-
|
|
356
|
-
|
|
603
|
+
- Retention cleanup: bounded batches. `setRetentionPolicy` automatically starts
|
|
604
|
+
leased, persisted cleanup chains for every policy-managed dataset; interrupted
|
|
605
|
+
attempts resume after lease expiry and stale generations cannot mutate the
|
|
606
|
+
active chain. A completed chain schedules its next cycle after one day and
|
|
607
|
+
recomputes its cutoff from the then-current policy. Policy updates replace the
|
|
608
|
+
generation, and `disableRetentionPolicy` makes every queued attempt stale.
|
|
609
|
+
`runRetentionPolicy` remains available for an explicit bounded run. The
|
|
610
|
+
`events` cleanup kind removes canonical event-batch documents and processes at
|
|
611
|
+
most 32 heavy batches per transaction.
|
|
612
|
+
- Retention cutoffs are bucket-safe: event minutes, hourly aggregates, daily
|
|
613
|
+
aggregates and feature rollups preserve the bucket containing a cutoff and
|
|
614
|
+
return the exact effective `before` boundary they applied.
|
|
357
615
|
|
|
358
616
|
Capacity claims use declared workload profiles. Complete release evidence
|
|
359
617
|
reports ten-minute `lean`, `realistic`, `heavy`, and browser `feature` profiles
|
|
@@ -361,12 +619,52 @@ separately. The browser `feature` profile exercises configured goals, funnels,
|
|
|
361
619
|
and affiliate attribution; trusted conversions require a separate server-side
|
|
362
620
|
driver and certification result.
|
|
363
621
|
|
|
364
|
-
|
|
365
|
-
|
|
622
|
+
### Historical ingest evidence requiring recertification
|
|
623
|
+
|
|
624
|
+
| Profile | Workload | Certified stored/day | p95 latency | 100M/day target |
|
|
625
|
+
| --------- | ------------------------------------------------- | -------------------: | ----------: | --------------- |
|
|
626
|
+
| Lean | Minimal events, maximum batches | 136.65M | 4,543 ms | Pass |
|
|
627
|
+
| Realistic | Reused sessions, browser mix, context, heartbeats | 120.81M | 4,247 ms | Pass |
|
|
628
|
+
| Heavy | 62 KiB requests with bounded custom properties | 76.62M | 3,670 ms | Below target |
|
|
629
|
+
| Feature | Goals, funnels, and affiliate attribution | 33.22M | 6,226 ms | Below target |
|
|
630
|
+
|
|
631
|
+
Every listed run lasted at least ten minutes, completed with zero request
|
|
632
|
+
failures, duplicates, or rejected events, and exactly reconciled the expected
|
|
633
|
+
stored events. Results came from one client process against a non-production
|
|
634
|
+
Synapse DEV deployment; they are engineering evidence, not a production SLA. The
|
|
635
|
+
heavy row is the formal recertification after removing redundant payload
|
|
636
|
+
serialization. GeoIP with a provider enabled and trusted server-side conversions
|
|
637
|
+
remain separate pending certifications.
|
|
638
|
+
|
|
639
|
+
These runs predate the current source-derived coverage and recurring-retention
|
|
640
|
+
changes. They remain historical evidence for the named revisions and profiles,
|
|
641
|
+
not certification of this uncommitted implementation. Recertify the final code
|
|
642
|
+
before presenting any row as current capacity.
|
|
643
|
+
|
|
644
|
+
The latest formal results are recorded in
|
|
645
|
+
[`docs/benchmarks/2026-08-21-formal-certification.md`](docs/benchmarks/2026-08-21-formal-certification.md).
|
|
646
|
+
Earlier development evidence remains available in
|
|
647
|
+
[`docs/benchmarks/2026-08-20-realistic.md`](docs/benchmarks/2026-08-20-realistic.md).
|
|
366
648
|
|
|
367
649
|
From a repository checkout, run a duration-based profile against a confirmed
|
|
368
650
|
non-production deployment and a dedicated idle benchmark site:
|
|
369
651
|
|
|
652
|
+
For a deployment-free CI smoke benchmark of write-heavy ingestion plus reactive
|
|
653
|
+
overview and feature report queries, run:
|
|
654
|
+
|
|
655
|
+
```bash
|
|
656
|
+
npm run benchmark:local
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
The local harness uses `convex-test`, enforces a 12-document ingest transaction
|
|
660
|
+
budget, checks reactive totals before and after a write, and queries proven
|
|
661
|
+
daily rollups with the maximum configured 50 goals, 20 funnels, and 100
|
|
662
|
+
affiliates under deterministic 1,000-document and 4 MiB read ceilings. It also
|
|
663
|
+
drives concurrent ingest calls and asserts that their shared rollout/retention
|
|
664
|
+
control document is unchanged. It fails when its generous CI latency budgets are
|
|
665
|
+
exceeded. This is a deterministic local regression guard, not evidence of live
|
|
666
|
+
deployment throughput or capacity.
|
|
667
|
+
|
|
370
668
|
```bash
|
|
371
669
|
npm run --silent benchmark:ingest -- \
|
|
372
670
|
--url https://example.site/rastro/events \
|
|
@@ -375,7 +673,8 @@ npm run --silent benchmark:ingest -- \
|
|
|
375
673
|
--origin https://your-allowed-origin.example \
|
|
376
674
|
--profile realistic \
|
|
377
675
|
--duration 600 \
|
|
378
|
-
--concurrency
|
|
676
|
+
--concurrency 75 \
|
|
677
|
+
--batch-size 50 \
|
|
379
678
|
--certify
|
|
380
679
|
```
|
|
381
680
|
|
|
@@ -390,10 +689,10 @@ generates exact session IDs from a cryptographic run UUID and reads them through
|
|
|
390
689
|
bounded index lookups, so unrelated traffic cannot alter the result. The query
|
|
391
690
|
uses the session counters persisted transactionally with event batches; it
|
|
392
691
|
certifies ingest accounting, not raw-event availability after later retention.
|
|
393
|
-
Reports distinguish accepted ingress, heartbeats, and stored events.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
692
|
+
Reports distinguish accepted ingress, heartbeats, and stored events. Shorter
|
|
693
|
+
completed runs report extrapolated daily estimates only. `--certify` requires at
|
|
694
|
+
least ten minutes and successful reconciliation before emitting certified daily
|
|
695
|
+
capacity fields or exiting successfully as a certification.
|
|
397
696
|
|
|
398
697
|
The ingestion transaction folds metric/dimension deltas, session rate counters,
|
|
399
698
|
site-shard counters, session/live state, visitor attribution, and exact funnel
|
|
@@ -407,16 +706,15 @@ Click/outbound/browser-conversion telemetry skips goal and funnel work that
|
|
|
407
706
|
cannot match those event types.
|
|
408
707
|
|
|
409
708
|
Raw telemetry is stored only in immutable `eventBatches`. Migrated events retain
|
|
410
|
-
their original per-event browser and geo context inside those batches, while
|
|
411
|
-
|
|
709
|
+
their original per-event browser and geo context inside those batches, while new
|
|
710
|
+
events hydrate context from their session. Journey `_id` values are stable
|
|
412
711
|
strings derived from batch and nested-event identity, and session-journey
|
|
413
712
|
cursors use timestamp, creation time, and ID.
|
|
414
713
|
|
|
415
714
|
Existing deployments that still contain the retired `events` table must not
|
|
416
|
-
upgrade directly to this cutover.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
need no migration.
|
|
715
|
+
upgrade directly to this cutover. Follow the exact intermediate release and
|
|
716
|
+
verification sequence in [`docs/upgrading.md`](docs/upgrading.md). Fresh
|
|
717
|
+
deployments need no legacy-event migration.
|
|
420
718
|
|
|
421
719
|
## Privacy contract
|
|
422
720
|
|
|
@@ -425,13 +723,20 @@ Amage Rastro's default tracker:
|
|
|
425
723
|
- sets no cookies;
|
|
426
724
|
- stores a pseudonymous session ID and referral slug in `sessionStorage`;
|
|
427
725
|
- never persists raw visitor IP addresses in component tables;
|
|
726
|
+
- counts anonymous unique visitors through a salted hash of the request IP and
|
|
727
|
+
`User-Agent` that rotates every UTC day, derived in the HTTP action from a
|
|
728
|
+
per-deployment secret; a host-supplied `visitorId` always takes precedence,
|
|
729
|
+
and the raw IP never reaches a mutation or a table;
|
|
730
|
+
- never persists the raw `User-Agent`, only a normalized browser, operating
|
|
731
|
+
system, and device;
|
|
428
732
|
- processes an IP only when an edge supplies geography or an operator enables a
|
|
429
733
|
GeoIP provider, which receives that IP under its own privacy terms;
|
|
430
734
|
- stores only country, city, and coordinates rounded to one decimal place;
|
|
431
|
-
- does not record the DOM, screenshots, keystrokes, form values,
|
|
432
|
-
page text;
|
|
433
|
-
- captures only bounded paths, explicit properties,
|
|
434
|
-
|
|
735
|
+
- does not record the DOM, screenshots, keystrokes, form values, input contents,
|
|
736
|
+
or arbitrary page text;
|
|
737
|
+
- captures only bounded paths, explicit properties, the visible label of a
|
|
738
|
+
clicked link or button (at most 64 characters, replaced by `data-rastro-label`
|
|
739
|
+
when present), and outbound link origins and pathnames;
|
|
435
740
|
- strips query strings and fragments from stored paths and links.
|
|
436
741
|
|
|
437
742
|
Cookieless does not automatically mean consent-exempt in every jurisdiction.
|
|
@@ -454,6 +759,10 @@ synapse dev --once
|
|
|
454
759
|
npm run dev:frontend
|
|
455
760
|
```
|
|
456
761
|
|
|
762
|
+
The root `convex.json` targets the control plane in `control-plane/convex`. The
|
|
763
|
+
reference consumer host in `example/` is a separate Convex app; see
|
|
764
|
+
[`example/README.md`](example/README.md) to run, seed, and pair it.
|
|
765
|
+
|
|
457
766
|
Quality gates:
|
|
458
767
|
|
|
459
768
|
```bash
|
|
@@ -461,20 +770,23 @@ npm test
|
|
|
461
770
|
npm run typecheck
|
|
462
771
|
npm run lint
|
|
463
772
|
npm run build
|
|
464
|
-
|
|
773
|
+
npm exec vite -- build example
|
|
465
774
|
```
|
|
466
775
|
|
|
467
776
|
The test suite covers origin enforcement, event/privacy sanitization,
|
|
468
777
|
idempotency, reusable session/site rate limits, bounded live expiry chains,
|
|
469
778
|
folded aggregate writes, goals, funnels, trusted payments, currency safety,
|
|
470
779
|
affiliate commission, cross-site journeys, keyset pagination beyond 1,000 rows,
|
|
471
|
-
retention,
|
|
780
|
+
retention, request metadata and GeoIP, exact byte boundaries, HTTP routes, React
|
|
781
|
+
integration, and tracker size/behavior.
|
|
472
782
|
|
|
473
|
-
## Synapse
|
|
783
|
+
## Synapse deployments
|
|
474
784
|
|
|
475
|
-
The
|
|
476
|
-
|
|
477
|
-
`.synapse/` are gitignored.
|
|
785
|
+
The repository root is linked to the private Synapse project whose DEV and PROD
|
|
786
|
+
deployments run the control plane in `control-plane/`. The public-facing site
|
|
787
|
+
URL is resolved from `.env.local`; credentials and `.synapse/` are gitignored.
|
|
788
|
+
The consumer host in `example/` is a separate Convex app with its own deployment
|
|
789
|
+
link; run its CLI commands from that directory.
|
|
478
790
|
|
|
479
791
|
## License
|
|
480
792
|
|