@iann29/rastro 0.1.0-alpha.10 → 0.1.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +215 -46
  2. package/agent/integration.md +186 -46
  3. package/agent/manifest.json +28 -10
  4. package/agent/manifest.schema.json +41 -8
  5. package/dist/client/federation.d.ts +156 -11
  6. package/dist/client/federation.d.ts.map +1 -1
  7. package/dist/client/federation.js +82 -4
  8. package/dist/client/federation.js.map +1 -1
  9. package/dist/client/index.d.ts +306 -97
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +116 -13
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +6 -0
  14. package/dist/component/_generated/api.d.ts.map +1 -1
  15. package/dist/component/_generated/api.js.map +1 -1
  16. package/dist/component/_generated/component.d.ts +26 -13
  17. package/dist/component/_generated/component.d.ts.map +1 -1
  18. package/dist/component/constants.d.ts +3 -0
  19. package/dist/component/constants.d.ts.map +1 -1
  20. package/dist/component/constants.js +6 -0
  21. package/dist/component/constants.js.map +1 -1
  22. package/dist/component/coverage.d.ts +8 -0
  23. package/dist/component/coverage.d.ts.map +1 -1
  24. package/dist/component/coverage.js +32 -6
  25. package/dist/component/coverage.js.map +1 -1
  26. package/dist/component/geo.d.ts +2 -0
  27. package/dist/component/geo.d.ts.map +1 -1
  28. package/dist/component/geo.js +56 -19
  29. package/dist/component/geo.js.map +1 -1
  30. package/dist/component/http.d.ts.map +1 -1
  31. package/dist/component/http.js +47 -27
  32. package/dist/component/http.js.map +1 -1
  33. package/dist/component/ingest.d.ts.map +1 -1
  34. package/dist/component/ingest.js +149 -295
  35. package/dist/component/ingest.js.map +1 -1
  36. package/dist/component/localTime.d.ts +25 -0
  37. package/dist/component/localTime.d.ts.map +1 -0
  38. package/dist/component/localTime.js +126 -0
  39. package/dist/component/localTime.js.map +1 -0
  40. package/dist/component/reports.d.ts +20 -13
  41. package/dist/component/reports.d.ts.map +1 -1
  42. package/dist/component/reports.js +158 -38
  43. package/dist/component/reports.js.map +1 -1
  44. package/dist/component/retention.js +26 -8
  45. package/dist/component/retention.js.map +1 -1
  46. package/dist/component/rollupStore.d.ts +320 -0
  47. package/dist/component/rollupStore.d.ts.map +1 -0
  48. package/dist/component/rollupStore.js +596 -0
  49. package/dist/component/rollupStore.js.map +1 -0
  50. package/dist/component/rollups.d.ts +20 -0
  51. package/dist/component/rollups.d.ts.map +1 -0
  52. package/dist/component/rollups.js +73 -0
  53. package/dist/component/rollups.js.map +1 -0
  54. package/dist/component/schema.d.ts +311 -5
  55. package/dist/component/schema.js +9 -0
  56. package/dist/component/schema.js.map +1 -1
  57. package/dist/component/sites.d.ts +12 -0
  58. package/dist/component/sites.d.ts.map +1 -1
  59. package/dist/component/sites.js +30 -0
  60. package/dist/component/sites.js.map +1 -1
  61. package/dist/component/validators.d.ts +24 -5
  62. package/dist/component/validators.d.ts.map +1 -1
  63. package/dist/component/validators.js +16 -1
  64. package/dist/component/validators.js.map +1 -1
  65. package/dist/tracker/generated.d.ts +3 -0
  66. package/dist/tracker/generated.d.ts.map +1 -1
  67. package/dist/tracker/generated.js +3 -0
  68. package/dist/tracker/generated.js.map +1 -1
  69. package/docs/federation-setup.md +93 -26
  70. package/docs/federation.md +120 -30
  71. package/docs/upgrading.md +102 -17
  72. package/llms.txt +6 -5
  73. package/package.json +1 -1
  74. package/src/component/_generated/api.ts +6 -0
  75. package/src/component/_generated/component.ts +27 -12
  76. package/src/component/constants.ts +6 -0
  77. package/src/component/coverage.ts +41 -6
  78. package/src/component/geo.ts +82 -28
  79. package/src/component/http.ts +60 -26
  80. package/src/component/ingest.ts +226 -425
  81. package/src/component/localTime.ts +167 -0
  82. package/src/component/reports.ts +222 -39
  83. package/src/component/retention.ts +25 -7
  84. package/src/component/rollupStore.ts +799 -0
  85. package/src/component/rollups.ts +82 -0
  86. package/src/component/schema.ts +17 -0
  87. package/src/component/sites.ts +32 -0
  88. package/src/component/validators.ts +20 -1
package/README.md CHANGED
@@ -30,9 +30,11 @@ the [machine manifest](https://www.amagerastro.com/agent/manifest.json).
30
30
  Current evidence supports more than 140 million stored events/day for realistic
31
31
  traffic and more than 178 million for lean traffic. Near-limit heavy payloads
32
32
  are certified at 55.26 million/day on a data-heavy store (76.62 million/day on a
33
- fresh store, 2026-08-22). The feature-saturated profile currently fails
34
- certification on a known rollup-contention defect
35
- ([#52](https://github.com/amageweb/amage-rastro/issues/52)). Workload-specific
33
+ fresh store, 2026-08-22). The feature-saturated profile failed its last
34
+ certification on a rollup-contention defect
35
+ ([#52](https://github.com/amageweb/amage-rastro/issues/52)); the contended write
36
+ path no longer exists (report rollups are folded by one job per site, off the
37
+ ingestion transaction) and the profile awaits recertification. Workload-specific
36
38
  results and caveats are reported separately rather than collapsed into one
37
39
  capacity claim.
38
40
 
@@ -60,8 +62,11 @@ capacity claim.
60
62
  ### Prerequisites
61
63
 
62
64
  - Convex `^1.43.0` or a compatible Synapse deployment.
63
- - An existing host authentication provider for administrative and local report
64
- functions. The browser ingestion route itself is public and origin-checked.
65
+ - A way to authorize site administration and local report functions: the host's
66
+ existing authentication provider, or, for a site with no user accounts, a
67
+ fixed opaque owner ID and internal functions run with deployment credentials
68
+ (see [Hosts without user authentication](#hosts-without-user-authentication)).
69
+ The browser ingestion route itself is public and origin-checked.
65
70
  - Two deployment origins recorded separately:
66
71
 
67
72
  | Purpose | Convex Cloud example | Synapse example |
@@ -133,7 +138,9 @@ it does not prove that a browser event was accepted.
133
138
 
134
139
  Rastro uses Convex request metadata as the canonical client IP and can resolve
135
140
  coarse city-level coordinates on the first request of each session. GeoIP is
136
- opt-in because the configured provider receives the request IP. For IPinfo Core:
141
+ opt-in because the configured provider receives the request IP. Declare the
142
+ settings as optional host environment variables and pass them through, so each
143
+ deployment decides on its own provider, token, and cap without a code change:
137
144
 
138
145
  ```ts
139
146
  // convex/convex.config.ts
@@ -141,19 +148,47 @@ import { defineApp } from "convex/server";
141
148
  import { v } from "convex/values";
142
149
  import rastro from "@iann29/rastro/convex.config.js";
143
150
 
144
- const app = defineApp({ env: { IPINFO_TOKEN: v.string() } });
151
+ const app = defineApp({
152
+ env: {
153
+ RASTRO_GEOIP_PROVIDER: v.optional(
154
+ v.union(v.literal("ipinfo"), v.literal("ipwhois")),
155
+ ),
156
+ RASTRO_GEOIP_TOKEN: v.optional(v.string()),
157
+ RASTRO_GEOIP_DAILY_LIMIT: v.optional(v.string()),
158
+ RASTRO_TRUST_PROXY: v.optional(v.literal("true")),
159
+ },
160
+ });
145
161
  app.use(rastro, {
146
162
  httpPrefix: "/rastro/",
147
163
  env: {
148
- RASTRO_GEOIP_PROVIDER: "ipinfo",
149
- RASTRO_GEOIP_TOKEN: app.env.IPINFO_TOKEN,
150
- RASTRO_GEOIP_DAILY_LIMIT: "1000",
164
+ RASTRO_GEOIP_PROVIDER: app.env.RASTRO_GEOIP_PROVIDER,
165
+ RASTRO_GEOIP_TOKEN: app.env.RASTRO_GEOIP_TOKEN,
166
+ RASTRO_GEOIP_DAILY_LIMIT: app.env.RASTRO_GEOIP_DAILY_LIMIT,
167
+ RASTRO_TRUST_PROXY: app.env.RASTRO_TRUST_PROXY,
151
168
  },
152
169
  });
153
170
 
154
171
  export default app;
155
172
  ```
156
173
 
174
+ This is the shape of the [example host](example/convex/convex.config.ts). With
175
+ nothing set, provider lookups stay off. To enable IPinfo Core on one deployment:
176
+
177
+ ```bash
178
+ # Convex Cloud development deployment
179
+ npx convex env set --deployment dev RASTRO_GEOIP_PROVIDER ipinfo
180
+ npx convex env set --deployment dev RASTRO_GEOIP_TOKEN 'YOUR_IPINFO_TOKEN'
181
+ npx convex env set --deployment dev RASTRO_GEOIP_DAILY_LIMIT 1000
182
+
183
+ # Synapse development deployment
184
+ synapse convex --dev env set RASTRO_GEOIP_PROVIDER ipinfo
185
+ synapse convex --dev env set RASTRO_GEOIP_TOKEN 'YOUR_IPINFO_TOKEN'
186
+ synapse convex --dev env set RASTRO_GEOIP_DAILY_LIMIT 1000
187
+ ```
188
+
189
+ Binding a literal instead of a passthrough (`RASTRO_GEOIP_PROVIDER: "ipinfo"`)
190
+ is also valid when every deployment of the project shares the provider.
191
+
157
192
  `ipwhois` is also supported. Its token is optional for the documented
158
193
  1,000-request/day free endpoint and recommended for production. Bind it as
159
194
  `RASTRO_GEOIP_TOKEN` in the same way as the IPinfo example.
@@ -162,11 +197,11 @@ may be set from `0` through `1000000` to match the provider plan. Admission is
162
197
  transactional, so concurrent first-session requests cannot exceed the cap.
163
198
 
164
199
  When Rastro runs behind a reverse proxy that supplies `X-Forwarded-For` or
165
- Vercel/Cloudflare geographic headers, explicitly bind
166
- `RASTRO_TRUST_PROXY: "true"`. Leave it unset for direct Convex deployments.
167
- Rastro uses forwarded data only when this opt-in is enabled and the runtime IP
168
- identifies a private immediate peer. Caller-supplied proxy or geographic headers
169
- are ignored when native metadata is absent or public.
200
+ Vercel/Cloudflare geographic headers, set `RASTRO_TRUST_PROXY` to `true` on that
201
+ deployment. Leave it unset for direct Convex deployments. Rastro uses forwarded
202
+ data only when this opt-in is enabled and the runtime IP identifies a private
203
+ immediate peer. Caller-supplied proxy or geographic headers are ignored when
204
+ native metadata is absent or public.
170
205
 
171
206
  The lookup has a 1.5-second timeout and fails open: telemetry ingestion
172
207
  continues without location when the provider is unavailable. A session reserves
@@ -181,7 +216,13 @@ whether GeoIP is configured and the daily cap, without returning the address or
181
216
  token. Deployment administrators can exercise the configured provider through
182
217
  the internal `geo:probe` action. If health reports no usable client IP, use
183
218
  trusted Vercel/Cloudflare geographic headers with `RASTRO_TRUST_PROXY` or fix
184
- the deployment's proxy forwarding.
219
+ the deployment's proxy forwarding. Health also names the build the deployment
220
+ serves under `tracker`: the package `version` plus the content `hash` of
221
+ `tracker.js` and the `vitalsHash` of `vitals.js`. Both scripts carry that hash
222
+ as a strong `ETag`, answer `If-None-Match` with `304`, and are cached for five
223
+ minutes with up to one hour of `stale-while-revalidate`, so a returning visitor
224
+ runs a new build after at most one stale page load; an open tab keeps its script
225
+ until it reloads.
185
226
 
186
227
  ### Browser and device classification
187
228
 
@@ -275,6 +316,66 @@ event/byte budgets.
275
316
 
276
317
  Domain entries are hostnames, not host-and-port pairs. Use `localhost` or a full
277
318
  origin such as `http://localhost:3000`; both normalize to the `localhost` host.
319
+ A site deployed on Vercel needs its preview hosts too: list `*.vercel.app` next
320
+ to the production host, or preview deployments are rejected as
321
+ `ORIGIN_NOT_ALLOWED`.
322
+
323
+ #### Hosts without user authentication
324
+
325
+ An institutional site with no user accounts has nobody to authenticate. Use one
326
+ fixed opaque owner ID and internal functions, which only deployment credentials
327
+ can run:
328
+
329
+ ```ts
330
+ // convex/analyticsAdmin.ts
331
+ import { v } from "convex/values";
332
+ import { Rastro } from "@iann29/rastro";
333
+ import { components } from "./_generated/api";
334
+ import { internalMutation } from "./_generated/server";
335
+
336
+ const analytics = new Rastro(components.rastroAnalytics);
337
+
338
+ // Opaque and stable. Reuse the same value as `ownerId` when provisioning a
339
+ // federation grant.
340
+ const SITE_OWNER_ID = "acme-site-owner";
341
+
342
+ export const createSite = internalMutation({
343
+ args: {
344
+ name: v.string(),
345
+ domains: v.array(v.string()),
346
+ currency: v.string(),
347
+ },
348
+ returns: v.string(),
349
+ handler: async (ctx, args) =>
350
+ await analytics.createSite(ctx, {
351
+ ownerId: SITE_OWNER_ID,
352
+ name: args.name,
353
+ domains: args.domains,
354
+ currency: args.currency,
355
+ cookieless: true,
356
+ }),
357
+ });
358
+ ```
359
+
360
+ Run it once per site with the deployment's own credentials, never from a
361
+ browser:
362
+
363
+ ```bash
364
+ # Convex Cloud development deployment
365
+ npx convex run --deployment dev analyticsAdmin:createSite \
366
+ '{"name":"Acme","domains":["acme.example","*.vercel.app"],"currency":"BRL"}'
367
+
368
+ # Synapse development deployment
369
+ synapse convex --dev run analyticsAdmin:createSite \
370
+ '{"name":"Acme","domains":["acme.example","*.vercel.app"],"currency":"BRL"}'
371
+ ```
372
+
373
+ Such a host needs no local report functions: the central dashboard reads the
374
+ [federated surface](#federated-dashboard-backend), and its `auth.config.ts`
375
+ lists only the Rastro federation provider, exactly as the
376
+ [example host](example/convex/auth.config.ts) does. Goals, funnels, affiliates,
377
+ and retention policies are configured the same way, through `internalMutation`s
378
+ that call the `Rastro` class with `SITE_OWNER_ID`.
278
379
 
279
380
  ### 4. Add the tracker
280
381
 
@@ -306,6 +407,33 @@ serves the script and ingestion from different origins:
306
407
  ></script>
307
408
  ```
308
409
 
410
+ In a Next.js App Router layout, `next/script` injects the same tag;
411
+ `document.currentScript` is set when it runs, so `data-site` and the inferred
412
+ endpoint work exactly as with a plain tag:
413
+
414
+ ```tsx
415
+ // app/layout.tsx
416
+ import Script from "next/script";
417
+
418
+ export default function RootLayout({ children }) {
419
+ return (
420
+ <html lang="pt-BR">
421
+ <body>
422
+ {children}
423
+ <Script
424
+ src="https://YOUR_DEPLOYMENT.convex.site/rastro/tracker.js"
425
+ data-site="YOUR_SITE_ID"
426
+ strategy="afterInteractive"
427
+ />
428
+ </body>
429
+ </html>
430
+ );
431
+ }
432
+ ```
433
+
434
+ Remember the site's `domains`: a Vercel preview runs on `*.vercel.app`, not on
435
+ the production host.
436
+
309
437
  ### Field Web Vitals (optional)
310
438
 
311
439
  A second, separate module measures real-visitor LCP, CLS, INP, FCP, and TTFB
@@ -371,17 +499,25 @@ after a session starts would make the session ambiguous.
371
499
  seconds.
372
500
  4. Confirm `POST /rastro/events` returns HTTP 200 and a response containing
373
501
  `accepted`, `duplicates`, and `rejected` counters.
374
- 5. Run one authenticated local report, such as `analytics:overview`, for the
375
- returned `siteId` and a complete UTC hour/day range.
502
+ 5. Run `analytics:liveVisitors` (with `siteIds` and `now`) or
503
+ `analytics:sessionJourney` for the returned `siteId`: both reflect the
504
+ request as soon as it is accepted. `analytics:listSessions` and the
505
+ aggregate-backed `analytics:overview` can trail the live view by a few
506
+ seconds, and the overview only reports complete UTC hour/day buckets, so an
507
+ empty result from those two right after the first request is not a failed
508
+ ingestion.
376
509
 
377
510
  The tracker intentionally does not interrupt the product UI when transport
378
511
  fails. A loaded `tracker.js` or successful `/health` response alone is not proof
379
512
  of ingestion. The usual causes of a rejected or missing request are:
380
513
 
381
514
  - using the functions URL instead of the HTTP-actions URL;
382
- - omitting the exact browser host from the site's `domains`;
515
+ - omitting the exact browser host from the site's `domains`, including
516
+ `*.vercel.app` for preview deployments;
383
517
  - including query strings or paths in a configured base URL;
384
- - querying a partial UTC bucket or a site the authenticated user does not own.
518
+ - querying a partial UTC bucket or a site the authenticated user does not own;
519
+ - reading `listSessions` or `overview` seconds after the first request instead
520
+ of `liveVisitors`.
385
521
 
386
522
  ## Track custom events
387
523
 
@@ -546,16 +682,22 @@ while telemetry remains in the customer's deployment. It uses
546
682
  `exposeFederatedAnalyticsApi` with issuer and connection-resolver options. The
547
683
  helper constructs its authorization boundary internally; it cannot accept a
548
684
  permissive host authorizer. Its surface contains a public protocol v1 manifest,
549
- authenticated redacted connection status, redacted site summaries, and only the
550
- reactive dashboard report queries.
685
+ authenticated redacted connection status, redacted site summaries, the reactive
686
+ dashboard report queries, and — behind the optional `configure` capability — the
687
+ functions that manage goals, funnels, affiliates, a site's name, domains and
688
+ timezone, and the retention policy from the dashboard.
551
689
 
552
690
  The JWT only identifies the requested connection. Site access remains
553
691
  authoritative in a local host record resolved on every query, so revocation is
554
- immediate and a token cannot grant itself another site. `listSites` accepts no
555
- site arguments and omits owner, domain, and network configuration. Stable
556
- manifest names, capabilities, limits, and authorization error codes are exported
557
- for connector clients. The host must already have Convex authentication and a
558
- local authoritative grant. Follow
692
+ immediate and a token cannot grant itself another site. The same record decides
693
+ whether the dashboard may configure: a configure function runs only when the
694
+ token carries `analytics:configure` (the control plane claims it for
695
+ organization owners and admins alone) and the host's grant lists it too;
696
+ otherwise the dashboard shows the same forms but hands over the code the host
697
+ runs itself. `listSites` accepts no site arguments and omits owner, domain, and
698
+ network configuration. Stable manifest names, capabilities, limits, and
699
+ authorization error codes are exported for connector clients. The host must
700
+ already have Convex authentication and a local authoritative grant. Follow
559
701
  [`docs/federation-setup.md`](docs/federation-setup.md) for the executable setup
560
702
  and verification sequence; use [`docs/federation.md`](docs/federation.md) as the
561
703
  protocol reference. The production control plane is available at
@@ -602,18 +744,35 @@ returns. When storage is blocked or an old cached tracker reports no id,
602
744
  sessions from the same device and network within one UTC day still fold into one
603
745
  visitor through a salted daily key; runtimes without a client IP fall back to
604
746
  one visitor per session, which `/rastro/health` reports as
605
- `uniqueVisitors: "session"` instead of `"dailyHash"`. Breakdown metadata
606
- likewise identifies page, source, country, device, browser, and event rankings
607
- as event-volume counts. Feature status is explicit: outbound-link event volume
608
- is supported, bot detection rejects self-declared agents by `User-Agent`, and
609
- session replay and error insights are currently unsupported.
747
+ `uniqueVisitors: "session"` instead of `"dailyHash"`. Breakdown metadata says
748
+ what each ranking counts: pages by pageviews and sources, campaigns, countries,
749
+ devices, and browsers by sessions once every bucket in the range carries those
750
+ counters (recorded since alpha.11), and raw event volume for ranges that reach
751
+ back before them; events always rank by volume. Totals carry the engagement
752
+ counters recorded since the same stamp — summed session duration, sessions that
753
+ reached a second pageview, and first-ever sessions of a visitor — and
754
+ `metadata.engagement` reports `counted` when the whole range has them, so bounce
755
+ rate, average duration, pages per session, and the share of new visitors derive
756
+ from one response; `unavailable` means the range reaches back before the
757
+ counters and those four stay blank. Feature status is explicit: outbound-link
758
+ event volume is supported, bot detection rejects self-declared agents by
759
+ `User-Agent`, and session replay and error insights are currently unsupported.
610
760
 
611
761
  All report ranges use integer Unix milliseconds with inclusive `from` and `to`
612
- boundaries. Range metadata reports `boundary: "inclusive"` and
613
- `timezone: "UTC"`; the site's timezone is presentation metadata and never
614
- changes report bucket boundaries. Overview queries must contain complete UTC
615
- hours or days, and funnel and affiliate reports must contain complete UTC days.
616
- Those aggregate-backed reports fail with `REPORT_INCOMPLETE` instead of silently
762
+ boundaries, and range metadata reports `boundary: "inclusive"`. Hourly overview
763
+ queries must contain complete UTC hours. A daily overview query must contain
764
+ either complete UTC days or, when every requested site has the same timezone,
765
+ complete calendar days in that zone: a site with a timezone gets one aggregate
766
+ bucket per local calendar day at ingestion, next to the UTC hour and day
767
+ buckets, so "yesterday" in São Paulo ends at midnight in São Paulo. Range
768
+ metadata then reports `timezone` as the zone the buckets follow, `"UTC"`
769
+ otherwise. Local-day coverage starts at the site's first ingest after the
770
+ upgrade (`analyticsCoverage.localDaySince`) and restarts when its timezone
771
+ changes; a range reaching further back is reported as partial coverage, and
772
+ `sites.localDays` (federated `listSites.localDays`) tells a dashboard the zone
773
+ and the stamp up front. A zone that never leaves UTC keeps no extra buckets.
774
+ Funnel and affiliate reports must contain complete UTC days. Those
775
+ aggregate-backed reports fail with `REPORT_INCOMPLETE` instead of silently
617
776
  including the rest of a partial bucket. Goal reports retain exact millisecond
618
777
  filtering while bounded goal-completion rows are available.
619
778
 
@@ -647,6 +806,13 @@ boundaries use different bucket alignment.
647
806
  - Primary aggregate shards: 256 hourly and 128 daily, derived from stable
648
807
  session identity
649
808
  - Funnel and affiliate aggregate shards: 16
809
+ - Report rollups (goal, funnel, affiliate, route, and Web Vitals daily rows) are
810
+ never written by the ingestion transaction: a batch appends one document to
811
+ the site's rollup queue and one leased fold job per site merges up to 200
812
+ queued batches per run into the rows, so every rollup row has a single writer
813
+ and concurrent ingestion cannot conflict on it. Reports see a batch's rollups
814
+ a few hundred milliseconds after ingestion accepts it; a fold chain that dies
815
+ is restarted by the next batch once its 60-second lease lapses
650
816
  - Site ingress budget: 4,096 deterministic per-minute shards, each capped at 300
651
817
  events and 512 KiB (1,228,800 admitted events/minute at uniform load)
652
818
  - Rate-window storage: one reusable row per active session and site shard
@@ -705,12 +871,13 @@ evidence, not a production SLA.
705
871
  Two caveats stand deliberately. The heavy row measured a store already holding
706
872
  the battery's own multi-million-event history; the 2026-08-22 fresh-store result
707
873
  (76.62M/day) remains valid for that state, and neither number overwrites the
708
- other. The feature profile cannot currently be certified at all: affiliate
709
- daily-rollup contention loses ~4–5% of batches under concurrent load
710
- ([#52](https://github.com/amageweb/amage-rastro/issues/52)), so no
711
- feature-profile capacity is claimed until the fix lands. GeoIP with a provider
712
- enabled, trusted server-side conversions, and vitals-bearing traffic remain
713
- separate pending certifications.
874
+ other. The feature profile is not certified: on alpha.6, affiliate daily-rollup
875
+ contention lost ~4–5% of batches under concurrent load
876
+ ([#52](https://github.com/amageweb/amage-rastro/issues/52)). Since alpha.11
877
+ ingestion no longer writes rollup rows at all (see the operational limits), but
878
+ no feature-profile capacity is claimed until a new battery certifies it. GeoIP
879
+ with a provider enabled, trusted server-side conversions, and vitals-bearing
880
+ traffic remain separate pending certifications.
714
881
 
715
882
  The full evidence is recorded in
716
883
  [`docs/benchmarks/2026-08-30-alpha6-recertification.md`](docs/benchmarks/2026-08-30-alpha6-recertification.md).
@@ -772,9 +939,11 @@ site-shard counters, session/live state, visitor attribution, and exact funnel
772
939
  progress before writing. Accepted non-heartbeat telemetry from one request is
773
940
  grouped into one immutable raw document per session and event-minute, without
774
941
  duplicating session geo or user-agent context. A 50-event same-session,
775
- same-minute baseline stays within 10 document writes. Each touched aggregate
776
- bucket is written at most once per batch, and dimension overflow folds into an
777
- explicit `(other)` bucket instead of relabeling old counts.
942
+ same-minute baseline stays within 10 document writes plus the rollup queue entry
943
+ and its fold lease. Each touched aggregate bucket is written at most once per
944
+ batch, and dimension overflow folds into an explicit `(other)` bucket instead of
945
+ relabeling old counts. Goal, funnel, affiliate, route, and Web Vitals rollup
946
+ rows are written only by the site's fold job, never by ingestion.
778
947
  Click/outbound/browser-conversion telemetry skips goal and funnel work that
779
948
  cannot match those event types.
780
949