@iann29/rastro 0.1.0-alpha.1 → 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.
Files changed (163) hide show
  1. package/README.md +521 -66
  2. package/agent/integration.md +661 -0
  3. package/agent/manifest.json +187 -0
  4. package/agent/manifest.schema.json +411 -0
  5. package/dist/client/federation.d.ts +236 -0
  6. package/dist/client/federation.d.ts.map +1 -0
  7. package/dist/client/federation.js +196 -0
  8. package/dist/client/federation.js.map +1 -0
  9. package/dist/client/index.d.ts +2467 -18
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +461 -32
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +16 -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 +302 -4
  17. package/dist/component/_generated/component.d.ts.map +1 -1
  18. package/dist/component/_generated/server.d.ts +4 -0
  19. package/dist/component/_generated/server.d.ts.map +1 -1
  20. package/dist/component/_generated/server.js.map +1 -1
  21. package/dist/component/affiliates.d.ts.map +1 -1
  22. package/dist/component/affiliates.js +6 -2
  23. package/dist/component/affiliates.js.map +1 -1
  24. package/dist/component/cardinality.d.ts +12 -0
  25. package/dist/component/cardinality.d.ts.map +1 -0
  26. package/dist/component/cardinality.js +94 -0
  27. package/dist/component/cardinality.js.map +1 -0
  28. package/dist/component/constants.d.ts +25 -1
  29. package/dist/component/constants.d.ts.map +1 -1
  30. package/dist/component/constants.js +38 -1
  31. package/dist/component/constants.js.map +1 -1
  32. package/dist/component/convex.config.d.ts +6 -1
  33. package/dist/component/convex.config.js +9 -1
  34. package/dist/component/convex.config.js.map +1 -1
  35. package/dist/component/coverage.d.ts +19 -0
  36. package/dist/component/coverage.d.ts.map +1 -0
  37. package/dist/component/coverage.js +30 -0
  38. package/dist/component/coverage.js.map +1 -0
  39. package/dist/component/diagnostics.d.ts +9 -0
  40. package/dist/component/diagnostics.d.ts.map +1 -0
  41. package/dist/component/diagnostics.js +47 -0
  42. package/dist/component/diagnostics.js.map +1 -0
  43. package/dist/component/errors.d.ts +1 -1
  44. package/dist/component/errors.d.ts.map +1 -1
  45. package/dist/component/errors.js.map +1 -1
  46. package/dist/component/eventStore.d.ts +21 -9
  47. package/dist/component/eventStore.d.ts.map +1 -1
  48. package/dist/component/eventStore.js +142 -152
  49. package/dist/component/eventStore.js.map +1 -1
  50. package/dist/component/funnels.d.ts.map +1 -1
  51. package/dist/component/funnels.js +5 -3
  52. package/dist/component/funnels.js.map +1 -1
  53. package/dist/component/geo.d.ts +71 -0
  54. package/dist/component/geo.d.ts.map +1 -0
  55. package/dist/component/geo.js +611 -0
  56. package/dist/component/geo.js.map +1 -0
  57. package/dist/component/goals.d.ts.map +1 -1
  58. package/dist/component/goals.js +8 -5
  59. package/dist/component/goals.js.map +1 -1
  60. package/dist/component/guards.d.ts.map +1 -1
  61. package/dist/component/guards.js.map +1 -1
  62. package/dist/component/http.d.ts.map +1 -1
  63. package/dist/component/http.js +248 -49
  64. package/dist/component/http.js.map +1 -1
  65. package/dist/component/identity.d.ts +13 -0
  66. package/dist/component/identity.d.ts.map +1 -0
  67. package/dist/component/identity.js +58 -0
  68. package/dist/component/identity.js.map +1 -0
  69. package/dist/component/ingest.d.ts +3 -1
  70. package/dist/component/ingest.d.ts.map +1 -1
  71. package/dist/component/ingest.js +604 -55
  72. package/dist/component/ingest.js.map +1 -1
  73. package/dist/component/live.d.ts.map +1 -1
  74. package/dist/component/live.js +7 -6
  75. package/dist/component/live.js.map +1 -1
  76. package/dist/component/reports.d.ts +259 -10
  77. package/dist/component/reports.d.ts.map +1 -1
  78. package/dist/component/reports.js +1110 -124
  79. package/dist/component/reports.js.map +1 -1
  80. package/dist/component/retention.d.ts +75 -1
  81. package/dist/component/retention.d.ts.map +1 -1
  82. package/dist/component/retention.js +537 -32
  83. package/dist/component/retention.js.map +1 -1
  84. package/dist/component/sanitize.d.ts +24 -1
  85. package/dist/component/sanitize.d.ts.map +1 -1
  86. package/dist/component/sanitize.js +96 -16
  87. package/dist/component/sanitize.js.map +1 -1
  88. package/dist/component/schema.d.ts +378 -62
  89. package/dist/component/schema.js +178 -20
  90. package/dist/component/schema.js.map +1 -1
  91. package/dist/component/sites.d.ts.map +1 -1
  92. package/dist/component/sites.js +11 -7
  93. package/dist/component/sites.js.map +1 -1
  94. package/dist/component/useragent.d.ts +9 -0
  95. package/dist/component/useragent.d.ts.map +1 -0
  96. package/dist/component/useragent.js +152 -0
  97. package/dist/component/useragent.js.map +1 -0
  98. package/dist/component/validators.d.ts +101 -65
  99. package/dist/component/validators.d.ts.map +1 -1
  100. package/dist/component/validators.js +19 -13
  101. package/dist/component/validators.js.map +1 -1
  102. package/dist/component/visitors.d.ts +19 -0
  103. package/dist/component/visitors.d.ts.map +1 -0
  104. package/dist/component/visitors.js +86 -0
  105. package/dist/component/visitors.js.map +1 -0
  106. package/dist/component/vitals.d.ts +41 -0
  107. package/dist/component/vitals.d.ts.map +1 -0
  108. package/dist/component/vitals.js +115 -0
  109. package/dist/component/vitals.js.map +1 -0
  110. package/dist/react/index.d.ts.map +1 -1
  111. package/dist/react/index.js.map +1 -1
  112. package/dist/tracker/generated.d.ts +8 -4
  113. package/dist/tracker/generated.d.ts.map +1 -1
  114. package/dist/tracker/generated.js +8 -4
  115. package/dist/tracker/generated.js.map +1 -1
  116. package/dist/tracker/tracker.d.ts +1 -1
  117. package/dist/tracker/tracker.d.ts.map +1 -1
  118. package/dist/tracker/tracker.js +57 -20
  119. package/dist/tracker/tracker.js.map +1 -1
  120. package/dist/tracker/vitals.d.ts +10 -0
  121. package/dist/tracker/vitals.d.ts.map +1 -0
  122. package/dist/tracker/vitals.js +140 -0
  123. package/dist/tracker/vitals.js.map +1 -0
  124. package/dist/tracker.min.js +1 -1
  125. package/dist/vitals.min.js +1 -0
  126. package/docs/benchmarks/2026-08-20-realistic.md +76 -76
  127. package/docs/benchmarks/2026-08-21-formal-certification.md +353 -0
  128. package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
  129. package/docs/federation-setup.md +397 -0
  130. package/docs/federation.md +262 -0
  131. package/docs/upgrading.md +259 -0
  132. package/llms.txt +71 -0
  133. package/package.json +55 -11
  134. package/scripts/benchmark-ingest.mjs +175 -73
  135. package/scripts/generate-federation-keys.mjs +20 -0
  136. package/src/component/_generated/api.ts +16 -0
  137. package/src/component/_generated/component.ts +366 -4
  138. package/src/component/_generated/server.ts +4 -0
  139. package/src/component/affiliates.ts +20 -5
  140. package/src/component/cardinality.ts +117 -0
  141. package/src/component/constants.ts +38 -1
  142. package/src/component/convex.config.ts +11 -1
  143. package/src/component/coverage.ts +36 -0
  144. package/src/component/diagnostics.ts +65 -0
  145. package/src/component/errors.ts +2 -1
  146. package/src/component/eventStore.ts +217 -193
  147. package/src/component/funnels.ts +19 -16
  148. package/src/component/geo.ts +781 -0
  149. package/src/component/goals.ts +29 -21
  150. package/src/component/guards.ts +3 -1
  151. package/src/component/http.ts +357 -57
  152. package/src/component/identity.ts +74 -0
  153. package/src/component/ingest.ts +1040 -135
  154. package/src/component/live.ts +13 -7
  155. package/src/component/reports.ts +1675 -183
  156. package/src/component/retention.ts +770 -96
  157. package/src/component/sanitize.ts +144 -29
  158. package/src/component/schema.ts +200 -21
  159. package/src/component/sites.ts +27 -12
  160. package/src/component/useragent.ts +171 -0
  161. package/src/component/validators.ts +29 -13
  162. package/src/component/visitors.ts +116 -0
  163. package/src/component/vitals.ts +146 -0
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 sub-1KB tracker.",
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.1",
9
+ "version": "0.1.0-alpha.10",
10
10
  "license": "Apache-2.0",
11
11
  "publishConfig": {
12
12
  "access": "public"
@@ -27,27 +27,31 @@
27
27
  "jsdelivr": "./dist/tracker.min.js",
28
28
  "scripts": {
29
29
  "dev": "synapse dev --start 'npm run dev:build'",
30
- "dev:frontend": "cd example && vite",
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 example/dist --build-command \"npm exec vite -- build example\"",
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",
36
36
  "build:tracker": "node ./scripts/build-tracker.mjs",
37
+ "federation:keys": "node ./scripts/generate-federation-keys.mjs",
37
38
  "benchmark:ingest": "node ./scripts/benchmark-ingest.mjs",
38
- "build:codegen": "npm run build && synapse convex codegen --component-dir ./src/component && npm run build && synapse dev --once",
39
+ "benchmark:local": "vitest run src/component/benchmark.test.ts",
40
+ "format:check": "prettier --check .",
41
+ "verify:agent-docs": "node ./scripts/verify-agent-docs.mjs",
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",
39
45
  "build:clean": "npm run build:codegen",
40
- "typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex",
46
+ "typecheck": "tsc --noEmit && tsc -p control-plane && tsc -p control-plane/convex && tsc -p example/convex",
41
47
  "lint": "eslint .",
42
48
  "test": "vitest run --typecheck",
43
49
  "test:watch": "vitest --typecheck --clearScreen false",
44
50
  "test:debug": "vitest --inspect-brk --no-file-parallelism",
45
51
  "test:coverage": "vitest run --coverage --coverage.reporter=text",
52
+ "e2e": "playwright test -c control-plane/playwright.config.ts",
46
53
  "prepare": "npm run build",
47
- "preversion": "npm ci && npm run build && npm run test && npm run lint && npm run typecheck",
48
- "alpha": "npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags",
49
- "release": "npm version patch && npm publish && git push --follow-tags",
50
- "version": "(npm whoami || npm login) && vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"
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",
@@ -55,6 +59,14 @@
55
59
  "src/component/**/*.ts",
56
60
  "!src/component/**/*.test.ts",
57
61
  "scripts/benchmark-ingest.mjs",
62
+ "scripts/generate-federation-keys.mjs",
63
+ "agent/manifest.json",
64
+ "agent/manifest.schema.json",
65
+ "agent/integration.md",
66
+ "llms.txt",
67
+ "docs/federation.md",
68
+ "docs/federation-setup.md",
69
+ "docs/upgrading.md",
58
70
  "docs/benchmarks/*.md",
59
71
  "assets/rastro-live-atlas.png"
60
72
  ],
@@ -69,6 +81,7 @@
69
81
  "default": "./dist/react/index.js"
70
82
  },
71
83
  "./tracker.js": "./dist/tracker.min.js",
84
+ "./vitals.js": "./dist/vitals.min.js",
72
85
  "./test": "./src/test.ts",
73
86
  "./_generated/component.js": {
74
87
  "types": "./dist/component/_generated/component.d.ts"
@@ -89,20 +102,37 @@
89
102
  "convex": "^1.43.0",
90
103
  "react": "^18.3.1 || ^19.0.0"
91
104
  },
105
+ "peerDependenciesMeta": {
106
+ "react": {
107
+ "optional": true
108
+ }
109
+ },
92
110
  "devDependencies": {
93
111
  "@convex-dev/eslint-plugin": "^4.0.0",
94
112
  "@convex-dev/static-hosting": "^0.2.1",
113
+ "@dicebear/core": "^9.4.3",
114
+ "@dicebear/lorelei": "^9.4.2",
95
115
  "@edge-runtime/vm": "^5.0.0",
96
116
  "@eslint/eslintrc": "^3.3.6",
97
117
  "@eslint/js": "9.39.5",
98
118
  "@fontsource-variable/instrument-sans": "^5.3.0",
99
119
  "@fontsource/ibm-plex-mono": "^5.3.0",
120
+ "@playwright/test": "^1.62.1",
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",
100
126
  "@types/node": "^24.13.3",
101
127
  "@types/react": "^19.2.18",
102
128
  "@types/react-dom": "^19.2.4",
103
129
  "@typescript/native": "npm:typescript@^7.0.2",
104
130
  "@vitejs/plugin-react": "^6.0.5",
131
+ "ajv": "^8.20.0",
132
+ "ajv-formats": "^3.0.1",
105
133
  "chokidar-cli": "3.0.0",
134
+ "class-variance-authority": "^0.7.1",
135
+ "clsx": "^2.1.1",
106
136
  "convex": "^1.44.0",
107
137
  "convex-test": "0.0.55",
108
138
  "eslint": "9.39.5",
@@ -110,12 +140,16 @@
110
140
  "eslint-plugin-react-hooks": "^7.1.1",
111
141
  "eslint-plugin-react-refresh": "^0.5.3",
112
142
  "globals": "^17.9.0",
143
+ "jsdom": "^30.0.1",
113
144
  "lucide-react": "^1.32.0",
114
145
  "maplibre-gl": "^6.4.1",
115
146
  "pkg-pr-new": "^0.0.86",
116
147
  "prettier": "3.9.6",
148
+ "radix-ui": "^1.6.7",
117
149
  "react": "^19.2.8",
118
150
  "react-dom": "^19.2.8",
151
+ "tailwind-merge": "^3.6.0",
152
+ "tailwindcss": "^4.3.3",
119
153
  "terser": "^5.50.0",
120
154
  "typescript": "npm:@typescript/typescript6@^6.0.2",
121
155
  "typescript-eslint": "8.66.0",
@@ -123,5 +157,15 @@
123
157
  "vitest": "4.1.10"
124
158
  },
125
159
  "types": "./dist/client/index.d.ts",
126
- "module": "./dist/client/index.js"
160
+ "module": "./dist/client/index.js",
161
+ "dependencies": {
162
+ "@convex-dev/better-auth": "^0.12.5",
163
+ "better-auth": "~1.6.15",
164
+ "country-flag-icons": "^1.6.20",
165
+ "jose": "^6.2.10",
166
+ "simple-icons": "^16.29.0"
167
+ },
168
+ "workspaces": [
169
+ "example"
170
+ ]
127
171
  }
@@ -1,11 +1,14 @@
1
1
  import process from "node:process";
2
2
  import { Buffer } from "node:buffer";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { performance } from "node:perf_hooks";
4
5
  import { clearInterval, setInterval } from "node:timers";
5
6
  import { ConvexHttpClient } from "convex/browser";
6
7
  import { makeFunctionReference } from "convex/server";
7
8
 
8
- const overviewQuery = makeFunctionReference("example:overview");
9
+ const reconcileSessionsQuery = makeFunctionReference(
10
+ "benchmark:reconcileSessionsPage",
11
+ );
9
12
 
10
13
  const PROFILE_DEFAULTS = {
11
14
  lean: { batchSize: 50, sessions: 0 },
@@ -64,16 +67,19 @@ function parseOptions(argv) {
64
67
  const values = new Map();
65
68
  for (let index = 0; index < argv.length; index += 1) {
66
69
  const argument = argv[index];
67
- if (!argument.startsWith("--")) throw new Error(`unexpected argument: ${argument}`);
70
+ if (!argument.startsWith("--"))
71
+ throw new Error(`unexpected argument: ${argument}`);
68
72
  const [rawName, inlineValue] = argument.slice(2).split("=", 2);
69
- if (!OPTION_NAMES.has(rawName)) throw new Error(`unknown option: --${rawName}`);
73
+ if (!OPTION_NAMES.has(rawName))
74
+ throw new Error(`unknown option: --${rawName}`);
70
75
  if (values.has(rawName)) throw new Error(`duplicate option: --${rawName}`);
71
76
  if (rawName === "dry-run" || rawName === "certify") {
72
77
  values.set(rawName, "true");
73
78
  continue;
74
79
  }
75
80
  const value = inlineValue ?? argv[++index];
76
- if (!value || value.startsWith("--")) throw new Error(`missing value for --${rawName}`);
81
+ if (!value || value.startsWith("--"))
82
+ throw new Error(`missing value for --${rawName}`);
77
83
  values.set(rawName, value);
78
84
  }
79
85
  const profile = values.get("profile") ?? "realistic";
@@ -92,7 +98,10 @@ function parseOptions(argv) {
92
98
  maximumRequests: values.has("requests")
93
99
  ? positiveInteger(values.get("requests"), "requests")
94
100
  : Number.POSITIVE_INFINITY,
95
- concurrency: positiveInteger(values.get("concurrency") ?? "100", "concurrency"),
101
+ concurrency: positiveInteger(
102
+ values.get("concurrency") ?? "100",
103
+ "concurrency",
104
+ ),
96
105
  batchSize: positiveInteger(
97
106
  values.get("batch-size") ?? String(defaults.batchSize),
98
107
  "batch-size",
@@ -109,20 +118,23 @@ function parseOptions(argv) {
109
118
 
110
119
  function positiveNumber(value, name) {
111
120
  const parsed = Number(value);
112
- if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`--${name} must be positive`);
121
+ if (!Number.isFinite(parsed) || parsed <= 0)
122
+ throw new Error(`--${name} must be positive`);
113
123
  return parsed;
114
124
  }
115
125
 
116
126
  function positiveInteger(value, name) {
117
127
  const parsed = positiveNumber(value, name);
118
- if (!Number.isSafeInteger(parsed)) throw new Error(`--${name} must be an integer`);
128
+ if (!Number.isSafeInteger(parsed))
129
+ throw new Error(`--${name} must be an integer`);
119
130
  return parsed;
120
131
  }
121
132
 
122
133
  function eventFor(profile, session, sequence, timestamp) {
123
- const path = profile === "lean"
124
- ? `/benchmark/lean/${sequence % 4}`
125
- : REALISTIC_PATHS[sequence % REALISTIC_PATHS.length];
134
+ const path =
135
+ profile === "lean"
136
+ ? `/benchmark/lean/${sequence % 4}`
137
+ : REALISTIC_PATHS[sequence % REALISTIC_PATHS.length];
126
138
  const base = {
127
139
  eventId: `${session.id}.${sequence.toString(36)}`,
128
140
  sessionId: session.id,
@@ -148,10 +160,20 @@ function eventFor(profile, session, sequence, timestamp) {
148
160
  if (profile === "feature") {
149
161
  const step = sequence % 3;
150
162
  if (step === 0) {
151
- return { ...base, type: "pageview", path: "/", affiliateSlug: "creator-club" };
163
+ return {
164
+ ...base,
165
+ type: "pageview",
166
+ path: "/",
167
+ affiliateSlug: "creator-club",
168
+ };
152
169
  }
153
170
  if (step === 1) {
154
- return { ...base, type: "pageview", path: "/pricing", affiliateSlug: "creator-club" };
171
+ return {
172
+ ...base,
173
+ type: "pageview",
174
+ path: "/pricing",
175
+ affiliateSlug: "creator-club",
176
+ };
155
177
  }
156
178
  return {
157
179
  ...base,
@@ -169,7 +191,10 @@ function eventFor(profile, session, sequence, timestamp) {
169
191
  return {
170
192
  ...base,
171
193
  type: "pageview",
172
- referrer: sequence % 2 === 0 ? "https://www.google.com/search?q=rastro" : undefined,
194
+ referrer:
195
+ sequence % 2 === 0
196
+ ? "https://www.google.com/search?q=rastro"
197
+ : undefined,
173
198
  };
174
199
  }
175
200
  if (kind < 13) return { ...base, type: "click", target: "Get started" };
@@ -181,7 +206,11 @@ function eventFor(profile, session, sequence, timestamp) {
181
206
  properties: { placement: "hero", experiment: `variant-${sequence % 3}` },
182
207
  };
183
208
  }
184
- return { ...base, type: "outbound", href: "https://github.com/amageweb/amage-rastro" };
209
+ return {
210
+ ...base,
211
+ type: "outbound",
212
+ href: "https://github.com/amageweb/amage-rastro",
213
+ };
185
214
  }
186
215
 
187
216
  function requestContext(session) {
@@ -197,7 +226,19 @@ function requestContext(session) {
197
226
 
198
227
  function percentile(sorted, fraction) {
199
228
  if (sorted.length === 0) return 0;
200
- return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)];
229
+ return sorted[
230
+ Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)
231
+ ];
232
+ }
233
+
234
+ function hasValidSessionTotals(value) {
235
+ return (
236
+ value !== null &&
237
+ typeof value === "object" &&
238
+ [value.requestedSessions, value.foundSessions, value.storedEvents].every(
239
+ (total) => Number.isSafeInteger(total) && total >= 0,
240
+ )
241
+ );
201
242
  }
202
243
 
203
244
  async function main() {
@@ -212,7 +253,7 @@ async function main() {
212
253
  if (options.certify && Number.isFinite(options.maximumRequests)) {
213
254
  throw new Error("--certify cannot be combined with --requests");
214
255
  }
215
- const runId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
256
+ const runId = randomUUID();
216
257
  const sessions = Array.from({ length: options.sessionCount }, (_, index) => ({
217
258
  id: `bench-${runId}-${index}`,
218
259
  visitorId: `visitor-${runId}-${index}`,
@@ -238,15 +279,16 @@ async function main() {
238
279
  const durations = [];
239
280
 
240
281
  const buildRequest = (requestIndex) => {
241
- const session = options.profile === "lean"
242
- ? {
243
- id: `bench-${runId}-${requestIndex}`,
244
- visitorId: `visitor-${runId}-${requestIndex}`,
245
- sequence: 0,
246
- locationIndex: requestIndex % LOCATIONS.length,
247
- userAgentIndex: requestIndex % USER_AGENTS.length,
248
- }
249
- : sessions[requestIndex % sessions.length];
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];
250
292
  const sequenceStart = session.sequence;
251
293
  session.sequence += options.batchSize;
252
294
  const now = Date.now();
@@ -265,18 +307,26 @@ async function main() {
265
307
  throw new Error("generated request exceeds the 64 KiB HTTP limit");
266
308
  }
267
309
  if (options.dryRun) {
268
- process.stdout.write(`${JSON.stringify({
269
- profile: options.profile,
270
- batchSize: options.batchSize,
271
- requestBytes: Buffer.byteLength(dryRequest.body),
272
- heartbeats: dryRequest.heartbeats,
273
- location: requestContext(sessions[0]),
274
- }, null, 2)}\n`);
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
+ );
275
323
  return;
276
324
  }
277
325
  if (options.profile !== "lean") sessions[0].sequence = 0;
278
326
  if (!options.url || !options.siteId) {
279
- throw new Error("--url and --site-id are required unless --dry-run is used");
327
+ throw new Error(
328
+ "--url and --site-id are required unless --dry-run is used",
329
+ );
280
330
  }
281
331
 
282
332
  const stopForSignal = (signal) => {
@@ -288,20 +338,25 @@ async function main() {
288
338
  process.once("SIGINT", stopForInterrupt);
289
339
  process.once("SIGTERM", stopForTermination);
290
340
 
291
- const wallClockStartedAt = Date.now();
292
- const reconciliationFrom = Math.floor(wallClockStartedAt / 3_600_000) * 3_600_000;
293
- const reconciliationTo = Math.ceil(
294
- (wallClockStartedAt + options.durationSeconds * 1_000 + 60_000) / 3_600_000,
295
- ) * 3_600_000 - 1;
296
- const convex = options.convexUrl ? new ConvexHttpClient(options.convexUrl) : null;
297
- const aggregateBefore = convex
298
- ? await convex.query(overviewQuery, {
299
- siteIds: [options.siteId],
300
- from: reconciliationFrom,
301
- to: reconciliationTo,
302
- interval: "hour",
303
- })
341
+ const convex = options.convexUrl
342
+ ? new ConvexHttpClient(options.convexUrl)
304
343
  : null;
344
+ if (convex) {
345
+ const preflight = await convex.query(reconcileSessionsQuery, {
346
+ siteId: options.siteId,
347
+ runId,
348
+ start: 0,
349
+ count: 1,
350
+ });
351
+ if (
352
+ !hasValidSessionTotals(preflight) ||
353
+ preflight.requestedSessions !== 1 ||
354
+ preflight.foundSessions !== 0 ||
355
+ preflight.storedEvents !== 0
356
+ ) {
357
+ throw new Error("reconciliation preflight returned unexpected totals");
358
+ }
359
+ }
305
360
  const startedAt = performance.now();
306
361
  const deadline = startedAt + options.durationSeconds * 1_000;
307
362
  const recordError = (key) => errors.set(key, (errors.get(key) ?? 0) + 1);
@@ -319,7 +374,9 @@ async function main() {
319
374
  const request = buildRequest(requestIndex);
320
375
  const requestBytes = Buffer.byteLength(request.body);
321
376
  if (requestBytes > 64 * 1024) {
322
- throw new Error(`generated request ${requestIndex} exceeds the 64 KiB HTTP limit`);
377
+ throw new Error(
378
+ `generated request ${requestIndex} exceeds the 64 KiB HTTP limit`,
379
+ );
323
380
  }
324
381
  sentBytes += requestBytes;
325
382
  sentEvents += options.batchSize;
@@ -354,7 +411,8 @@ async function main() {
354
411
  ![result.accepted, result.duplicates, result.rejected].every(
355
412
  (value) => Number.isSafeInteger(value) && value >= 0,
356
413
  ) ||
357
- result.accepted + result.duplicates + result.rejected !== options.batchSize
414
+ result.accepted + result.duplicates + result.rejected !==
415
+ options.batchSize
358
416
  ) {
359
417
  throw new Error("response has invalid event accounting");
360
418
  }
@@ -370,7 +428,9 @@ async function main() {
370
428
  } catch (error) {
371
429
  failures += 1;
372
430
  failedEvents += options.batchSize;
373
- recordError(`fetch:${error instanceof Error ? error.message : "unknown"}`);
431
+ recordError(
432
+ `fetch:${error instanceof Error ? error.message : "unknown"}`,
433
+ );
374
434
  } finally {
375
435
  durations.push(performance.now() - requestStartedAt);
376
436
  completedRequests += 1;
@@ -379,11 +439,14 @@ async function main() {
379
439
  };
380
440
 
381
441
  const progress = setInterval(() => {
382
- const elapsedSeconds = Math.max(0.001, (performance.now() - startedAt) / 1_000);
442
+ const elapsedSeconds = Math.max(
443
+ 0.001,
444
+ (performance.now() - startedAt) / 1_000,
445
+ );
383
446
  process.stderr.write(
384
447
  `[${options.label}] ${Math.round(elapsedSeconds)}s ` +
385
- `${accepted} accepted ${Math.round(accepted / elapsedSeconds)} events/s ` +
386
- `${failures} failures\n`,
448
+ `${accepted} accepted ${Math.round(accepted / elapsedSeconds)} events/s ` +
449
+ `${failures} failures\n`,
387
450
  );
388
451
  }, options.progressSeconds * 1_000);
389
452
  progress.unref();
@@ -391,32 +454,69 @@ async function main() {
391
454
  clearInterval(progress);
392
455
 
393
456
  const elapsedMs = performance.now() - startedAt;
394
- terminationReason ??= performance.now() >= deadline ? "duration" : "completed";
395
- const aggregateAfter = convex
396
- ? await convex.query(overviewQuery, {
397
- siteIds: [options.siteId],
398
- from: reconciliationFrom,
399
- to: reconciliationTo,
400
- interval: "hour",
401
- })
402
- : null;
457
+ terminationReason ??=
458
+ performance.now() >= deadline ? "duration" : "completed";
403
459
  process.removeListener("SIGINT", stopForInterrupt);
404
460
  process.removeListener("SIGTERM", stopForTermination);
405
461
  const durationCompleted = terminationReason === "duration";
406
462
  durations.sort((left, right) => left - right);
407
- const storedEvents = accepted - acceptedHeartbeats;
408
- const reconciliation = aggregateBefore && aggregateAfter
463
+ const expectedSessions =
464
+ options.profile === "lean"
465
+ ? nextRequest
466
+ : Math.min(nextRequest, options.sessionCount);
467
+ let sessionTotals = null;
468
+ if (convex && expectedSessions > 0) {
469
+ try {
470
+ sessionTotals = {
471
+ requestedSessions: 0,
472
+ foundSessions: 0,
473
+ storedEvents: 0,
474
+ };
475
+ for (let start = 0; start < expectedSessions; start += 100) {
476
+ const page = await convex.query(reconcileSessionsQuery, {
477
+ siteId: options.siteId,
478
+ runId,
479
+ start,
480
+ count: Math.min(100, expectedSessions - start),
481
+ });
482
+ if (!hasValidSessionTotals(page)) {
483
+ throw new Error("reconciliation response has invalid totals");
484
+ }
485
+ sessionTotals.requestedSessions += page.requestedSessions;
486
+ sessionTotals.foundSessions += page.foundSessions;
487
+ sessionTotals.storedEvents += page.storedEvents;
488
+ }
489
+ } catch (error) {
490
+ reconciliationReliable = false;
491
+ sessionTotals = null;
492
+ recordError(
493
+ `reconciliation:${error instanceof Error ? error.message : "unknown"}`,
494
+ );
495
+ }
496
+ }
497
+ const expectedStoredEvents = reconciliationReliable
498
+ ? accepted - acceptedHeartbeats
499
+ : null;
500
+ const storedEvents = sessionTotals?.storedEvents ?? expectedStoredEvents;
501
+ const reconciliation = sessionTotals
409
502
  ? {
410
- scope: "site-wide-delta",
503
+ scope: "run-session-ids",
411
504
  reliable: reconciliationReliable && failures === 0 && rejected === 0,
412
- expectedStoredEvents: storedEvents,
413
- aggregateEventsDelta:
414
- aggregateAfter.totals.events - aggregateBefore.totals.events,
505
+ expectedSessions,
506
+ requestedSessions: sessionTotals.requestedSessions,
507
+ foundSessions: sessionTotals.foundSessions,
508
+ acceptedEvents: accepted,
509
+ acceptedHeartbeats: reconciliationReliable ? acceptedHeartbeats : null,
510
+ expectedStoredEvents,
511
+ storedEvents: sessionTotals.storedEvents,
415
512
  matches:
416
513
  reconciliationReliable &&
417
514
  failures === 0 &&
418
515
  rejected === 0 &&
419
- aggregateAfter.totals.events - aggregateBefore.totals.events === storedEvents,
516
+ expectedStoredEvents !== null &&
517
+ sessionTotals.requestedSessions === expectedSessions &&
518
+ sessionTotals.foundSessions === expectedSessions &&
519
+ sessionTotals.storedEvents === expectedStoredEvents,
420
520
  }
421
521
  : null;
422
522
  const cleanRun =
@@ -441,7 +541,8 @@ async function main() {
441
541
  },
442
542
  concurrency: options.concurrency,
443
543
  batchSize: options.batchSize,
444
- sessionPool: options.profile === "lean" ? "unique-per-request" : options.sessionCount,
544
+ sessionPool:
545
+ options.profile === "lean" ? "unique-per-request" : options.sessionCount,
445
546
  maximumRequests: Number.isFinite(options.maximumRequests)
446
547
  ? options.maximumRequests
447
548
  : null,
@@ -458,15 +559,16 @@ async function main() {
458
559
  errors: Object.fromEntries(errors),
459
560
  throughput: {
460
561
  eventsPerSecond: Math.round((accepted * 1_000) / elapsedMs),
461
- storedEventsPerSecond: reconciliationReliable
562
+ storedEventsPerSecond: reconciliationPassed
462
563
  ? Math.round((storedEvents * 1_000) / elapsedMs)
463
564
  : null,
464
565
  estimatedEventsPerDay: durationCompleted
465
566
  ? Math.round((accepted * 86_400_000) / elapsedMs)
466
567
  : null,
467
- estimatedStoredEventsPerDay: durationCompleted && reconciliationReliable
468
- ? Math.round((storedEvents * 86_400_000) / elapsedMs)
469
- : null,
568
+ estimatedStoredEventsPerDay:
569
+ durationCompleted && reconciliationPassed
570
+ ? Math.round((storedEvents * 86_400_000) / elapsedMs)
571
+ : null,
470
572
  certifiedEventsPerDay: certificationPassed
471
573
  ? Math.round((accepted * 86_400_000) / elapsedMs)
472
574
  : null,
@@ -0,0 +1,20 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import process from "node:process";
3
+ import { exportJWK, exportPKCS8, generateKeyPair } from "jose";
4
+
5
+ const { privateKey, publicKey } = await generateKeyPair("RS256", {
6
+ extractable: true,
7
+ });
8
+ const publicJwk = await exportJWK(publicKey);
9
+
10
+ process.stdout.write(
11
+ JSON.stringify(
12
+ {
13
+ keyId: `rastro-${randomUUID()}`,
14
+ privateKeyPem: await exportPKCS8(privateKey),
15
+ publicJwk,
16
+ },
17
+ null,
18
+ 2,
19
+ ) + "\n",
20
+ );
@@ -9,20 +9,28 @@
9
9
  */
10
10
 
11
11
  import type * as affiliates from "../affiliates.js";
12
+ import type * as cardinality from "../cardinality.js";
12
13
  import type * as constants from "../constants.js";
14
+ import type * as coverage from "../coverage.js";
15
+ import type * as diagnostics from "../diagnostics.js";
13
16
  import type * as errors from "../errors.js";
14
17
  import type * as eventStore from "../eventStore.js";
15
18
  import type * as funnels from "../funnels.js";
19
+ import type * as geo from "../geo.js";
16
20
  import type * as goals from "../goals.js";
17
21
  import type * as guards from "../guards.js";
18
22
  import type * as http from "../http.js";
23
+ import type * as identity from "../identity.js";
19
24
  import type * as ingest from "../ingest.js";
20
25
  import type * as live from "../live.js";
21
26
  import type * as reports from "../reports.js";
22
27
  import type * as retention from "../retention.js";
23
28
  import type * as sanitize from "../sanitize.js";
24
29
  import type * as sites from "../sites.js";
30
+ import type * as useragent from "../useragent.js";
25
31
  import type * as validators from "../validators.js";
32
+ import type * as visitors from "../visitors.js";
33
+ import type * as vitals from "../vitals.js";
26
34
 
27
35
  import type {
28
36
  ApiFromModules,
@@ -33,20 +41,28 @@ import { anyApi, componentsGeneric } from "convex/server";
33
41
 
34
42
  const fullApi: ApiFromModules<{
35
43
  affiliates: typeof affiliates;
44
+ cardinality: typeof cardinality;
36
45
  constants: typeof constants;
46
+ coverage: typeof coverage;
47
+ diagnostics: typeof diagnostics;
37
48
  errors: typeof errors;
38
49
  eventStore: typeof eventStore;
39
50
  funnels: typeof funnels;
51
+ geo: typeof geo;
40
52
  goals: typeof goals;
41
53
  guards: typeof guards;
42
54
  http: typeof http;
55
+ identity: typeof identity;
43
56
  ingest: typeof ingest;
44
57
  live: typeof live;
45
58
  reports: typeof reports;
46
59
  retention: typeof retention;
47
60
  sanitize: typeof sanitize;
48
61
  sites: typeof sites;
62
+ useragent: typeof useragent;
49
63
  validators: typeof validators;
64
+ visitors: typeof visitors;
65
+ vitals: typeof vitals;
50
66
  }> = anyApi as any;
51
67
 
52
68
  /**