@iann29/rastro 0.1.0-alpha.4 → 0.1.0-alpha.6
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 +203 -56
- package/agent/integration.md +17 -15
- package/agent/manifest.json +6 -4
- package/agent/manifest.schema.json +8 -5
- package/dist/client/federation.d.ts +34 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +17 -3
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +486 -31
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +209 -41
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +8 -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 +82 -8
- package/dist/component/_generated/component.d.ts.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.map +1 -1
- package/dist/component/cardinality.js.map +1 -1
- package/dist/component/constants.d.ts +6 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +12 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/diagnostics.d.ts.map +1 -1
- package/dist/component/diagnostics.js.map +1 -1
- package/dist/component/eventStore.d.ts +3 -1
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/eventStore.js +13 -10
- package/dist/component/eventStore.js.map +1 -1
- 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.map +1 -1
- package/dist/component/geo.js +3 -2
- package/dist/component/geo.js.map +1 -1
- package/dist/component/goals.d.ts.map +1 -1
- package/dist/component/goals.js +8 -5
- 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 +71 -37
- 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 +168 -29
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +1 -3
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +70 -4
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +232 -59
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts +4 -4
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +54 -22
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +42 -11
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +83 -10
- package/dist/component/schema.js +34 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +6 -6
- 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 +29 -18
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +8 -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.map +1 -1
- package/dist/tracker/tracker.js +23 -11
- 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 +137 -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 +71 -71
- package/docs/benchmarks/2026-08-21-formal-certification.md +50 -30
- package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
- package/docs/federation.md +1 -1
- package/package.json +27 -9
- package/scripts/benchmark-ingest.mjs +101 -48
- package/src/component/_generated/api.ts +8 -0
- package/src/component/_generated/component.ts +87 -5
- package/src/component/affiliates.ts +20 -5
- package/src/component/cardinality.ts +8 -7
- package/src/component/constants.ts +12 -0
- package/src/component/diagnostics.ts +3 -2
- package/src/component/eventStore.ts +53 -46
- package/src/component/funnels.ts +19 -16
- package/src/component/geo.ts +16 -14
- package/src/component/goals.ts +29 -21
- package/src/component/guards.ts +3 -1
- package/src/component/http.ts +126 -78
- package/src/component/identity.ts +74 -0
- package/src/component/ingest.ts +349 -130
- package/src/component/live.ts +5 -4
- package/src/component/reports.ts +572 -221
- package/src/component/retention.ts +221 -98
- package/src/component/sanitize.ts +77 -27
- package/src/component/schema.ts +46 -5
- package/src/component/sites.ts +22 -11
- package/src/component/useragent.ts +171 -0
- package/src/component/validators.ts +19 -7
- 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.
|
package/docs/federation.md
CHANGED
|
@@ -239,7 +239,7 @@ either overview dataset.
|
|
|
239
239
|
## Control-plane status
|
|
240
240
|
|
|
241
241
|
The production control plane at `https://www.amagerastro.com` and its reference
|
|
242
|
-
implementation under `
|
|
242
|
+
implementation under `control-plane/` provide Better Auth sessions,
|
|
243
243
|
organization-scoped connections, OIDC discovery, JWKS, ten-minute RS256 tokens,
|
|
244
244
|
manifest verification, redacted site discovery, audit records, revocation of new
|
|
245
245
|
token issuance, and dynamic browser subscriptions to active deployments.
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iann29/rastro",
|
|
3
|
-
"description": "Privacy-first, real-time web analytics for Convex: live visitor atlas, journeys, revenue, goals, funnels, affiliates, and a
|
|
3
|
+
"description": "Privacy-first, real-time web analytics for Convex: live visitor atlas, journeys, revenue, goals, funnels, affiliates, and a 1.1 KB tracker.",
|
|
4
4
|
"repository": "github:amageweb/amage-rastro",
|
|
5
5
|
"homepage": "https://github.com/amageweb/amage-rastro#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/amageweb/amage-rastro/issues"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.6",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"jsdelivr": "./dist/tracker.min.js",
|
|
28
28
|
"scripts": {
|
|
29
29
|
"dev": "synapse dev --start 'npm run dev:build'",
|
|
30
|
-
"dev:frontend": "cd
|
|
30
|
+
"dev:frontend": "cd control-plane && vite",
|
|
31
31
|
"dev:build": "chokidar 'tsconfig*.json' 'src/**/*.{ts,js}' 'scripts/build-tracker.mjs' -i '**/*.test.ts' -c 'npm run build:codegen' --initial",
|
|
32
|
-
"deploy:dashboard": "static-hosting upload --build --dist
|
|
32
|
+
"deploy:dashboard": "static-hosting upload --build --dist control-plane/dist --build-command \"npm exec vite -- build control-plane\"",
|
|
33
33
|
"predev": "npm run build:codegen",
|
|
34
34
|
"prebuild": "rm -rf dist *.tsbuildinfo",
|
|
35
35
|
"build": "npm run build:tracker && tsc --project ./tsconfig.build.json",
|
|
@@ -37,17 +37,21 @@
|
|
|
37
37
|
"federation:keys": "node ./scripts/generate-federation-keys.mjs",
|
|
38
38
|
"benchmark:ingest": "node ./scripts/benchmark-ingest.mjs",
|
|
39
39
|
"benchmark:local": "vitest run src/component/benchmark.test.ts",
|
|
40
|
+
"format:check": "prettier --check .",
|
|
40
41
|
"verify:agent-docs": "node ./scripts/verify-agent-docs.mjs",
|
|
41
|
-
"
|
|
42
|
+
"verify:skill-paths": "node ./scripts/verify-skill-paths.mjs",
|
|
43
|
+
"build:codegen": "npm run build && synapse convex codegen --component-dir ./src/component && npm run build && npm run codegen:example && synapse dev --once",
|
|
44
|
+
"codegen:example": "npm run --workspace example codegen",
|
|
42
45
|
"build:clean": "npm run build:codegen",
|
|
43
|
-
"typecheck": "tsc --noEmit && tsc -p
|
|
46
|
+
"typecheck": "tsc --noEmit && tsc -p control-plane && tsc -p control-plane/convex && tsc -p example/convex",
|
|
44
47
|
"lint": "eslint .",
|
|
45
48
|
"test": "vitest run --typecheck",
|
|
46
49
|
"test:watch": "vitest --typecheck --clearScreen false",
|
|
47
50
|
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
|
48
51
|
"test:coverage": "vitest run --coverage --coverage.reporter=text",
|
|
52
|
+
"e2e": "playwright test -c control-plane/playwright.config.ts",
|
|
49
53
|
"prepare": "npm run build",
|
|
50
|
-
"preversion": "npm run build && npm exec vite -- build
|
|
54
|
+
"preversion": "npm run build && VITE_CONVEX_URL=https://preflight.example.convex.cloud VITE_CONVEX_SITE_URL=https://preflight.example.convex.site npm exec vite -- build control-plane && npm run test && npm run format:check && npm run lint && npm run typecheck && npm run benchmark:local && npm run verify:agent-docs && npm run verify:skill-paths"
|
|
51
55
|
},
|
|
52
56
|
"files": [
|
|
53
57
|
"dist",
|
|
@@ -77,6 +81,7 @@
|
|
|
77
81
|
"default": "./dist/react/index.js"
|
|
78
82
|
},
|
|
79
83
|
"./tracker.js": "./dist/tracker.min.js",
|
|
84
|
+
"./vitals.js": "./dist/vitals.min.js",
|
|
80
85
|
"./test": "./src/test.ts",
|
|
81
86
|
"./_generated/component.js": {
|
|
82
87
|
"types": "./dist/component/_generated/component.d.ts"
|
|
@@ -105,12 +110,19 @@
|
|
|
105
110
|
"devDependencies": {
|
|
106
111
|
"@convex-dev/eslint-plugin": "^4.0.0",
|
|
107
112
|
"@convex-dev/static-hosting": "^0.2.1",
|
|
113
|
+
"@dicebear/core": "^9.4.3",
|
|
114
|
+
"@dicebear/lorelei": "^9.4.2",
|
|
108
115
|
"@edge-runtime/vm": "^5.0.0",
|
|
109
116
|
"@eslint/eslintrc": "^3.3.6",
|
|
110
117
|
"@eslint/js": "9.39.5",
|
|
111
118
|
"@fontsource-variable/instrument-sans": "^5.3.0",
|
|
112
119
|
"@fontsource/ibm-plex-mono": "^5.3.0",
|
|
120
|
+
"@playwright/test": "^1.62.1",
|
|
113
121
|
"@tailwindcss/vite": "^4.3.3",
|
|
122
|
+
"@testing-library/dom": "^10.4.1",
|
|
123
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
124
|
+
"@testing-library/react": "^16.3.3",
|
|
125
|
+
"@testing-library/user-event": "^14.6.6",
|
|
114
126
|
"@types/node": "^24.13.3",
|
|
115
127
|
"@types/react": "^19.2.18",
|
|
116
128
|
"@types/react-dom": "^19.2.4",
|
|
@@ -128,6 +140,7 @@
|
|
|
128
140
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
129
141
|
"eslint-plugin-react-refresh": "^0.5.3",
|
|
130
142
|
"globals": "^17.9.0",
|
|
143
|
+
"jsdom": "^30.0.1",
|
|
131
144
|
"lucide-react": "^1.32.0",
|
|
132
145
|
"maplibre-gl": "^6.4.1",
|
|
133
146
|
"pkg-pr-new": "^0.0.86",
|
|
@@ -148,6 +161,11 @@
|
|
|
148
161
|
"dependencies": {
|
|
149
162
|
"@convex-dev/better-auth": "^0.12.5",
|
|
150
163
|
"better-auth": "~1.6.15",
|
|
151
|
-
"
|
|
152
|
-
|
|
164
|
+
"country-flag-icons": "^1.6.20",
|
|
165
|
+
"jose": "^6.2.10",
|
|
166
|
+
"simple-icons": "^16.29.0"
|
|
167
|
+
},
|
|
168
|
+
"workspaces": [
|
|
169
|
+
"example"
|
|
170
|
+
]
|
|
153
171
|
}
|
|
@@ -67,16 +67,19 @@ function parseOptions(argv) {
|
|
|
67
67
|
const values = new Map();
|
|
68
68
|
for (let index = 0; index < argv.length; index += 1) {
|
|
69
69
|
const argument = argv[index];
|
|
70
|
-
if (!argument.startsWith("--"))
|
|
70
|
+
if (!argument.startsWith("--"))
|
|
71
|
+
throw new Error(`unexpected argument: ${argument}`);
|
|
71
72
|
const [rawName, inlineValue] = argument.slice(2).split("=", 2);
|
|
72
|
-
if (!OPTION_NAMES.has(rawName))
|
|
73
|
+
if (!OPTION_NAMES.has(rawName))
|
|
74
|
+
throw new Error(`unknown option: --${rawName}`);
|
|
73
75
|
if (values.has(rawName)) throw new Error(`duplicate option: --${rawName}`);
|
|
74
76
|
if (rawName === "dry-run" || rawName === "certify") {
|
|
75
77
|
values.set(rawName, "true");
|
|
76
78
|
continue;
|
|
77
79
|
}
|
|
78
80
|
const value = inlineValue ?? argv[++index];
|
|
79
|
-
if (!value || value.startsWith("--"))
|
|
81
|
+
if (!value || value.startsWith("--"))
|
|
82
|
+
throw new Error(`missing value for --${rawName}`);
|
|
80
83
|
values.set(rawName, value);
|
|
81
84
|
}
|
|
82
85
|
const profile = values.get("profile") ?? "realistic";
|
|
@@ -95,7 +98,10 @@ function parseOptions(argv) {
|
|
|
95
98
|
maximumRequests: values.has("requests")
|
|
96
99
|
? positiveInteger(values.get("requests"), "requests")
|
|
97
100
|
: Number.POSITIVE_INFINITY,
|
|
98
|
-
concurrency: positiveInteger(
|
|
101
|
+
concurrency: positiveInteger(
|
|
102
|
+
values.get("concurrency") ?? "100",
|
|
103
|
+
"concurrency",
|
|
104
|
+
),
|
|
99
105
|
batchSize: positiveInteger(
|
|
100
106
|
values.get("batch-size") ?? String(defaults.batchSize),
|
|
101
107
|
"batch-size",
|
|
@@ -112,20 +118,23 @@ function parseOptions(argv) {
|
|
|
112
118
|
|
|
113
119
|
function positiveNumber(value, name) {
|
|
114
120
|
const parsed = Number(value);
|
|
115
|
-
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
121
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
122
|
+
throw new Error(`--${name} must be positive`);
|
|
116
123
|
return parsed;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
function positiveInteger(value, name) {
|
|
120
127
|
const parsed = positiveNumber(value, name);
|
|
121
|
-
if (!Number.isSafeInteger(parsed))
|
|
128
|
+
if (!Number.isSafeInteger(parsed))
|
|
129
|
+
throw new Error(`--${name} must be an integer`);
|
|
122
130
|
return parsed;
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
function eventFor(profile, session, sequence, timestamp) {
|
|
126
|
-
const path =
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
const path =
|
|
135
|
+
profile === "lean"
|
|
136
|
+
? `/benchmark/lean/${sequence % 4}`
|
|
137
|
+
: REALISTIC_PATHS[sequence % REALISTIC_PATHS.length];
|
|
129
138
|
const base = {
|
|
130
139
|
eventId: `${session.id}.${sequence.toString(36)}`,
|
|
131
140
|
sessionId: session.id,
|
|
@@ -151,10 +160,20 @@ function eventFor(profile, session, sequence, timestamp) {
|
|
|
151
160
|
if (profile === "feature") {
|
|
152
161
|
const step = sequence % 3;
|
|
153
162
|
if (step === 0) {
|
|
154
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
...base,
|
|
165
|
+
type: "pageview",
|
|
166
|
+
path: "/",
|
|
167
|
+
affiliateSlug: "creator-club",
|
|
168
|
+
};
|
|
155
169
|
}
|
|
156
170
|
if (step === 1) {
|
|
157
|
-
return {
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
type: "pageview",
|
|
174
|
+
path: "/pricing",
|
|
175
|
+
affiliateSlug: "creator-club",
|
|
176
|
+
};
|
|
158
177
|
}
|
|
159
178
|
return {
|
|
160
179
|
...base,
|
|
@@ -172,7 +191,10 @@ function eventFor(profile, session, sequence, timestamp) {
|
|
|
172
191
|
return {
|
|
173
192
|
...base,
|
|
174
193
|
type: "pageview",
|
|
175
|
-
referrer:
|
|
194
|
+
referrer:
|
|
195
|
+
sequence % 2 === 0
|
|
196
|
+
? "https://www.google.com/search?q=rastro"
|
|
197
|
+
: undefined,
|
|
176
198
|
};
|
|
177
199
|
}
|
|
178
200
|
if (kind < 13) return { ...base, type: "click", target: "Get started" };
|
|
@@ -184,7 +206,11 @@ function eventFor(profile, session, sequence, timestamp) {
|
|
|
184
206
|
properties: { placement: "hero", experiment: `variant-${sequence % 3}` },
|
|
185
207
|
};
|
|
186
208
|
}
|
|
187
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
...base,
|
|
211
|
+
type: "outbound",
|
|
212
|
+
href: "https://github.com/amageweb/amage-rastro",
|
|
213
|
+
};
|
|
188
214
|
}
|
|
189
215
|
|
|
190
216
|
function requestContext(session) {
|
|
@@ -200,15 +226,19 @@ function requestContext(session) {
|
|
|
200
226
|
|
|
201
227
|
function percentile(sorted, fraction) {
|
|
202
228
|
if (sorted.length === 0) return 0;
|
|
203
|
-
return sorted[
|
|
229
|
+
return sorted[
|
|
230
|
+
Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)
|
|
231
|
+
];
|
|
204
232
|
}
|
|
205
233
|
|
|
206
234
|
function hasValidSessionTotals(value) {
|
|
207
|
-
return
|
|
235
|
+
return (
|
|
236
|
+
value !== null &&
|
|
208
237
|
typeof value === "object" &&
|
|
209
238
|
[value.requestedSessions, value.foundSessions, value.storedEvents].every(
|
|
210
239
|
(total) => Number.isSafeInteger(total) && total >= 0,
|
|
211
|
-
)
|
|
240
|
+
)
|
|
241
|
+
);
|
|
212
242
|
}
|
|
213
243
|
|
|
214
244
|
async function main() {
|
|
@@ -249,15 +279,16 @@ async function main() {
|
|
|
249
279
|
const durations = [];
|
|
250
280
|
|
|
251
281
|
const buildRequest = (requestIndex) => {
|
|
252
|
-
const session =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
282
|
+
const session =
|
|
283
|
+
options.profile === "lean"
|
|
284
|
+
? {
|
|
285
|
+
id: `bench-${runId}-${requestIndex}`,
|
|
286
|
+
visitorId: `visitor-${runId}-${requestIndex}`,
|
|
287
|
+
sequence: 0,
|
|
288
|
+
locationIndex: requestIndex % LOCATIONS.length,
|
|
289
|
+
userAgentIndex: requestIndex % USER_AGENTS.length,
|
|
290
|
+
}
|
|
291
|
+
: sessions[requestIndex % sessions.length];
|
|
261
292
|
const sequenceStart = session.sequence;
|
|
262
293
|
session.sequence += options.batchSize;
|
|
263
294
|
const now = Date.now();
|
|
@@ -276,18 +307,26 @@ async function main() {
|
|
|
276
307
|
throw new Error("generated request exceeds the 64 KiB HTTP limit");
|
|
277
308
|
}
|
|
278
309
|
if (options.dryRun) {
|
|
279
|
-
process.stdout.write(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
310
|
+
process.stdout.write(
|
|
311
|
+
`${JSON.stringify(
|
|
312
|
+
{
|
|
313
|
+
profile: options.profile,
|
|
314
|
+
batchSize: options.batchSize,
|
|
315
|
+
requestBytes: Buffer.byteLength(dryRequest.body),
|
|
316
|
+
heartbeats: dryRequest.heartbeats,
|
|
317
|
+
location: requestContext(sessions[0]),
|
|
318
|
+
},
|
|
319
|
+
null,
|
|
320
|
+
2,
|
|
321
|
+
)}\n`,
|
|
322
|
+
);
|
|
286
323
|
return;
|
|
287
324
|
}
|
|
288
325
|
if (options.profile !== "lean") sessions[0].sequence = 0;
|
|
289
326
|
if (!options.url || !options.siteId) {
|
|
290
|
-
throw new Error(
|
|
327
|
+
throw new Error(
|
|
328
|
+
"--url and --site-id are required unless --dry-run is used",
|
|
329
|
+
);
|
|
291
330
|
}
|
|
292
331
|
|
|
293
332
|
const stopForSignal = (signal) => {
|
|
@@ -299,7 +338,9 @@ async function main() {
|
|
|
299
338
|
process.once("SIGINT", stopForInterrupt);
|
|
300
339
|
process.once("SIGTERM", stopForTermination);
|
|
301
340
|
|
|
302
|
-
const convex = options.convexUrl
|
|
341
|
+
const convex = options.convexUrl
|
|
342
|
+
? new ConvexHttpClient(options.convexUrl)
|
|
343
|
+
: null;
|
|
303
344
|
if (convex) {
|
|
304
345
|
const preflight = await convex.query(reconcileSessionsQuery, {
|
|
305
346
|
siteId: options.siteId,
|
|
@@ -333,7 +374,9 @@ async function main() {
|
|
|
333
374
|
const request = buildRequest(requestIndex);
|
|
334
375
|
const requestBytes = Buffer.byteLength(request.body);
|
|
335
376
|
if (requestBytes > 64 * 1024) {
|
|
336
|
-
throw new Error(
|
|
377
|
+
throw new Error(
|
|
378
|
+
`generated request ${requestIndex} exceeds the 64 KiB HTTP limit`,
|
|
379
|
+
);
|
|
337
380
|
}
|
|
338
381
|
sentBytes += requestBytes;
|
|
339
382
|
sentEvents += options.batchSize;
|
|
@@ -368,7 +411,8 @@ async function main() {
|
|
|
368
411
|
![result.accepted, result.duplicates, result.rejected].every(
|
|
369
412
|
(value) => Number.isSafeInteger(value) && value >= 0,
|
|
370
413
|
) ||
|
|
371
|
-
result.accepted + result.duplicates + result.rejected !==
|
|
414
|
+
result.accepted + result.duplicates + result.rejected !==
|
|
415
|
+
options.batchSize
|
|
372
416
|
) {
|
|
373
417
|
throw new Error("response has invalid event accounting");
|
|
374
418
|
}
|
|
@@ -384,7 +428,9 @@ async function main() {
|
|
|
384
428
|
} catch (error) {
|
|
385
429
|
failures += 1;
|
|
386
430
|
failedEvents += options.batchSize;
|
|
387
|
-
recordError(
|
|
431
|
+
recordError(
|
|
432
|
+
`fetch:${error instanceof Error ? error.message : "unknown"}`,
|
|
433
|
+
);
|
|
388
434
|
} finally {
|
|
389
435
|
durations.push(performance.now() - requestStartedAt);
|
|
390
436
|
completedRequests += 1;
|
|
@@ -393,11 +439,14 @@ async function main() {
|
|
|
393
439
|
};
|
|
394
440
|
|
|
395
441
|
const progress = setInterval(() => {
|
|
396
|
-
const elapsedSeconds = Math.max(
|
|
442
|
+
const elapsedSeconds = Math.max(
|
|
443
|
+
0.001,
|
|
444
|
+
(performance.now() - startedAt) / 1_000,
|
|
445
|
+
);
|
|
397
446
|
process.stderr.write(
|
|
398
447
|
`[${options.label}] ${Math.round(elapsedSeconds)}s ` +
|
|
399
|
-
|
|
400
|
-
|
|
448
|
+
`${accepted} accepted ${Math.round(accepted / elapsedSeconds)} events/s ` +
|
|
449
|
+
`${failures} failures\n`,
|
|
401
450
|
);
|
|
402
451
|
}, options.progressSeconds * 1_000);
|
|
403
452
|
progress.unref();
|
|
@@ -405,14 +454,16 @@ async function main() {
|
|
|
405
454
|
clearInterval(progress);
|
|
406
455
|
|
|
407
456
|
const elapsedMs = performance.now() - startedAt;
|
|
408
|
-
terminationReason ??=
|
|
457
|
+
terminationReason ??=
|
|
458
|
+
performance.now() >= deadline ? "duration" : "completed";
|
|
409
459
|
process.removeListener("SIGINT", stopForInterrupt);
|
|
410
460
|
process.removeListener("SIGTERM", stopForTermination);
|
|
411
461
|
const durationCompleted = terminationReason === "duration";
|
|
412
462
|
durations.sort((left, right) => left - right);
|
|
413
|
-
const expectedSessions =
|
|
414
|
-
|
|
415
|
-
|
|
463
|
+
const expectedSessions =
|
|
464
|
+
options.profile === "lean"
|
|
465
|
+
? nextRequest
|
|
466
|
+
: Math.min(nextRequest, options.sessionCount);
|
|
416
467
|
let sessionTotals = null;
|
|
417
468
|
if (convex && expectedSessions > 0) {
|
|
418
469
|
try {
|
|
@@ -490,7 +541,8 @@ async function main() {
|
|
|
490
541
|
},
|
|
491
542
|
concurrency: options.concurrency,
|
|
492
543
|
batchSize: options.batchSize,
|
|
493
|
-
sessionPool:
|
|
544
|
+
sessionPool:
|
|
545
|
+
options.profile === "lean" ? "unique-per-request" : options.sessionCount,
|
|
494
546
|
maximumRequests: Number.isFinite(options.maximumRequests)
|
|
495
547
|
? options.maximumRequests
|
|
496
548
|
: null,
|
|
@@ -513,9 +565,10 @@ async function main() {
|
|
|
513
565
|
estimatedEventsPerDay: durationCompleted
|
|
514
566
|
? Math.round((accepted * 86_400_000) / elapsedMs)
|
|
515
567
|
: null,
|
|
516
|
-
estimatedStoredEventsPerDay:
|
|
517
|
-
|
|
518
|
-
|
|
568
|
+
estimatedStoredEventsPerDay:
|
|
569
|
+
durationCompleted && reconciliationPassed
|
|
570
|
+
? Math.round((storedEvents * 86_400_000) / elapsedMs)
|
|
571
|
+
: null,
|
|
519
572
|
certifiedEventsPerDay: certificationPassed
|
|
520
573
|
? Math.round((accepted * 86_400_000) / elapsedMs)
|
|
521
574
|
: null,
|
|
@@ -20,13 +20,17 @@ import type * as geo from "../geo.js";
|
|
|
20
20
|
import type * as goals from "../goals.js";
|
|
21
21
|
import type * as guards from "../guards.js";
|
|
22
22
|
import type * as http from "../http.js";
|
|
23
|
+
import type * as identity from "../identity.js";
|
|
23
24
|
import type * as ingest from "../ingest.js";
|
|
24
25
|
import type * as live from "../live.js";
|
|
25
26
|
import type * as reports from "../reports.js";
|
|
26
27
|
import type * as retention from "../retention.js";
|
|
27
28
|
import type * as sanitize from "../sanitize.js";
|
|
28
29
|
import type * as sites from "../sites.js";
|
|
30
|
+
import type * as useragent from "../useragent.js";
|
|
29
31
|
import type * as validators from "../validators.js";
|
|
32
|
+
import type * as visitors from "../visitors.js";
|
|
33
|
+
import type * as vitals from "../vitals.js";
|
|
30
34
|
|
|
31
35
|
import type {
|
|
32
36
|
ApiFromModules,
|
|
@@ -48,13 +52,17 @@ const fullApi: ApiFromModules<{
|
|
|
48
52
|
goals: typeof goals;
|
|
49
53
|
guards: typeof guards;
|
|
50
54
|
http: typeof http;
|
|
55
|
+
identity: typeof identity;
|
|
51
56
|
ingest: typeof ingest;
|
|
52
57
|
live: typeof live;
|
|
53
58
|
reports: typeof reports;
|
|
54
59
|
retention: typeof retention;
|
|
55
60
|
sanitize: typeof sanitize;
|
|
56
61
|
sites: typeof sites;
|
|
62
|
+
useragent: typeof useragent;
|
|
57
63
|
validators: typeof validators;
|
|
64
|
+
visitors: typeof visitors;
|
|
65
|
+
vitals: typeof vitals;
|
|
58
66
|
}> = anyApi as any;
|
|
59
67
|
|
|
60
68
|
/**
|