@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.
- package/README.md +521 -66
- 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 +16 -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 +25 -1
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +38 -1
- 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 +21 -9
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/eventStore.js +142 -152
- 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 +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 +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 +248 -49
- 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 +604 -55
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +7 -6
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +259 -10
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +1110 -124
- 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 +537 -32
- 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 +96 -16
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +378 -62
- package/dist/component/schema.js +178 -20
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +11 -7
- 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 -65
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +19 -13
- 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 +57 -20
- 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 +76 -76
- 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 +55 -11
- package/scripts/benchmark-ingest.mjs +175 -73
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +16 -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 +38 -1
- 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 +217 -193
- package/src/component/funnels.ts +19 -16
- package/src/component/geo.ts +781 -0
- package/src/component/goals.ts +29 -21
- package/src/component/guards.ts +3 -1
- package/src/component/http.ts +357 -57
- package/src/component/identity.ts +74 -0
- package/src/component/ingest.ts +1040 -135
- package/src/component/live.ts +13 -7
- package/src/component/reports.ts +1675 -183
- package/src/component/retention.ts +770 -96
- package/src/component/sanitize.ts +144 -29
- package/src/component/schema.ts +200 -21
- package/src/component/sites.ts +27 -12
- package/src/component/useragent.ts +171 -0
- package/src/component/validators.ts +29 -13
- package/src/component/visitors.ts +116 -0
- 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
|
|
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.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
|
|
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",
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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("--"))
|
|
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))
|
|
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("--"))
|
|
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(
|
|
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)
|
|
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))
|
|
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 =
|
|
124
|
-
|
|
125
|
-
|
|
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 {
|
|
163
|
+
return {
|
|
164
|
+
...base,
|
|
165
|
+
type: "pageview",
|
|
166
|
+
path: "/",
|
|
167
|
+
affiliateSlug: "creator-club",
|
|
168
|
+
};
|
|
152
169
|
}
|
|
153
170
|
if (step === 1) {
|
|
154
|
-
return {
|
|
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:
|
|
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 {
|
|
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[
|
|
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 =
|
|
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 =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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(
|
|
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
|
|
292
|
-
|
|
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(
|
|
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 !==
|
|
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(
|
|
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(
|
|
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
|
-
|
|
386
|
-
|
|
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 ??=
|
|
395
|
-
|
|
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
|
|
408
|
-
|
|
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: "
|
|
503
|
+
scope: "run-session-ids",
|
|
411
504
|
reliable: reconciliationReliable && failures === 0 && rejected === 0,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
468
|
-
|
|
469
|
-
|
|
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
|
/**
|