@iann29/rastro 0.3.0 → 0.5.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 +199 -50
- package/agent/integration.md +26 -10
- package/agent/manifest.json +10 -5
- package/agent/manifest.schema.json +14 -5
- package/dist/client/federation.d.ts +28 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +12 -1
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +262 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +119 -2
- 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 +72 -1
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +1 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/constants.d.ts +9 -2
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +31 -3
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.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 +6 -1
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/eventStore.d.ts +2 -0
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/geo.d.ts +1 -0
- package/dist/component/geo.d.ts.map +1 -1
- package/dist/component/geo.js +1 -1
- package/dist/component/geo.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +65 -2
- package/dist/component/http.js.map +1 -1
- package/dist/component/identity.d.ts +12 -0
- package/dist/component/identity.d.ts.map +1 -1
- package/dist/component/identity.js +30 -2
- package/dist/component/identity.js.map +1 -1
- package/dist/component/ingest.d.ts +36 -0
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +111 -20
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +34 -4
- package/dist/component/live.js.map +1 -1
- package/dist/component/origin.d.ts +70 -0
- package/dist/component/origin.d.ts.map +1 -0
- package/dist/component/origin.js +230 -0
- package/dist/component/origin.js.map +1 -0
- package/dist/component/platforms.d.ts +33 -0
- package/dist/component/platforms.d.ts.map +1 -0
- package/dist/component/platforms.js +328 -0
- package/dist/component/platforms.js.map +1 -0
- package/dist/component/reports.d.ts +27 -72
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +72 -16
- package/dist/component/reports.js.map +1 -1
- package/dist/component/rollupStore.d.ts +7 -4
- package/dist/component/rollupStore.d.ts.map +1 -1
- package/dist/component/rollupStore.js +37 -17
- package/dist/component/rollupStore.js.map +1 -1
- package/dist/component/rollups.d.ts +27 -8
- package/dist/component/rollups.d.ts.map +1 -1
- package/dist/component/rollups.js +110 -32
- package/dist/component/rollups.js.map +1 -1
- package/dist/component/sanitize.d.ts +5 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +33 -1
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +68 -9
- package/dist/component/schema.js +21 -3
- package/dist/component/schema.js.map +1 -1
- package/dist/component/trackedLinks.d.ts +91 -0
- package/dist/component/trackedLinks.d.ts.map +1 -0
- package/dist/component/trackedLinks.js +314 -0
- package/dist/component/trackedLinks.js.map +1 -0
- package/dist/component/useragent.d.ts +6 -0
- package/dist/component/useragent.d.ts.map +1 -1
- package/dist/component/useragent.js +9 -0
- package/dist/component/useragent.js.map +1 -1
- package/dist/component/validators.d.ts +51 -11
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +35 -1
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +11 -11
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +11 -11
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.d.ts +1 -0
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +104 -47
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker/vitals.d.ts.map +1 -1
- package/dist/tracker/vitals.js +22 -4
- package/dist/tracker/vitals.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/dist/vitals.min.js +1 -1
- package/docs/upgrading.md +234 -45
- package/llms.txt +9 -4
- package/package.json +2 -2
- package/src/component/_generated/api.ts +6 -0
- package/src/component/_generated/component.ts +153 -1
- package/src/component/_generated/server.ts +1 -0
- package/src/component/constants.ts +31 -3
- package/src/component/convex.config.ts +3 -0
- package/src/component/coverage.ts +7 -1
- package/src/component/geo.ts +1 -1
- package/src/component/http.ts +71 -2
- package/src/component/identity.ts +41 -2
- package/src/component/ingest.ts +146 -33
- package/src/component/live.ts +51 -4
- package/src/component/origin.ts +273 -0
- package/src/component/platforms.ts +359 -0
- package/src/component/reports.ts +98 -18
- package/src/component/rollupStore.ts +51 -20
- package/src/component/rollups.ts +141 -32
- package/src/component/sanitize.ts +39 -1
- package/src/component/schema.ts +24 -3
- package/src/component/trackedLinks.ts +384 -0
- package/src/component/useragent.ts +11 -0
- package/src/component/validators.ts +71 -0
- package/src/tracker/generated.ts +11 -11
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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.4 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.
|
|
@@ -42,7 +42,7 @@ capacity claim.
|
|
|
42
42
|
|
|
43
43
|
| Capability | Implementation |
|
|
44
44
|
| --------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
45
|
-
| Live visitor map | Reactive live sessions with city-level coordinates
|
|
45
|
+
| Live visitor map | Reactive live sessions with city-level coordinates; hidden or closed tabs leave in 10 s, crashes in 90 s |
|
|
46
46
|
| Traffic analytics | Sharded hour/day rollups, time series, top pages, sources, countries, devices, browsers, and events |
|
|
47
47
|
| Field Web Vitals | Opt-in tracker module reporting LCP, CLS, INP, FCP, and TTFB into bounded per-page daily histograms |
|
|
48
48
|
| Site map | Daily per-route rollups: entries, derived exits and bounces, time on page, and route-to-route transitions |
|
|
@@ -54,8 +54,8 @@ capacity claim.
|
|
|
54
54
|
| Automatic GeoIP | Opt-in native request metadata plus IPinfo/IPWhois fallback; bounded quota and no raw-IP persistence |
|
|
55
55
|
| Auto-captured clicks | Anchors, buttons, and `data-rastro-event` controls with a bounded visible label; outbound origin+path |
|
|
56
56
|
| Bot filtering | Self-declared crawlers, unfurlers, monitors, HTTP libraries, and headless browsers rejected at ingestion |
|
|
57
|
-
| Cookieless mode | No cookies:
|
|
58
|
-
| Tiny tracker | 2,
|
|
57
|
+
| Cookieless mode | No cookies: pseudonymous visitor id in `localStorage`; sessions end after 30 idle minutes or a sign-in |
|
|
58
|
+
| Tiny tracker | 2,594 bytes raw, 1,395 bytes gzip; the test suite enforces `< 1,400` bytes gzip |
|
|
59
59
|
|
|
60
60
|
## Quickstart
|
|
61
61
|
|
|
@@ -82,7 +82,7 @@ Never append `/rastro/tracker.js` to the functions URL.
|
|
|
82
82
|
npm install @iann29/rastro
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
The untagged npm `latest` channel is `0.
|
|
85
|
+
The untagged npm `latest` channel is `0.5.0`. Pin the exact version in the host;
|
|
86
86
|
the changelog and the upgrading guide say what each release changes.
|
|
87
87
|
|
|
88
88
|
### 2. Mount the component
|
|
@@ -203,6 +203,15 @@ data only when this opt-in is enabled and the runtime IP identifies a private
|
|
|
203
203
|
immediate peer. Caller-supplied proxy or geographic headers are ignored when
|
|
204
204
|
native metadata is absent or public.
|
|
205
205
|
|
|
206
|
+
`RASTRO_CLIENT_RATE_LIMIT` is for load-test canaries only. Set to `off`, it
|
|
207
|
+
lifts the per-client ingest budget (see
|
|
208
|
+
[Operational limits](#operational-limits)), so a benchmark driver sending every
|
|
209
|
+
request from one address is bounded by the per-site shards alone. Bind it like
|
|
210
|
+
the others: `RASTRO_CLIENT_RATE_LIMIT: v.optional(v.literal("off"))` in
|
|
211
|
+
`defineApp` and `RASTRO_CLIENT_RATE_LIMIT: app.env.RASTRO_CLIENT_RATE_LIMIT` in
|
|
212
|
+
`app.use`, as the example host does. Leave it unset on every customer
|
|
213
|
+
deployment.
|
|
214
|
+
|
|
206
215
|
The lookup has a 1.5-second timeout and fails open: telemetry ingestion
|
|
207
216
|
continues without location when the provider is unavailable. A session reserves
|
|
208
217
|
its single lookup attempt atomically only after its initial event is persisted;
|
|
@@ -228,8 +237,9 @@ until it reloads.
|
|
|
228
237
|
|
|
229
238
|
The tracker sends no client identification. Ingestion derives `browser`, `os`,
|
|
230
239
|
and `device` from headers the browser attaches itself, and persists only those
|
|
231
|
-
three normalized values;
|
|
232
|
-
|
|
240
|
+
three normalized values; it also reads the in-app browser's platform id
|
|
241
|
+
(`instagram`, `facebook`, `tiktok`…) for the session's origin, below. The raw
|
|
242
|
+
`User-Agent` never reaches a mutation or a component table.
|
|
233
243
|
|
|
234
244
|
Chromium attaches the low-entropy `Sec-CH-UA`, `Sec-CH-UA-Mobile`, and
|
|
235
245
|
`Sec-CH-UA-Platform` client hints to cross-origin requests from secure origins,
|
|
@@ -310,9 +320,11 @@ Do not call the authenticated example with a bare CLI command: without an
|
|
|
310
320
|
explicit test identity, `ctx.auth.getUserIdentity()` correctly returns `null`.
|
|
311
321
|
|
|
312
322
|
Allowed domains are checked at ingestion. Exact hosts and wildcard subdomains
|
|
313
|
-
such as `*.example.com` are supported. `Origin` is
|
|
314
|
-
|
|
315
|
-
|
|
323
|
+
such as `*.example.com` are supported. The `Origin` check is a browser control,
|
|
324
|
+
not an anti-abuse one: a browser cannot forge the header, but any other sender
|
|
325
|
+
writes the one it likes, and the site ID is in every page's source. Abuse is
|
|
326
|
+
bounded by event and byte budgets per session, per client network, and per site
|
|
327
|
+
(see [Operational limits](#operational-limits)).
|
|
316
328
|
|
|
317
329
|
Domain entries are hostnames, not host-and-port pairs. Use `localhost` or a full
|
|
318
330
|
origin such as `http://localhost:3000`; both normalize to the `localhost` host.
|
|
@@ -449,10 +461,12 @@ budget; add it only where you want performance data:
|
|
|
449
461
|
></script>
|
|
450
462
|
```
|
|
451
463
|
|
|
452
|
-
The module
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
464
|
+
The module derives the tracker's session and visitor ids by the same rule, from
|
|
465
|
+
`sessionStorage` and `localStorage`, whichever script loads first, and accepts
|
|
466
|
+
the same `data-endpoint` and `data-visitor` overrides. It needs the base tracker
|
|
467
|
+
on the page: vitals annotate the session the tracker opened, and ingestion
|
|
468
|
+
rejects them for a session it never saw. Measurements are attributed to the
|
|
469
|
+
landing path of the page load, never to later SPA routes. Vitals feed only the
|
|
456
470
|
`vitalsReport` histograms — analytics event counts, sessions, and live presence
|
|
457
471
|
do not change. Browsers without an entry type simply skip that metric (Safari
|
|
458
472
|
reports no LCP, CLS, or INP).
|
|
@@ -563,17 +577,115 @@ payload-bound by the site's financial ledger.
|
|
|
563
577
|
|
|
564
578
|
The tracker reads `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, and
|
|
565
579
|
`utm_content` from the landing URL, keeps them in `sessionStorage` for the rest
|
|
566
|
-
of the session
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
the
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
580
|
+
of the tab — a later session the tab opens after a sign-in or 30 idle minutes
|
|
581
|
+
keeps them too — and sends them as properties of every pageview. Besides those
|
|
582
|
+
five, the only thing the landing URL ever yields is the **name** of a click-ID
|
|
583
|
+
key it carried (below); no other query parameter, and no query value at all,
|
|
584
|
+
ever leaves the page. A session's `source` is the lowercase `utm_source` when
|
|
585
|
+
present and the referrer host otherwise, so campaign traffic no longer reads as
|
|
586
|
+
`direct`. `utmSource`, `utmMedium`, and `utmCampaign` stay on the session for
|
|
587
|
+
journeys; `overview.topCampaigns` ranks `utm_campaign` and `overview.topMediums`
|
|
588
|
+
the lowercase `utm_medium` next to `topSources`, by sessions once the range is
|
|
589
|
+
covered by the counters. `overview.topOutbound` ranks the hosts outbound clicks
|
|
590
|
+
reached — the host alone, never the path or query — by clicks. Both lists are
|
|
591
|
+
recorded from `metadata.dimensionsSince` (each site's first ingest on 0.1.0 plus
|
|
592
|
+
the rollup delay); a range that starts earlier ranks them from partial data, and
|
|
593
|
+
the dashboard says so.
|
|
594
|
+
|
|
595
|
+
A session also keeps where it came from, classified once on its first touch and
|
|
596
|
+
never guessed: `platform` (an id from a closed, versioned table of about forty —
|
|
597
|
+
`instagram`, `google`, `whatsapp`, `chatgpt` — or `unknown` for a host or
|
|
598
|
+
`utm_source` outside it, or `none`), `channel` (`direct`, `organic_search`,
|
|
599
|
+
`paid_search`, `organic_social`, `paid_social`, `messaging`, `email`,
|
|
600
|
+
`affiliate`, `ai`, `display`, `referral`, `offline`), and `evidence`, the rule
|
|
601
|
+
that decided it. One rule decides and the next only runs when it did not:
|
|
602
|
+
`utm_source` through the alias table, then click-ID names, then the referrer
|
|
603
|
+
host by registrable domain, then the in-app browser, then an affiliate `ref`,
|
|
604
|
+
which sets the channel and keeps the platform an earlier rule found. Hosts match
|
|
605
|
+
by whole label, never by substring, so `l.instagram.com`, `instagram.com` and
|
|
606
|
+
`com.instagram.android` are one platform while `docs.google.com` is not Google;
|
|
607
|
+
`utm_medium` picks the channel from a closed table that also accepts the channel
|
|
608
|
+
ids themselves, which is what a tracked link (below) emits, `offline` included.
|
|
609
|
+
Nothing at all is `direct`, the only way to be direct, and a referrer on the
|
|
610
|
+
site's own domains is an internal hop, never an origin. `overview.topPlatforms`,
|
|
611
|
+
`topChannels` and `topEvidence` rank them beside `topSources`; a `topPlatforms`
|
|
612
|
+
value is the platform id alone when the session arrived by that platform's
|
|
613
|
+
default channel and `platform@channel` when it did not (`google@paid_search`),
|
|
614
|
+
so one platform ranks once per channel it was reached by —
|
|
615
|
+
`splitPlatformDimension` reads the pair back. `dataCoverage`'s `origin` dataset
|
|
616
|
+
says from when a site carries them; sessions older than that are classified on
|
|
617
|
+
read and marked `evidence: "legacy"`. `source` keeps the raw `utm_source` or
|
|
618
|
+
host for the drill-down, exactly as before.
|
|
619
|
+
|
|
620
|
+
The tracker reads the click-ID keys of its landing URL and sends them in `clid`:
|
|
621
|
+
their **names** only (`fbclid`, `gclid`, `ttclid`, `mc_cid`…), never their
|
|
622
|
+
values, so no advertising identifier leaves the page. It matches the same
|
|
623
|
+
allowlist the server does, in the same ranking order, and sends at most eight;
|
|
624
|
+
ingestion drops any name outside the allowlist and keeps at most eight of what
|
|
625
|
+
is left. The referrer it sends is scheme and host, so a referrer of the form
|
|
626
|
+
`android-app://<package>` keeps the package alone and the app that opened the
|
|
627
|
+
link is a platform (`com.google.android.gm` is Gmail) instead of nothing.
|
|
628
|
+
|
|
629
|
+
## Tracked links
|
|
630
|
+
|
|
631
|
+
Some traffic arrives with nothing to read. A link tapped inside WhatsApp,
|
|
632
|
+
Telegram, an SMS or a native e-mail app, or scanned from a QR code, opens with
|
|
633
|
+
no referrer and no campaign, and any analytics tool files it as `direct`. A
|
|
634
|
+
tracked link closes that gap: the dashboard's **Links** tab (or the host API)
|
|
635
|
+
creates `/rastro/l/<slug>` for one place the link will be published, and every
|
|
636
|
+
click lands on the site with that place in its `utm_*`.
|
|
637
|
+
|
|
638
|
+
`GET /rastro/l/<slug>` answers `302` with `Cache-Control: no-store` to the
|
|
639
|
+
link's destination, after appending:
|
|
640
|
+
|
|
641
|
+
| Parameter | Value |
|
|
642
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
643
|
+
| `utm_source` | the platform: `instagram`, `whatsapp`, `qr`, or any id of 2–24 lowercase letters, digits or underscores |
|
|
644
|
+
| `utm_medium` | the channel: `organic_social`, `paid_social`, `messaging`, `email`, `affiliate`, `display`, `referral` or `offline` |
|
|
645
|
+
| `utm_campaign` | the campaign, or the slug when the link has none |
|
|
646
|
+
| `utm_content` | the slug |
|
|
647
|
+
|
|
648
|
+
The destination's own query stays exactly as it was, and a `utm_*` key it
|
|
649
|
+
already carries is never overridden. Unknown and disabled slugs answer `404` in
|
|
650
|
+
plain text. The tracker needs nothing new: the landing reads the `utm_*` like
|
|
651
|
+
any campaign link.
|
|
652
|
+
|
|
653
|
+
- **Clicks and bots.** Each click lands in a daily counter spread over four
|
|
654
|
+
shards, so a link that goes viral never serializes on one document. A
|
|
655
|
+
self-declared bot, including the WhatsApp, Facebook and Telegram link
|
|
656
|
+
previews, is still redirected but counted under `bots`, never `clicks`.
|
|
657
|
+
`listTrackedLinks` reports both, all time and over the 30 UTC days ending on
|
|
658
|
+
the day of the `now` it is given.
|
|
659
|
+
- **No open redirect.** A destination must be an absolute `http(s)` URL without
|
|
660
|
+
credentials on one of the site's domains (exact, or a `*.` pattern for
|
|
661
|
+
subdomains). The rule is checked when a link is created or changed and again
|
|
662
|
+
on every click, so a domain removed from the site stops redirecting at once.
|
|
663
|
+
- **Slugs** are 3–32 lowercase letters, digits or hyphens, starting and ending
|
|
664
|
+
with a letter or digit, and unique across the deployment because the route
|
|
665
|
+
names no site. A site keeps at most 200 links. Deleting a link frees its slug
|
|
666
|
+
and removes its counters; disabling it keeps both.
|
|
667
|
+
- **A short domain.** The route lives on the deployment's HTTP origin
|
|
668
|
+
(`https://<deployment>.convex.site/rastro/l/promo`). A host that wants a
|
|
669
|
+
presentable link serves that origin under a short domain of its own, such as
|
|
670
|
+
`go.example.com/rastro/l/promo`; nothing in the component changes.
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
await analytics.createTrackedLink(ctx, {
|
|
674
|
+
siteId,
|
|
675
|
+
slug: "bio-instagram",
|
|
676
|
+
destination: "https://www.example.com/promo",
|
|
677
|
+
platform: "instagram",
|
|
678
|
+
channel: "organic_social",
|
|
679
|
+
campaign: "spring-sale",
|
|
680
|
+
createdBy: userId, // an opaque id, never an e-mail address
|
|
681
|
+
});
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
A federated dashboard reaches the same functions once the host re-exports
|
|
685
|
+
`listTrackedLinks`, `createTrackedLink`, `updateTrackedLink`, and
|
|
686
|
+
`deleteTrackedLink` from `convex/rastroFederation.ts`. The connection then
|
|
687
|
+
advertises the `trackedLinks` capability: any reader lists the links, and
|
|
688
|
+
changing them needs `analytics:configure`, like goals and affiliates.
|
|
577
689
|
|
|
578
690
|
## Identify visitors after signup
|
|
579
691
|
|
|
@@ -596,14 +708,16 @@ await analytics.linkVisitor(ctx, {
|
|
|
596
708
|
```
|
|
597
709
|
|
|
598
710
|
`visitorJourney` for either id then returns one chronological journey; every
|
|
599
|
-
event keeps the id it was recorded with.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
711
|
+
event keeps the id it was recorded with. A session belongs to one identity: the
|
|
712
|
+
first page rendered with the new `data-visitor` opens a new session in the same
|
|
713
|
+
tab, carrying the referral and campaign over, and the link joins both sessions
|
|
714
|
+
in the journey. Links are idempotent, an anonymous id belongs to at most one
|
|
715
|
+
identity, a linked id cannot be linked again, chains and identity merges are
|
|
716
|
+
rejected, and an identity holds at most ten aliases. The tracker still sets no
|
|
717
|
+
cookie: the host's own session identifies the account and the anonymous ids live
|
|
718
|
+
in the browser's own storage (the visitor id in `localStorage`, the session id
|
|
719
|
+
in `sessionStorage`). Linking anonymous history to an account turns it into
|
|
720
|
+
personal data under the host's privacy policy; disclose it and offer an opt-out.
|
|
607
721
|
|
|
608
722
|
## Attribute trusted payments
|
|
609
723
|
|
|
@@ -738,6 +852,8 @@ The `Rastro` class wraps the component boundary for host functions:
|
|
|
738
852
|
- Configuration: `upsertGoal`, `listGoals`, `removeGoal`, `upsertFunnel`,
|
|
739
853
|
`listFunnels`, `removeFunnel`, `upsertAffiliate`, `listAffiliates`,
|
|
740
854
|
`removeAffiliate`
|
|
855
|
+
- Tracked links: `createTrackedLink`, `updateTrackedLink`, `deleteTrackedLink`,
|
|
856
|
+
`listTrackedLinks`
|
|
741
857
|
- Retention: `cleanup`, `setRetentionPolicy`, `disableRetentionPolicy`,
|
|
742
858
|
`runRetentionPolicy`, `retentionStatus`
|
|
743
859
|
|
|
@@ -832,24 +948,50 @@ boundaries use different bucket alignment.
|
|
|
832
948
|
never written by the ingestion transaction: a batch appends one document to
|
|
833
949
|
the site's rollup queue and one leased fold job per site merges up to 200
|
|
834
950
|
queued batches per run into the rows, so every rollup row has a single writer
|
|
835
|
-
and concurrent ingestion cannot conflict on it.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
951
|
+
and concurrent ingestion cannot conflict on it. Runs are debounced: the first
|
|
952
|
+
batch schedules a run one second out, batches arriving while it is pending
|
|
953
|
+
neither write the lease nor schedule anything, and the chain keeps running
|
|
954
|
+
once a second until a run finds the queue empty. Reports see a batch's rollups
|
|
955
|
+
about a second after ingestion accepts it; a fold chain that dies is restarted
|
|
956
|
+
by the next batch once its 60-second lease lapses. A run that throws is rolled
|
|
957
|
+
back and retries its oldest batch alone; a batch that fails three runs in a
|
|
958
|
+
row is set aside as a logged dead letter (kept 30 days) so the rest of the
|
|
959
|
+
queue keeps folding
|
|
960
|
+
- Session ingress budget: 120 events and 256 KiB per session per minute
|
|
961
|
+
- Client ingress budget: 1,200 events and 2 MiB per site per minute from one
|
|
962
|
+
client network, an IPv4 address or an IPv6 /64. The window is keyed by the
|
|
963
|
+
salted daily hash behind anonymous visitor keys, never by the address, and
|
|
964
|
+
every visitor behind one office or carrier-grade NAT shares it. It applies
|
|
965
|
+
when `/rastro/health` reports `clientIpAvailable: true`;
|
|
966
|
+
`RASTRO_CLIENT_RATE_LIMIT=off` lifts it on a load-test canary
|
|
967
|
+
- Site ingress budget: 256 deterministic per-minute shards, each capped at 1,500
|
|
968
|
+
events and 2 MiB, so a site admits at most 384,000 events and 512 MiB a
|
|
969
|
+
minute. Only evenly spread load (random session IDs) reaches that; a real peak
|
|
970
|
+
is uneven, and the realistic benchmark's ~100k events/minute tops out near
|
|
971
|
+
1,050 events in its hottest shard-minute
|
|
972
|
+
- Rate-window storage: one reusable row per active session, client network and
|
|
973
|
+
UTC day, and site shard
|
|
974
|
+
- Live session timeout: a `leave` beacon when the tab hides or the page unloads
|
|
975
|
+
retires the visitor about ten seconds later, and showing the tab again or a
|
|
976
|
+
back/forward-cache return revives them immediately; heartbeats fire every 20
|
|
977
|
+
seconds only while the tab is visible, so a hidden tab sends nothing; the
|
|
978
|
+
safety net for a browser that never says goodbye is 90 seconds since the last
|
|
979
|
+
event or heartbeat, with one bounded five-second expiry sweep per site; legacy
|
|
980
|
+
per-session jobs hand off to that sweep during the cutover
|
|
981
|
+
- Session: one identity's visit in one tab; a new `data-visitor` or 30 minutes
|
|
982
|
+
without an event opens the next one, which starts with a pageview of the
|
|
983
|
+
current page
|
|
847
984
|
- Clock alignment: the tracker and the vitals module stamp each batch with
|
|
848
985
|
`sentAt`; when that clock is more than 30 seconds from the server's, every
|
|
849
986
|
event in the batch is re-anchored by the offset before ingestion, so a visitor
|
|
850
987
|
whose device clock is wrong still goes live and is filed under the right hour
|
|
851
|
-
- Dashboard overview: up to ten sites
|
|
852
|
-
|
|
988
|
+
- Dashboard overview: up to ten sites. One overview reads at most 13,000
|
|
989
|
+
aggregate rows and a day holds 128 per site, so a daily range covers at most
|
|
990
|
+
101 days for one site, 50 for two, and 10 for ten; a wider range fails with
|
|
991
|
+
`LIMIT_EXCEEDED` naming its bound. A busy range that would cross the 8 MiB a
|
|
992
|
+
query may read fails with `REPORT_INCOMPLETE` first
|
|
993
|
+
- Hourly overview: up to 24 hours (an hour holds 256 rows per site, so 16 hours
|
|
994
|
+
for three sites and 5 for ten); wider ranges use daily buckets
|
|
853
995
|
- Retention cleanup: bounded batches. `setRetentionPolicy` automatically starts
|
|
854
996
|
leased, persisted cleanup chains for every policy-managed dataset; interrupted
|
|
855
997
|
attempts resume after lease expiry and stale generations cannot mutate the
|
|
@@ -888,7 +1030,8 @@ driver session pool is 4,096: at current speed the historical 2,048-session pool
|
|
|
888
1030
|
pushes single sessions past the documented 120 events/minute budget. Results
|
|
889
1031
|
came from one client process against the same non-production Synapse DEV
|
|
890
1032
|
deployment and site as the 2026-08-21 certification; they are engineering
|
|
891
|
-
evidence, not a production SLA.
|
|
1033
|
+
evidence, not a production SLA. Because that one client process is one address,
|
|
1034
|
+
a certification canary now also sets `RASTRO_CLIENT_RATE_LIMIT=off`.
|
|
892
1035
|
|
|
893
1036
|
Two caveats stand deliberately. The heavy row measured a store already holding
|
|
894
1037
|
the battery's own multi-million-event history; the 2026-08-22 fresh-store result
|
|
@@ -985,7 +1128,8 @@ deployments need no legacy-event migration.
|
|
|
985
1128
|
Amage Rastro's default tracker:
|
|
986
1129
|
|
|
987
1130
|
- sets no cookies;
|
|
988
|
-
- stores a pseudonymous session ID
|
|
1131
|
+
- stores a pseudonymous session ID, the visitor ID it opened with, the time of
|
|
1132
|
+
its last event, and the referral slug and campaign in `sessionStorage`, and a
|
|
989
1133
|
persistent pseudonymous visitor ID in `localStorage` — scoped to the site's
|
|
990
1134
|
origin, never shared across sites, gone when the visitor clears site data;
|
|
991
1135
|
- never persists raw visitor IP addresses in component tables;
|
|
@@ -1005,7 +1149,12 @@ Amage Rastro's default tracker:
|
|
|
1005
1149
|
clicked link or button (at most 64 characters, replaced by `data-rastro-label`
|
|
1006
1150
|
when present), and outbound link origins and pathnames (the overview ranks
|
|
1007
1151
|
destinations by host alone);
|
|
1008
|
-
- strips query strings and fragments from stored paths and links
|
|
1152
|
+
- strips query strings and fragments from stored paths and links; of the landing
|
|
1153
|
+
query it keeps the five `utm_*` values and the bare **names** of the
|
|
1154
|
+
advertising click-ID keys it recognizes (`fbclid`, `gclid`…), never a click-ID
|
|
1155
|
+
value, and never any other parameter;
|
|
1156
|
+
- sends the referrer as scheme and host only — an `android-app://<package>`
|
|
1157
|
+
included — never its path or query.
|
|
1009
1158
|
|
|
1010
1159
|
The optional Web Vitals module inherits this contract: it reports at most five
|
|
1011
1160
|
timing numbers per page load (milliseconds, or CLS scaled by 1000) against the
|
package/agent/integration.md
CHANGED
|
@@ -519,21 +519,25 @@ export const {
|
|
|
519
519
|
retentionStatus,
|
|
520
520
|
setRetentionPolicy,
|
|
521
521
|
disableRetentionPolicy,
|
|
522
|
+
listTrackedLinks,
|
|
523
|
+
createTrackedLink,
|
|
524
|
+
updateTrackedLink,
|
|
525
|
+
deleteTrackedLink,
|
|
522
526
|
} = federated;
|
|
523
527
|
```
|
|
524
528
|
|
|
525
529
|
`manifest` is public static metadata. Every report function is read-only and
|
|
526
530
|
requires both a valid JWT and a matching, non-revoked local grant. The functions
|
|
527
|
-
from `siteSettings`
|
|
528
|
-
`configure`, hosts from `alpha.11`): they additionally
|
|
529
|
-
`analytics:configure` on the token — the control plane claims it only
|
|
530
|
-
organization owners and admins — and in the grant's `permissions`, and fail
|
|
531
|
-
`FEDERATION_CONFIGURE_FORBIDDEN` otherwise. Export them even when every
|
|
532
|
-
stays read only, so a later opt-in needs no host deploy; the dashboard
|
|
533
|
-
calls them for a read-only connection. The issuer falls back to the
|
|
534
|
-
control plane so the module compiles and pushes before the
|
|
535
|
-
exists; tokens are still rejected until `auth.config.ts`
|
|
536
|
-
which needs both variables set and a push afterwards.
|
|
531
|
+
from `siteSettings` to `disableRetentionPolicy` form the configure scope
|
|
532
|
+
(optional capability `configure`, hosts from `alpha.11`): they additionally
|
|
533
|
+
require `analytics:configure` on the token — the control plane claims it only
|
|
534
|
+
for organization owners and admins — and in the grant's `permissions`, and fail
|
|
535
|
+
with `FEDERATION_CONFIGURE_FORBIDDEN` otherwise. Export them even when every
|
|
536
|
+
grant stays read only, so a later opt-in needs no host deploy; the dashboard
|
|
537
|
+
never calls them for a read-only connection. The issuer falls back to the
|
|
538
|
+
production control plane so the module compiles and pushes before the
|
|
539
|
+
environment variable exists; tokens are still rejected until `auth.config.ts`
|
|
540
|
+
trusts the same issuer, which needs both variables set and a push afterwards.
|
|
537
541
|
|
|
538
542
|
A public link's token carries `analytics:public` instead of `analytics:read` and
|
|
539
543
|
`rastro_site_ids` naming the link's sites. It runs only `manifest`,
|
|
@@ -587,6 +591,18 @@ the token and in the grant:
|
|
|
587
591
|
| `setRetentionPolicy` | `siteId` and five day counts between 1 and 3650 | Starts the recurring cleanup chains |
|
|
588
592
|
| `disableRetentionPolicy` | one `siteId` | Stops automatic cleanup; nothing is restored |
|
|
589
593
|
|
|
594
|
+
Tracked links, behind the optional `trackedLinks` capability. Listing needs only
|
|
595
|
+
a reader; the three writes require `analytics:configure` like the configure
|
|
596
|
+
scope above. A host serves them by re-exporting the four functions; one that
|
|
597
|
+
does not keeps working and advertises no `trackedLinks` capability.
|
|
598
|
+
|
|
599
|
+
| Function | Input summary | Important output/behavior |
|
|
600
|
+
| ------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
601
|
+
| `listTrackedLinks` | one `siteId` and `now` | Definitions with clicks and bot hits, all time and over 30 UTC days |
|
|
602
|
+
| `createTrackedLink` | `siteId`, slug, destination, platform, channel, campaign, `createdBy` | Returns the link ID; the destination must be on one of the site's domains |
|
|
603
|
+
| `updateTrackedLink` | `siteId`, `linkId`, optional destination, platform, channel, campaign, disabled | Changes everything but the slug; a disabled link answers 404 |
|
|
604
|
+
| `deleteTrackedLink` | `siteId`, `linkId` | Deletes the link, frees its slug, and purges its click counters |
|
|
605
|
+
|
|
590
606
|
The federated surface deliberately excludes ingestion, owner IDs, network IDs,
|
|
591
607
|
and arbitrary host functions, and shows a site's domains only through
|
|
592
608
|
`siteSettings` to a connection allowed to change them. Goal, funnel, and
|
package/agent/manifest.json
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"upgrading": "https://www.amagerastro.com/docs/reference/upgrading.md",
|
|
16
16
|
"integrity": {
|
|
17
17
|
"algorithm": "sha256",
|
|
18
|
-
"agent": "
|
|
19
|
-
"llms": "
|
|
18
|
+
"agent": "994ba7a57ada6238b9d8fdc3280e352c185c0a97772e627ee1bdc1afda1185cb",
|
|
19
|
+
"llms": "46fb9a206d2f41d30f72f80ac9764d30535d7d1d65afe475e03d72b075cf6359",
|
|
20
20
|
"protocol": "f2be7e3db250824d0c1293eb31b2e4514ebed8b21fe4974d4f4797fd86abe04f",
|
|
21
21
|
"setup": "bf2509a87a8eab9c047276b813f029bb2c9ddd58c0e54754432e3fef7891f3c9",
|
|
22
|
-
"upgrading": "
|
|
22
|
+
"upgrading": "e675d73c3df963ad85aa918c3b4bd8acb74c9179940497bf034ff566fa513cc7"
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"controlPlane": {
|
|
@@ -85,7 +85,11 @@
|
|
|
85
85
|
"removeAffiliate": "removeAffiliate",
|
|
86
86
|
"retentionStatus": "retentionStatus",
|
|
87
87
|
"setRetentionPolicy": "setRetentionPolicy",
|
|
88
|
-
"disableRetentionPolicy": "disableRetentionPolicy"
|
|
88
|
+
"disableRetentionPolicy": "disableRetentionPolicy",
|
|
89
|
+
"listTrackedLinks": "listTrackedLinks",
|
|
90
|
+
"createTrackedLink": "createTrackedLink",
|
|
91
|
+
"updateTrackedLink": "updateTrackedLink",
|
|
92
|
+
"deleteTrackedLink": "deleteTrackedLink"
|
|
89
93
|
},
|
|
90
94
|
"capabilities": [
|
|
91
95
|
"siteSummaries",
|
|
@@ -104,7 +108,8 @@
|
|
|
104
108
|
"vitals",
|
|
105
109
|
"siteMap",
|
|
106
110
|
"localDays",
|
|
107
|
-
"configure"
|
|
111
|
+
"configure",
|
|
112
|
+
"trackedLinks"
|
|
108
113
|
],
|
|
109
114
|
"limits": {
|
|
110
115
|
"maxSitesPerRequest": 10,
|
|
@@ -185,7 +185,11 @@
|
|
|
185
185
|
"removeAffiliate",
|
|
186
186
|
"retentionStatus",
|
|
187
187
|
"setRetentionPolicy",
|
|
188
|
-
"disableRetentionPolicy"
|
|
188
|
+
"disableRetentionPolicy",
|
|
189
|
+
"listTrackedLinks",
|
|
190
|
+
"createTrackedLink",
|
|
191
|
+
"updateTrackedLink",
|
|
192
|
+
"deleteTrackedLink"
|
|
189
193
|
],
|
|
190
194
|
"properties": {
|
|
191
195
|
"manifest": { "const": "manifest" },
|
|
@@ -218,14 +222,18 @@
|
|
|
218
222
|
"removeAffiliate": { "const": "removeAffiliate" },
|
|
219
223
|
"retentionStatus": { "const": "retentionStatus" },
|
|
220
224
|
"setRetentionPolicy": { "const": "setRetentionPolicy" },
|
|
221
|
-
"disableRetentionPolicy": { "const": "disableRetentionPolicy" }
|
|
225
|
+
"disableRetentionPolicy": { "const": "disableRetentionPolicy" },
|
|
226
|
+
"listTrackedLinks": { "const": "listTrackedLinks" },
|
|
227
|
+
"createTrackedLink": { "const": "createTrackedLink" },
|
|
228
|
+
"updateTrackedLink": { "const": "updateTrackedLink" },
|
|
229
|
+
"deleteTrackedLink": { "const": "deleteTrackedLink" }
|
|
222
230
|
},
|
|
223
231
|
"additionalProperties": false
|
|
224
232
|
},
|
|
225
233
|
"capabilities": {
|
|
226
234
|
"type": "array",
|
|
227
|
-
"minItems":
|
|
228
|
-
"maxItems":
|
|
235
|
+
"minItems": 16,
|
|
236
|
+
"maxItems": 18,
|
|
229
237
|
"uniqueItems": true,
|
|
230
238
|
"items": {
|
|
231
239
|
"enum": [
|
|
@@ -245,7 +253,8 @@
|
|
|
245
253
|
"vitals",
|
|
246
254
|
"siteMap",
|
|
247
255
|
"localDays",
|
|
248
|
-
"configure"
|
|
256
|
+
"configure",
|
|
257
|
+
"trackedLinks"
|
|
249
258
|
]
|
|
250
259
|
}
|
|
251
260
|
},
|