@iann29/rastro 0.1.0-alpha.0 → 0.1.0-alpha.10
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 +579 -57
- package/agent/integration.md +661 -0
- package/agent/manifest.json +187 -0
- package/agent/manifest.schema.json +411 -0
- package/dist/client/federation.d.ts +236 -0
- package/dist/client/federation.d.ts.map +1 -0
- package/dist/client/federation.js +196 -0
- package/dist/client/federation.js.map +1 -0
- package/dist/client/index.d.ts +2467 -18
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +461 -32
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +18 -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 +302 -4
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +4 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/affiliates.d.ts.map +1 -1
- package/dist/component/affiliates.js +6 -2
- package/dist/component/affiliates.js.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 +34 -7
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +47 -7
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +6 -1
- package/dist/component/convex.config.js +9 -1
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/coverage.d.ts +19 -0
- package/dist/component/coverage.d.ts.map +1 -0
- package/dist/component/coverage.js +30 -0
- package/dist/component/coverage.js.map +1 -0
- package/dist/component/diagnostics.d.ts +9 -0
- package/dist/component/diagnostics.d.ts.map +1 -0
- package/dist/component/diagnostics.js +47 -0
- package/dist/component/diagnostics.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 +106 -0
- package/dist/component/eventStore.d.ts.map +1 -0
- package/dist/component/eventStore.js +294 -0
- package/dist/component/eventStore.js.map +1 -0
- package/dist/component/funnels.d.ts.map +1 -1
- package/dist/component/funnels.js +5 -3
- package/dist/component/funnels.js.map +1 -1
- package/dist/component/geo.d.ts +71 -0
- package/dist/component/geo.d.ts.map +1 -0
- package/dist/component/geo.js +611 -0
- package/dist/component/geo.js.map +1 -0
- package/dist/component/goals.d.ts.map +1 -1
- package/dist/component/goals.js +25 -9
- package/dist/component/goals.js.map +1 -1
- package/dist/component/guards.d.ts.map +1 -1
- package/dist/component/guards.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +284 -53
- 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 +3 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +1105 -243
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts +8 -0
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +74 -9
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +273 -24
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +1320 -150
- 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 +541 -31
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts +24 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +107 -19
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +462 -55
- package/dist/component/schema.js +195 -15
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +38 -25
- 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 +101 -16
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +19 -2
- 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/component/vitals.d.ts +41 -0
- package/dist/component/vitals.d.ts.map +1 -0
- package/dist/component/vitals.js +115 -0
- package/dist/component/vitals.js.map +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/tracker/generated.d.ts +8 -4
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +8 -4
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.d.ts +1 -1
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +81 -33
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker/vitals.d.ts +10 -0
- package/dist/tracker/vitals.d.ts.map +1 -0
- package/dist/tracker/vitals.js +140 -0
- package/dist/tracker/vitals.js.map +1 -0
- package/dist/tracker.min.js +1 -1
- package/dist/vitals.min.js +1 -0
- package/docs/benchmarks/2026-08-20-realistic.md +171 -0
- package/docs/benchmarks/2026-08-21-formal-certification.md +353 -0
- package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
- package/docs/federation-setup.md +397 -0
- package/docs/federation.md +262 -0
- package/docs/upgrading.md +259 -0
- package/llms.txt +71 -0
- package/package.json +61 -11
- package/scripts/benchmark-ingest.mjs +601 -0
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +18 -0
- package/src/component/_generated/component.ts +366 -4
- package/src/component/_generated/server.ts +4 -0
- package/src/component/affiliates.ts +20 -5
- package/src/component/cardinality.ts +117 -0
- package/src/component/constants.ts +47 -7
- package/src/component/convex.config.ts +11 -1
- package/src/component/coverage.ts +36 -0
- package/src/component/diagnostics.ts +65 -0
- package/src/component/errors.ts +2 -1
- package/src/component/eventStore.ts +489 -0
- package/src/component/funnels.ts +19 -16
- package/src/component/geo.ts +781 -0
- package/src/component/goals.ts +45 -16
- package/src/component/guards.ts +3 -1
- package/src/component/http.ts +403 -60
- package/src/component/identity.ts +74 -0
- package/src/component/ingest.ts +1652 -291
- package/src/component/live.ts +90 -9
- package/src/component/reports.ts +1929 -199
- package/src/component/retention.ts +774 -97
- package/src/component/sanitize.ts +154 -32
- package/src/component/schema.ts +219 -15
- package/src/component/sites.ts +56 -28
- package/src/component/useragent.ts +171 -0
- package/src/component/validators.ts +29 -1
- package/src/component/visitors.ts +116 -0
- package/src/component/vitals.ts +146 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Ingest recertification, 2026-08-30
|
|
2
|
+
|
|
3
|
+
This report recertifies the four workload profiles ahead of `0.1.0-alpha.6`,
|
|
4
|
+
against the same deployment and benchmark site as the
|
|
5
|
+
[2026-08-21 formal certification](2026-08-21-formal-certification.md). It is
|
|
6
|
+
engineering evidence for the tested revisions and deployment state, not a
|
|
7
|
+
production SLA.
|
|
8
|
+
|
|
9
|
+
## Environment
|
|
10
|
+
|
|
11
|
+
| Item | Value |
|
|
12
|
+
| ------------------- | ---------------------------------------------------------------- |
|
|
13
|
+
| Deployment | DEV canary `fast-finch-7454` (unchanged since 2026-08-21) |
|
|
14
|
+
| Benchmark site | `kx75pnr9wap522n0zmh4nnb4jh8cv110` (unchanged) |
|
|
15
|
+
| Lean run revision | `ba9eff1` (main before Web Vitals) |
|
|
16
|
+
| Later run revisions | Web Vitals build; component sources identical to merge `6250526` |
|
|
17
|
+
| Driver | One local Node.js process |
|
|
18
|
+
| Reconciliation | Exact run-scoped session IDs, indexed pages of 100 |
|
|
19
|
+
| GeoIP provider | Disabled |
|
|
20
|
+
| Health before runs | `ok: true` |
|
|
21
|
+
|
|
22
|
+
Since the 2026-08-21 certification the wire path gained salted daily visitor
|
|
23
|
+
keys, User-Agent bot rejection, client-hint browser detection, the federation
|
|
24
|
+
hardening, and the `vital` ingest branch (present but idle: no profile sends
|
|
25
|
+
vitals). Unlike 2026-08-21, the canary was not fresh: it accumulated the
|
|
26
|
+
battery's own multi-million-event history while the profiles ran, which is the
|
|
27
|
+
honest way most deployments look after a real day.
|
|
28
|
+
|
|
29
|
+
All certified runs used `--duration 600 --certify`, ran sequentially with at
|
|
30
|
+
least five minutes of cooldown, and had no competing local load. Two runs were
|
|
31
|
+
discarded and repeated because the local DNS resolver failed their first
|
|
32
|
+
connection wave (a client-side condition — the deployment was idle and healthy
|
|
33
|
+
each time); one more was discarded after a mid-run deploy from this same
|
|
34
|
+
machine. Discarded runs are not reported as capacity.
|
|
35
|
+
|
|
36
|
+
## Results
|
|
37
|
+
|
|
38
|
+
Meeting the 100 million stored-events/day target requires at least 1,157.41
|
|
39
|
+
stored events/second.
|
|
40
|
+
|
|
41
|
+
| Profile | Concurrency | Batch | Sessions | Accepted | Stored | Stored/s | Certified stored/day | p95 | Exact reconciliation | 100M/day target |
|
|
42
|
+
| --------- | ----------: | ----: | -------: | --------: | --------: | -------: | -------------------: | -------: | --------------------- | ---------------- |
|
|
43
|
+
| Lean | 100 | 50 | unique | 1,247,000 | 1,247,000 | 2,071 | 178,948,552 | 4,167 ms | 1,247,000 / 1,247,000 | Pass |
|
|
44
|
+
| Realistic | 75 | 50 | 4,096 | 1,039,050 | 988,995 | 1,644 | 142,036,529 | 4,953 ms | 988,995 / 988,995 | Pass |
|
|
45
|
+
| Heavy | 100 | 20 | 2,048 | 388,040 | 388,040 | 640 | 55,259,642 | 7,702 ms | 388,040 / 388,040 | **Below target** |
|
|
46
|
+
| Feature | 100 | 15 | 2,048 | — | — | — | **failed** | — | — | **Failed** |
|
|
47
|
+
|
|
48
|
+
### Lean
|
|
49
|
+
|
|
50
|
+
Certified by the harness: 24,940 requests, zero failures, duplicates, or
|
|
51
|
+
rejections; exact reconciliation. 178.95M stored events/day is 31% above the
|
|
52
|
+
2026-08-21 result (136.65M) on the same deployment — the byte-measurement reuse
|
|
53
|
+
first recertified for heavy on 2026-08-22 now shows up across the board, and the
|
|
54
|
+
new per-request work (bot check, client hints, salted visitor keys) did not cost
|
|
55
|
+
the hot path its headroom.
|
|
56
|
+
|
|
57
|
+
### Realistic
|
|
58
|
+
|
|
59
|
+
The driver's session pool was doubled to 4,096 (default 2,048) and that change
|
|
60
|
+
is part of this row's workload definition. At the current deployment speed the
|
|
61
|
+
2,048-session pool drives individual sessions past the documented 120
|
|
62
|
+
events/session/minute ingest budget, so a prior attempt lost 2 of 20,477
|
|
63
|
+
requests to `429 RATE_LIMITED` — the limiter working as documented, but not a
|
|
64
|
+
clean certification. With 4,096 sessions the run completed 20,781 requests with
|
|
65
|
+
**zero failures** of any kind.
|
|
66
|
+
|
|
67
|
+
The harness itself did not stamp this run: after the clean 600 s window, its
|
|
68
|
+
first reconciliation query lost a fetch to the local resolver
|
|
69
|
+
(`reconciliation:fetch failed`) and gave up. Reconciliation was re-run
|
|
70
|
+
immediately afterwards with the same `benchmark:reconcileSessionsPage` pages the
|
|
71
|
+
harness uses, with per-page retries:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"runId": "3c81abe6-60cd-4b40-828f-cedf03fedd39",
|
|
76
|
+
"requested": 4096,
|
|
77
|
+
"found": 4096,
|
|
78
|
+
"stored": 988995,
|
|
79
|
+
"expectedStored": 988995
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
988,995 expected stored events (1,039,050 accepted − 50,055 heartbeats), 988,995
|
|
84
|
+
found — exact. 988,995 stored over the 601.6 s window is 1,644 stored/s, 142.04M
|
|
85
|
+
stored events/day, 17.6% above the 2026-08-21 result (120.81M).
|
|
86
|
+
|
|
87
|
+
### Heavy
|
|
88
|
+
|
|
89
|
+
Certified by the harness: 19,402 requests, zero failures, exact reconciliation,
|
|
90
|
+
1,990,190 request bytes/second under the 64 KiB body limit. 55.26M stored
|
|
91
|
+
events/day is 28% below the 2026-08-22 fresh-store recertification (76.62M). The
|
|
92
|
+
payload and validation are unchanged; the store was not. This run followed hours
|
|
93
|
+
of sustained battery load on a canary holding millions of freshly written rows,
|
|
94
|
+
where 2026-08-22 hit a much lighter store. Both numbers stand side by side:
|
|
95
|
+
76.62M remains the fresh-store ceiling, 55.26M is the same workload on a loaded
|
|
96
|
+
store. Neither is overwritten by the other without a matched-state rerun.
|
|
97
|
+
|
|
98
|
+
### Feature — certification FAILED (reproducible defect)
|
|
99
|
+
|
|
100
|
+
The feature profile (goals, funnels, and affiliate attribution all matching)
|
|
101
|
+
cannot currently be certified: a steady ~4–5% of requests fail with
|
|
102
|
+
`500 INTERNAL_ERROR` at every concurrency tried (100, 15, and 5), and each
|
|
103
|
+
failed request loses its whole batch — reconciliation of a 60 s smoke found only
|
|
104
|
+
946 of 997 run sessions, so this is data loss under load. Backend logs show OCC
|
|
105
|
+
retry exhaustion on the affiliate daily rollups, always on the same two
|
|
106
|
+
documents — the two `REPORT_ROLLUP_SHARDS` rows for the demo affiliate's day:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
[CONVEX M(ingest:ingestBatch)] Documents read from or written to the
|
|
110
|
+
"affiliateDailyRollups" table changed while this mutation was being run and on
|
|
111
|
+
every subsequent retry. Another call to this mutation changed the document
|
|
112
|
+
with ID "mh735zse9410dt27ftc8t3tv4s8deazf".
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The defect postdates alpha.3 (the 2026-08-21 feature run predates the
|
|
116
|
+
generation-1 daily rollups and passed clean at 384 stored/s) and predates the
|
|
117
|
+
Web Vitals change, whose fold path is separate. It is tracked as
|
|
118
|
+
[issue #52](https://github.com/amageweb/amage-rastro/issues/52); the fix needs
|
|
119
|
+
wider rollup sharding, rebalanced feature-report read budgets, and a
|
|
120
|
+
rollout-generation bump. Until it lands, no feature-profile capacity is claimed
|
|
121
|
+
at all — the 2026-08-21 33.22M row is historical evidence for alpha.3 only.
|
|
122
|
+
|
|
123
|
+
## Certified run JSON
|
|
124
|
+
|
|
125
|
+
### Lean (`cert-lean-alpha5`, run `862673e3-ff44-492e-a078-5d8c33f70101`)
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"requests": { "started": 24940, "completed": 24940, "failures": 0 },
|
|
130
|
+
"events": {
|
|
131
|
+
"sent": 1247000,
|
|
132
|
+
"heartbeats": 0,
|
|
133
|
+
"accepted": 1247000,
|
|
134
|
+
"stored": 1247000,
|
|
135
|
+
"duplicates": 0,
|
|
136
|
+
"rejected": 0,
|
|
137
|
+
"failed": 0
|
|
138
|
+
},
|
|
139
|
+
"throughput": {
|
|
140
|
+
"storedEventsPerSecond": 2071,
|
|
141
|
+
"certifiedStoredEventsPerDay": 178948552,
|
|
142
|
+
"requestBytesPerSecond": 575501
|
|
143
|
+
},
|
|
144
|
+
"latencyMs": { "p50": 2156, "p95": 4167, "p99": 5925, "max": 7189 },
|
|
145
|
+
"reconciliation": {
|
|
146
|
+
"expectedStoredEvents": 1247000,
|
|
147
|
+
"storedEvents": 1247000,
|
|
148
|
+
"matches": true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Heavy (`cert-heavy-alpha5-r2`)
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"requests": { "started": 19402, "completed": 19402, "failures": 0 },
|
|
158
|
+
"events": {
|
|
159
|
+
"sent": 388040,
|
|
160
|
+
"accepted": 388040,
|
|
161
|
+
"stored": 388040,
|
|
162
|
+
"duplicates": 0,
|
|
163
|
+
"rejected": 0,
|
|
164
|
+
"failed": 0
|
|
165
|
+
},
|
|
166
|
+
"throughput": {
|
|
167
|
+
"storedEventsPerSecond": 640,
|
|
168
|
+
"certifiedStoredEventsPerDay": 55259642,
|
|
169
|
+
"requestBytesPerSecond": 1990190
|
|
170
|
+
},
|
|
171
|
+
"latencyMs": { "p50": 2545, "p95": 7702, "p99": 10325, "max": 11220 },
|
|
172
|
+
"reconciliation": {
|
|
173
|
+
"expectedStoredEvents": 388040,
|
|
174
|
+
"storedEvents": 388040,
|
|
175
|
+
"matches": true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Realistic (`cert-realistic-alpha5-r3-a1`, run `3c81abe6-60cd-4b40-828f-cedf03fedd39`)
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"requests": { "started": 20781, "completed": 20781, "failures": 0 },
|
|
185
|
+
"events": {
|
|
186
|
+
"sent": 1039050,
|
|
187
|
+
"heartbeats": 50055,
|
|
188
|
+
"accepted": 1039050,
|
|
189
|
+
"duplicates": 0,
|
|
190
|
+
"rejected": 0,
|
|
191
|
+
"failed": 0
|
|
192
|
+
},
|
|
193
|
+
"throughput": {
|
|
194
|
+
"eventsPerSecond": 1727,
|
|
195
|
+
"requestBytesPerSecond": 523501
|
|
196
|
+
},
|
|
197
|
+
"latencyMs": { "p50": 1790, "p95": 4953, "p99": 7096, "max": 9274 },
|
|
198
|
+
"reconciliation": "lost to a client-side DNS failure; re-run exactly, see above"
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## What still lacks certification
|
|
203
|
+
|
|
204
|
+
GeoIP with a provider enabled, trusted server-side conversions, vitals-bearing
|
|
205
|
+
traffic (the `vital` ingest branch ran idle here), and the feature profile until
|
|
206
|
+
issue #52 is fixed.
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# Federated dashboard setup
|
|
2
|
+
|
|
3
|
+
This runbook connects an existing Convex or Synapse project to the Amage Rastro
|
|
4
|
+
control plane without moving telemetry out of that project. Read
|
|
5
|
+
[`federation.md`](federation.md) for the protocol and threat model.
|
|
6
|
+
|
|
7
|
+
## Package eligibility requirement
|
|
8
|
+
|
|
9
|
+
Do not infer federation availability from this website, a dist-tag, or a source
|
|
10
|
+
checkout. Discover the current tag without changing the host:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm view @iann29/rastro@alpha version
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
After the operator approves that exact version, execute the complete static
|
|
17
|
+
tarball, export-map, AST, and TypeScript consumer probe in the **Package
|
|
18
|
+
eligibility gate** of the canonical
|
|
19
|
+
[`agent/integration.md`](https://www.amagerastro.com/agent/integration.md). Do
|
|
20
|
+
not replace it with an `import()` test, which executes package code, or a text
|
|
21
|
+
search, which can accept comments and broken re-exports.
|
|
22
|
+
|
|
23
|
+
If verification fails, stop and request an eligible release. If it passes,
|
|
24
|
+
record that exact version but do not install it yet. Detect the currently
|
|
25
|
+
installed version with `npm ls @iann29/rastro --depth=0`, then follow
|
|
26
|
+
[`upgrading.md`](upgrading.md): snapshot/rehearse maintained deployments and
|
|
27
|
+
complete every required migration bridge. Only then install with
|
|
28
|
+
`npm install --ignore-scripts --save-exact @iann29/rastro@EXACT_VERSION` and
|
|
29
|
+
commit the lockfile. Never fetch a mutable branch or install a source tarball
|
|
30
|
+
supplied by this website.
|
|
31
|
+
|
|
32
|
+
## 1. Verify local analytics first
|
|
33
|
+
|
|
34
|
+
Do not configure federation until the customer deployment already has:
|
|
35
|
+
|
|
36
|
+
- the Rastro component mounted under `/rastro/`;
|
|
37
|
+
- at least one site with the correct allowed browser domains;
|
|
38
|
+
- a tracker using the deployment's HTTP-actions URL;
|
|
39
|
+
- an accepted `POST /rastro/events` request;
|
|
40
|
+
- a protected local report returning that site's telemetry.
|
|
41
|
+
- an audit through authorized local reports of retained routes, visitor/session
|
|
42
|
+
IDs, live location, campaigns, affiliates, conversions, and custom properties,
|
|
43
|
+
proving that no unexpected personal data, credentials, secrets, or form values
|
|
44
|
+
will become visible to the intended dashboard organization members and that
|
|
45
|
+
retention is acceptable.
|
|
46
|
+
|
|
47
|
+
The README quickstart covers those steps. Federation only exposes existing
|
|
48
|
+
reports; it does not install the tracker or create sites.
|
|
49
|
+
|
|
50
|
+
## 2. Record both deployment URLs
|
|
51
|
+
|
|
52
|
+
The connection form requires two different base URLs with no path, query, hash,
|
|
53
|
+
username, or password:
|
|
54
|
+
|
|
55
|
+
| Field | Used for | Convex Cloud example | Synapse example |
|
|
56
|
+
| -------------- | --------------------------------- | ------------------------------ | -------------------------------------- |
|
|
57
|
+
| URL de funções | Reactive queries and JWT audience | `https://product.convex.cloud` | `https://product.synapse.example` |
|
|
58
|
+
| URL HTTP | Tracker and event ingestion | `https://product.convex.site` | `https://product.site.synapse.example` |
|
|
59
|
+
|
|
60
|
+
Custom domains are valid when they route to the corresponding Convex surface.
|
|
61
|
+
The control-plane operator must add each custom functions origin to
|
|
62
|
+
`RASTRO_FEDERATION_ALLOWED_DEPLOYMENT_ORIGINS` as an exact, comma-separated
|
|
63
|
+
HTTPS origin before it can be registered. Do not derive one URL by appending
|
|
64
|
+
paths to the other or derive the allowlist from browser input. If the dashboard
|
|
65
|
+
returns `UNTRUSTED_DEPLOYMENT_HOST`, stop and ask the Amage Rastro operator who
|
|
66
|
+
issued the organization access to add the exact functions origin. Provide only
|
|
67
|
+
that public HTTPS origin, never credentials; retry after the operator confirms
|
|
68
|
+
the allowlist update.
|
|
69
|
+
|
|
70
|
+
## 3. Create the pending connection
|
|
71
|
+
|
|
72
|
+
In the central dashboard:
|
|
73
|
+
|
|
74
|
+
1. Sign in and select the intended organization.
|
|
75
|
+
2. Open **Conexões**.
|
|
76
|
+
3. Enter a recognizable name, the functions URL, and the HTTP URL.
|
|
77
|
+
4. Create the connection, but do not verify it yet.
|
|
78
|
+
5. Copy the displayed issuer, audience, connection ID, and organization ID.
|
|
79
|
+
|
|
80
|
+
The audience must exactly equal the normalized functions URL. The connection
|
|
81
|
+
remains pending until its local grant is installed and verification succeeds.
|
|
82
|
+
|
|
83
|
+
## 4. Add the authoritative local grant
|
|
84
|
+
|
|
85
|
+
Before editing, inventory every existing schema table/index, component mount/env
|
|
86
|
+
declaration, and authentication provider. After editing, prove each remains in
|
|
87
|
+
the diff and typechecks. The snippets in this runbook are insertion fragments,
|
|
88
|
+
not replacement files.
|
|
89
|
+
|
|
90
|
+
Insert this table property into the pilot project's existing
|
|
91
|
+
`defineSchema({ ... })` object:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
rastroFederationGrants: defineTable({
|
|
95
|
+
connectionId: v.string(),
|
|
96
|
+
organizationId: v.string(),
|
|
97
|
+
siteIds: v.array(v.string()),
|
|
98
|
+
revokedAt: v.optional(v.number()),
|
|
99
|
+
createdAt: v.number(),
|
|
100
|
+
updatedAt: v.number(),
|
|
101
|
+
}).index("by_connectionId", ["connectionId"]),
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The grant, not the JWT, owns the allowed `siteIds`. Add operator-only
|
|
105
|
+
provisioning functions:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// convex/rastroFederationAdmin.ts
|
|
109
|
+
import { ConvexError, v } from "convex/values";
|
|
110
|
+
import { Rastro } from "@iann29/rastro";
|
|
111
|
+
import { components } from "./_generated/api";
|
|
112
|
+
import { internalMutation } from "./_generated/server";
|
|
113
|
+
|
|
114
|
+
const rastro = new Rastro(components.rastroAnalytics);
|
|
115
|
+
|
|
116
|
+
export const provisionConnection = internalMutation({
|
|
117
|
+
args: {
|
|
118
|
+
connectionId: v.string(),
|
|
119
|
+
organizationId: v.string(),
|
|
120
|
+
ownerId: v.string(),
|
|
121
|
+
siteIds: v.array(v.string()),
|
|
122
|
+
},
|
|
123
|
+
returns: v.null(),
|
|
124
|
+
handler: async (ctx, args) => {
|
|
125
|
+
const siteIds = [...new Set(args.siteIds)];
|
|
126
|
+
if (
|
|
127
|
+
args.connectionId.length === 0 ||
|
|
128
|
+
args.connectionId.length > 128 ||
|
|
129
|
+
args.organizationId.length === 0 ||
|
|
130
|
+
args.organizationId.length > 128 ||
|
|
131
|
+
siteIds.length === 0 ||
|
|
132
|
+
siteIds.length > 10 ||
|
|
133
|
+
siteIds.length !== args.siteIds.length
|
|
134
|
+
) {
|
|
135
|
+
throw new ConvexError({ code: "INVALID_FEDERATION_GRANT" });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const siteId of siteIds) {
|
|
139
|
+
const site = await rastro.getSite(ctx, siteId);
|
|
140
|
+
if (!site || site.ownerId !== args.ownerId) {
|
|
141
|
+
throw new ConvexError({ code: "FEDERATION_SITE_NOT_OWNED" });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const existing = await ctx.db
|
|
146
|
+
.query("rastroFederationGrants")
|
|
147
|
+
.withIndex("by_connectionId", (query) =>
|
|
148
|
+
query.eq("connectionId", args.connectionId),
|
|
149
|
+
)
|
|
150
|
+
.unique();
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const grant = {
|
|
153
|
+
connectionId: args.connectionId,
|
|
154
|
+
organizationId: args.organizationId,
|
|
155
|
+
siteIds,
|
|
156
|
+
revokedAt: undefined,
|
|
157
|
+
updatedAt: now,
|
|
158
|
+
};
|
|
159
|
+
if (existing) {
|
|
160
|
+
await ctx.db.patch("rastroFederationGrants", existing._id, grant);
|
|
161
|
+
} else {
|
|
162
|
+
await ctx.db.insert("rastroFederationGrants", {
|
|
163
|
+
...grant,
|
|
164
|
+
createdAt: now,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const revokeConnection = internalMutation({
|
|
172
|
+
args: { connectionId: v.string() },
|
|
173
|
+
returns: v.null(),
|
|
174
|
+
handler: async (ctx, args) => {
|
|
175
|
+
const grant = await ctx.db
|
|
176
|
+
.query("rastroFederationGrants")
|
|
177
|
+
.withIndex("by_connectionId", (query) =>
|
|
178
|
+
query.eq("connectionId", args.connectionId),
|
|
179
|
+
)
|
|
180
|
+
.unique();
|
|
181
|
+
if (!grant) {
|
|
182
|
+
throw new ConvexError({ code: "FEDERATION_GRANT_NOT_FOUND" });
|
|
183
|
+
}
|
|
184
|
+
if (grant.revokedAt === undefined) {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
await ctx.db.patch("rastroFederationGrants", grant._id, {
|
|
187
|
+
revokedAt: now,
|
|
188
|
+
updatedAt: now,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
These mutations are `internalMutation`s so a browser cannot call them. Run them
|
|
197
|
+
only with deployment administrator credentials. A host that needs self-service
|
|
198
|
+
pairing may wrap equivalent logic in its own authenticated owner/admin mutation;
|
|
199
|
+
never accept an owner or user ID from an untrusted caller.
|
|
200
|
+
|
|
201
|
+
## 5. Trust the federation issuer
|
|
202
|
+
|
|
203
|
+
The displayed issuer must exactly equal the independently verified production
|
|
204
|
+
manifest issuer, `https://site.api.amagerastro.com/federation`; abort on any
|
|
205
|
+
mismatch. Derive the audience locally from the normalized functions URL and
|
|
206
|
+
compare it with the displayed audience. For Convex Cloud DEV:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
npx convex env set --deployment dev RASTRO_FEDERATION_ISSUER \
|
|
210
|
+
'https://site.api.amagerastro.com/federation'
|
|
211
|
+
npx convex env set --deployment dev RASTRO_FEDERATION_AUDIENCE \
|
|
212
|
+
'https://product.convex.cloud'
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Use `--deployment prod` only with fresh production consent. For an existing
|
|
216
|
+
Synapse-managed deployment, target its live Convex environment explicitly:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
synapse convex --dev env set \
|
|
220
|
+
RASTRO_FEDERATION_ISSUER 'https://site.api.amagerastro.com/federation'
|
|
221
|
+
synapse convex --dev env set \
|
|
222
|
+
RASTRO_FEDERATION_AUDIENCE 'https://product.synapse.example'
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Use `synapse env set NAME=value --for=dev` only when defining project defaults
|
|
226
|
+
before a new deployment is created; defaults do not update an existing live
|
|
227
|
+
deployment.
|
|
228
|
+
|
|
229
|
+
Insert only these two properties into the pilot project's existing
|
|
230
|
+
`defineApp({ env: { ... } })` object. If no `env` object exists, add it without
|
|
231
|
+
changing any other `defineApp` option. Do not add a second Rastro mount:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
RASTRO_FEDERATION_ISSUER: v.string(),
|
|
235
|
+
RASTRO_FEDERATION_AUDIENCE: v.string(),
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Insert this conditional spread into the existing `providers` array; never
|
|
239
|
+
replace the array or its current entries:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// convex/auth.config.ts
|
|
243
|
+
import type { AuthConfig } from "convex/server";
|
|
244
|
+
|
|
245
|
+
const issuer = process.env.RASTRO_FEDERATION_ISSUER;
|
|
246
|
+
const audience = process.env.RASTRO_FEDERATION_AUDIENCE;
|
|
247
|
+
|
|
248
|
+
...(issuer && audience
|
|
249
|
+
? [
|
|
250
|
+
{
|
|
251
|
+
domain: issuer.replace(/\/$/, ""),
|
|
252
|
+
applicationID: audience.replace(/\/$/, ""),
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
: []),
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
An exact issuer mismatch is rejected even when the JWT signature is otherwise
|
|
259
|
+
valid. Convex discovers keys from `<issuer>/.well-known/openid-configuration`.
|
|
260
|
+
|
|
261
|
+
## 6. Expose the read-only host module
|
|
262
|
+
|
|
263
|
+
Create the canonical module with this exact filename:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
// convex/rastroFederation.ts
|
|
267
|
+
import {
|
|
268
|
+
exposeFederatedAnalyticsApi,
|
|
269
|
+
type FederatedAnalyticsConnection,
|
|
270
|
+
} from "@iann29/rastro";
|
|
271
|
+
import { components } from "./_generated/api";
|
|
272
|
+
import { env, type QueryCtx } from "./_generated/server";
|
|
273
|
+
|
|
274
|
+
const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
|
|
275
|
+
issuer: env.RASTRO_FEDERATION_ISSUER.replace(/\/$/, ""),
|
|
276
|
+
resolveConnection: async (ctx, identity) => {
|
|
277
|
+
const db = ctx.db as unknown as QueryCtx["db"];
|
|
278
|
+
const grant = await db
|
|
279
|
+
.query("rastroFederationGrants")
|
|
280
|
+
.withIndex("by_connectionId", (query) =>
|
|
281
|
+
query.eq("connectionId", identity.connectionId),
|
|
282
|
+
)
|
|
283
|
+
.unique();
|
|
284
|
+
if (!grant || grant.organizationId !== identity.organizationId) return null;
|
|
285
|
+
return {
|
|
286
|
+
connectionId: grant.connectionId,
|
|
287
|
+
organizationId: grant.organizationId,
|
|
288
|
+
siteIds: grant.siteIds,
|
|
289
|
+
revokedAt: grant.revokedAt,
|
|
290
|
+
} satisfies FederatedAnalyticsConnection;
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
export const {
|
|
295
|
+
manifest,
|
|
296
|
+
connectionStatus,
|
|
297
|
+
listSites,
|
|
298
|
+
overview,
|
|
299
|
+
liveVisitors,
|
|
300
|
+
listSessions,
|
|
301
|
+
sessionJourney,
|
|
302
|
+
listConversions,
|
|
303
|
+
visitorJourney,
|
|
304
|
+
goalsReport,
|
|
305
|
+
funnelsReport,
|
|
306
|
+
affiliatesReport,
|
|
307
|
+
vitalsReport,
|
|
308
|
+
siteMap,
|
|
309
|
+
dataCoverage,
|
|
310
|
+
} = federated;
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The manifest is public static metadata. Every other function is read-only and
|
|
314
|
+
requires both a valid JWT and a matching non-revoked local grant.
|
|
315
|
+
|
|
316
|
+
## 7. Deploy and provision
|
|
317
|
+
|
|
318
|
+
Push the customer host changes first:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# Convex Cloud DEV, one push
|
|
322
|
+
npx convex dev --once
|
|
323
|
+
|
|
324
|
+
# Synapse DEV
|
|
325
|
+
synapse dev --once
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Then provision the exact site scope. Replace every placeholder with values
|
|
329
|
+
copied from the pending connection and the host's existing site records:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# Convex Cloud DEV
|
|
333
|
+
npx convex run --deployment dev rastroFederationAdmin:provisionConnection \
|
|
334
|
+
'{"connectionId":"CONNECTION_ID","organizationId":"ORGANIZATION_ID","ownerId":"OPAQUE_HOST_OWNER_ID","siteIds":["SITE_ID"]}'
|
|
335
|
+
|
|
336
|
+
# Synapse DEV
|
|
337
|
+
synapse convex --dev run rastroFederationAdmin:provisionConnection \
|
|
338
|
+
'{"connectionId":"CONNECTION_ID","organizationId":"ORGANIZATION_ID","ownerId":"OPAQUE_HOST_OWNER_ID","siteIds":["SITE_ID"]}'
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Use the corresponding explicit production target only after rehearsing DEV. The
|
|
342
|
+
command is idempotent for one `connectionId` and replaces its site scope.
|
|
343
|
+
|
|
344
|
+
## 8. Verify in order
|
|
345
|
+
|
|
346
|
+
1. Run `rastroFederation:manifest` without auth and confirm protocol version 1.
|
|
347
|
+
2. Return to **Conexões** in the central dashboard.
|
|
348
|
+
3. Click **Verificar conexão**.
|
|
349
|
+
4. Confirm the connection becomes active and reports the expected site count.
|
|
350
|
+
5. Open Visão geral and confirm the deployment selector names the new host.
|
|
351
|
+
6. Trigger one real browser pageview and confirm Ao vivo updates reactively.
|
|
352
|
+
7. Check Metas/Afiliados with a complete UTC-day range when those features are
|
|
353
|
+
configured.
|
|
354
|
+
|
|
355
|
+
The central verification signs a short-lived RS256 token, calls authenticated
|
|
356
|
+
`connectionStatus` and `listSites`, and refuses incompatible manifests or
|
|
357
|
+
inconsistent site counts.
|
|
358
|
+
|
|
359
|
+
## 9. Revoke safely
|
|
360
|
+
|
|
361
|
+
For immediate denial, revoke the local grant first:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
npx convex run --deployment dev rastroFederationAdmin:revokeConnection \
|
|
365
|
+
'{"connectionId":"CONNECTION_ID"}'
|
|
366
|
+
|
|
367
|
+
# Synapse DEV
|
|
368
|
+
synapse convex --dev run rastroFederationAdmin:revokeConnection \
|
|
369
|
+
'{"connectionId":"CONNECTION_ID"}'
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
For production, repeat the rehearsed command with the platform's explicit
|
|
373
|
+
production target: `npx convex run --deployment prod ...` or
|
|
374
|
+
`synapse convex --prod run ...`. Never infer the target from the current shell
|
|
375
|
+
state.
|
|
376
|
+
|
|
377
|
+
Then revoke the connection in the central dashboard. Central revocation stops
|
|
378
|
+
new tokens; local revocation rejects already-issued tokens immediately. Without
|
|
379
|
+
the local step, an existing token remains usable until its ten-minute expiry.
|
|
380
|
+
|
|
381
|
+
## Troubleshooting
|
|
382
|
+
|
|
383
|
+
| Symptom or code | Check |
|
|
384
|
+
| ------------------------------ | ----------------------------------------------------------------------------- |
|
|
385
|
+
| Manifest not found | File must be `convex/rastroFederation.ts`; deploy/codegen must have completed |
|
|
386
|
+
| `FEDERATION_UNAUTHENTICATED` | Federation provider missing, token absent, or deployment not redeployed |
|
|
387
|
+
| `FEDERATION_ISSUER_MISMATCH` | `auth.config.ts` domain and helper issuer differ, often by a trailing slash |
|
|
388
|
+
| JWT audience failure | `RASTRO_FEDERATION_AUDIENCE` must equal the functions URL, not the HTTP URL |
|
|
389
|
+
| `FEDERATION_CONNECTION_DENIED` | Grant missing, organization mismatched, empty, malformed, or revoked |
|
|
390
|
+
| `FEDERATION_SITE_DENIED` | Dashboard requested a site outside the local grant |
|
|
391
|
+
| Tracker 404 | Tracker uses the functions URL instead of the HTTP-actions URL |
|
|
392
|
+
| `ORIGIN_NOT_ALLOWED` | Browser host is absent from the site's exact/wildcard domains |
|
|
393
|
+
| `REPORT_INCOMPLETE` | Use complete UTC buckets or reduce a range that exceeds a bounded read budget |
|
|
394
|
+
| `UNTRUSTED_DEPLOYMENT_HOST` | Ask the control-plane operator to allow the exact custom functions origin |
|
|
395
|
+
|
|
396
|
+
Do not fix authorization errors by weakening the resolver, copying site IDs into
|
|
397
|
+
JWT claims, or exposing a permissive report API.
|