@iann29/rastro 0.1.0-alpha.5 → 0.1.0-alpha.7
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 +82 -29
- package/agent/integration.md +17 -15
- package/agent/manifest.json +7 -5
- 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 +418 -21
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +191 -41
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -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 +63 -7
- 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 +7 -1
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +13 -1
- 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 +2 -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 +51 -17
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts +2 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +143 -23
- 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 +60 -3
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +215 -49
- 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 +11 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +58 -11
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +47 -5
- package/dist/component/schema.js +21 -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/validators.d.ts +14 -9
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +5 -1
- package/dist/component/validators.js.map +1 -1
- package/dist/component/visitors.d.ts.map +1 -1
- package/dist/component/visitors.js.map +1 -1
- 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 +3 -1
- 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 +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/upgrading.md +46 -8
- package/llms.txt +5 -1
- package/package.json +17 -3
- package/scripts/benchmark-ingest.mjs +101 -48
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +69 -4
- package/src/component/affiliates.ts +20 -5
- package/src/component/cardinality.ts +8 -7
- package/src/component/constants.ts +13 -1
- 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 +93 -54
- package/src/component/ingest.ts +320 -127
- package/src/component/live.ts +5 -4
- package/src/component/reports.ts +563 -219
- package/src/component/retention.ts +221 -98
- package/src/component/sanitize.ts +99 -29
- package/src/component/schema.ts +31 -5
- package/src/component/sites.ts +22 -11
- package/src/component/validators.ts +16 -7
- package/src/component/visitors.ts +16 -5
- package/src/component/vitals.ts +146 -0
|
@@ -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,
|
|
@@ -30,6 +30,7 @@ import type * as sites from "../sites.js";
|
|
|
30
30
|
import type * as useragent from "../useragent.js";
|
|
31
31
|
import type * as validators from "../validators.js";
|
|
32
32
|
import type * as visitors from "../visitors.js";
|
|
33
|
+
import type * as vitals from "../vitals.js";
|
|
33
34
|
|
|
34
35
|
import type {
|
|
35
36
|
ApiFromModules,
|
|
@@ -61,6 +62,7 @@ const fullApi: ApiFromModules<{
|
|
|
61
62
|
useragent: typeof useragent;
|
|
62
63
|
validators: typeof validators;
|
|
63
64
|
visitors: typeof visitors;
|
|
65
|
+
vitals: typeof vitals;
|
|
64
66
|
}> = anyApi as any;
|
|
65
67
|
|
|
66
68
|
/**
|
|
@@ -207,7 +207,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
207
207
|
| "custom"
|
|
208
208
|
| "conversion"
|
|
209
209
|
| "heartbeat"
|
|
210
|
-
| "outbound"
|
|
210
|
+
| "outbound"
|
|
211
|
+
| "vital";
|
|
212
|
+
value?: number;
|
|
211
213
|
visitorId: string;
|
|
212
214
|
}>;
|
|
213
215
|
origin: string;
|
|
@@ -303,7 +305,8 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
303
305
|
| "conversions"
|
|
304
306
|
| "goals"
|
|
305
307
|
| "funnels"
|
|
306
|
-
| "affiliates"
|
|
308
|
+
| "affiliates"
|
|
309
|
+
| "vitals";
|
|
307
310
|
}>;
|
|
308
311
|
from: number;
|
|
309
312
|
siteId: string;
|
|
@@ -691,7 +694,8 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
691
694
|
| "custom"
|
|
692
695
|
| "conversion"
|
|
693
696
|
| "heartbeat"
|
|
694
|
-
| "outbound"
|
|
697
|
+
| "outbound"
|
|
698
|
+
| "vital";
|
|
695
699
|
visitorId: string;
|
|
696
700
|
}>;
|
|
697
701
|
pageStatus?: "SplitRecommended" | "SplitRequired" | null;
|
|
@@ -739,11 +743,69 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
739
743
|
| "custom"
|
|
740
744
|
| "conversion"
|
|
741
745
|
| "heartbeat"
|
|
742
|
-
| "outbound"
|
|
746
|
+
| "outbound"
|
|
747
|
+
| "vital";
|
|
743
748
|
visitorId: string;
|
|
744
749
|
}>,
|
|
745
750
|
Name
|
|
746
751
|
>;
|
|
752
|
+
vitalsReport: FunctionReference<
|
|
753
|
+
"query",
|
|
754
|
+
"internal",
|
|
755
|
+
{ from: number; siteId: string; to: number },
|
|
756
|
+
{
|
|
757
|
+
metadata: {
|
|
758
|
+
basis: "dailyRollup" | "boundedLegacy";
|
|
759
|
+
coverage: {
|
|
760
|
+
availableFrom: number | null;
|
|
761
|
+
availableThrough: number | null;
|
|
762
|
+
generation: number | null;
|
|
763
|
+
retainedBefore: number | null;
|
|
764
|
+
state: "complete" | "partial" | "retained" | "unavailable";
|
|
765
|
+
};
|
|
766
|
+
range: {
|
|
767
|
+
boundary: "inclusive";
|
|
768
|
+
from: number;
|
|
769
|
+
timezone: "UTC";
|
|
770
|
+
to: number;
|
|
771
|
+
};
|
|
772
|
+
};
|
|
773
|
+
metrics: Array<{
|
|
774
|
+
average: number;
|
|
775
|
+
count: number;
|
|
776
|
+
devices: Array<{
|
|
777
|
+
average: number;
|
|
778
|
+
count: number;
|
|
779
|
+
device: string;
|
|
780
|
+
good: number;
|
|
781
|
+
needsImprovement: number;
|
|
782
|
+
p75: number;
|
|
783
|
+
poor: number;
|
|
784
|
+
}>;
|
|
785
|
+
good: number;
|
|
786
|
+
metric: "LCP" | "CLS" | "INP" | "FCP" | "TTFB";
|
|
787
|
+
needsImprovement: number;
|
|
788
|
+
p75: number;
|
|
789
|
+
pages: Array<{
|
|
790
|
+
average: number;
|
|
791
|
+
count: number;
|
|
792
|
+
good: number;
|
|
793
|
+
needsImprovement: number;
|
|
794
|
+
p75: number;
|
|
795
|
+
page: string;
|
|
796
|
+
poor: number;
|
|
797
|
+
}>;
|
|
798
|
+
poor: number;
|
|
799
|
+
timeSeries: Array<{
|
|
800
|
+
count: number;
|
|
801
|
+
p75: number;
|
|
802
|
+
timestamp: number;
|
|
803
|
+
}>;
|
|
804
|
+
}>;
|
|
805
|
+
truncated: boolean;
|
|
806
|
+
},
|
|
807
|
+
Name
|
|
808
|
+
>;
|
|
747
809
|
};
|
|
748
810
|
retention: {
|
|
749
811
|
cleanup: FunctionReference<
|
|
@@ -765,6 +827,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
765
827
|
| "goalRollups"
|
|
766
828
|
| "funnelRollups"
|
|
767
829
|
| "affiliateRollups"
|
|
830
|
+
| "vitalRollups"
|
|
768
831
|
| "attributions"
|
|
769
832
|
| "liveSessions"
|
|
770
833
|
| "ingestWindows"
|
|
@@ -799,6 +862,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
799
862
|
| "goalRollups"
|
|
800
863
|
| "funnelRollups"
|
|
801
864
|
| "affiliateRollups"
|
|
865
|
+
| "vitalRollups"
|
|
802
866
|
| "attributions"
|
|
803
867
|
| "liveSessions"
|
|
804
868
|
| "ingestWindows"
|
|
@@ -844,6 +908,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
844
908
|
| "goalRollups"
|
|
845
909
|
| "funnelRollups"
|
|
846
910
|
| "affiliateRollups"
|
|
911
|
+
| "vitalRollups"
|
|
847
912
|
| "attributions"
|
|
848
913
|
| "liveSessions"
|
|
849
914
|
| "ingestWindows"
|
|
@@ -22,16 +22,27 @@ export const upsert = mutation({
|
|
|
22
22
|
},
|
|
23
23
|
returns: v.id("affiliates"),
|
|
24
24
|
handler: async (ctx, args) => {
|
|
25
|
-
if (!(await ctx.db.get("sites", args.siteId)))
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
if (!(await ctx.db.get("sites", args.siteId)))
|
|
26
|
+
fail("NOT_FOUND", "site not found");
|
|
27
|
+
if (
|
|
28
|
+
!Number.isSafeInteger(args.commissionBps) ||
|
|
29
|
+
args.commissionBps < 0 ||
|
|
30
|
+
args.commissionBps > 10_000
|
|
31
|
+
) {
|
|
32
|
+
fail(
|
|
33
|
+
"INVALID_ARGUMENT",
|
|
34
|
+
"commissionBps must be an integer between 0 and 10000",
|
|
35
|
+
);
|
|
28
36
|
}
|
|
29
37
|
if (
|
|
30
38
|
!Number.isSafeInteger(args.attributionWindowDays) ||
|
|
31
39
|
args.attributionWindowDays < 1 ||
|
|
32
40
|
args.attributionWindowDays > 365
|
|
33
41
|
) {
|
|
34
|
-
fail(
|
|
42
|
+
fail(
|
|
43
|
+
"INVALID_ARGUMENT",
|
|
44
|
+
"attributionWindowDays must be an integer between 1 and 365",
|
|
45
|
+
);
|
|
35
46
|
}
|
|
36
47
|
let slug: string;
|
|
37
48
|
try {
|
|
@@ -52,7 +63,11 @@ export const upsert = mutation({
|
|
|
52
63
|
)
|
|
53
64
|
.unique();
|
|
54
65
|
const affiliateId = args.affiliateId ?? existingBySlug?._id;
|
|
55
|
-
if (
|
|
66
|
+
if (
|
|
67
|
+
args.affiliateId &&
|
|
68
|
+
existingBySlug &&
|
|
69
|
+
existingBySlug._id !== args.affiliateId
|
|
70
|
+
) {
|
|
56
71
|
fail("CONFLICT", "another affiliate already uses this slug");
|
|
57
72
|
}
|
|
58
73
|
const now = Date.now();
|
|
@@ -60,13 +60,14 @@ export function unionVisitorSketches(
|
|
|
60
60
|
export function estimateVisitors(sketch: VisitorSketch): number {
|
|
61
61
|
const registers = checkedRegisters(sketch);
|
|
62
62
|
const count = registers.length;
|
|
63
|
-
const alpha =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
const alpha =
|
|
64
|
+
count === 16
|
|
65
|
+
? 0.673
|
|
66
|
+
: count === 32
|
|
67
|
+
? 0.697
|
|
68
|
+
: count === 64
|
|
69
|
+
? 0.709
|
|
70
|
+
: 0.7213 / (1 + 1.079 / count);
|
|
70
71
|
let denominator = 0;
|
|
71
72
|
let empty = 0;
|
|
72
73
|
for (const register of registers) {
|
|
@@ -7,8 +7,9 @@ export const MAX_BYTES_PER_SESSION_WINDOW = 256 * 1024;
|
|
|
7
7
|
export const SITE_INGEST_SHARDS = 4_096;
|
|
8
8
|
export const MAX_SITE_EVENTS_PER_SHARD_WINDOW = 300;
|
|
9
9
|
export const MAX_SITE_BYTES_PER_SHARD_WINDOW = 512 * 1024;
|
|
10
|
-
export const LIVE_SESSION_TTL_MS = 60_000;
|
|
10
|
+
export const LIVE_SESSION_TTL_MS = 5 * 60_000;
|
|
11
11
|
export const LIVE_SWEEP_INTERVAL_MS = 5_000;
|
|
12
|
+
export const CLOCK_SKEW_TOLERANCE_MS = 30_000;
|
|
12
13
|
export const AGGREGATE_SHARDS = 128;
|
|
13
14
|
export const HOURLY_AGGREGATE_SHARDS = 256;
|
|
14
15
|
export const SECONDARY_AGGREGATE_SHARDS = 16;
|
|
@@ -36,6 +37,17 @@ export const FEATURE_REPORT_DOCUMENT_RESERVE = 500;
|
|
|
36
37
|
export const REPORT_ROLLUP_SHARDS = 2;
|
|
37
38
|
export const ROLLUP_SAFETY_DELAY_MS = 5 * 60_000;
|
|
38
39
|
export const RETENTION_LEASE_MS = 60_000;
|
|
40
|
+
// Shared Web Vitals histogram edges. Every Google rating threshold (LCP
|
|
41
|
+
// 2500/4000, CLS×1000 100/250, INP 200/500, FCP 1800/3000, TTFB 800/1800) is
|
|
42
|
+
// an edge, so rating counts stay exact while one layout serves all metrics.
|
|
43
|
+
export const VITAL_HISTOGRAM_EDGES = [
|
|
44
|
+
100, 200, 250, 300, 500, 800, 1_000, 1_200, 1_800, 2_500, 3_000, 4_000, 5_000,
|
|
45
|
+
7_500, 10_000,
|
|
46
|
+
];
|
|
47
|
+
export const MAX_VITAL_VALUE = 600_000;
|
|
48
|
+
export const VITAL_PAGE_SLOTS = 48;
|
|
49
|
+
export const MAX_VITALS_REPORT_ROWS = 5_000;
|
|
50
|
+
export const VITALS_REPORT_PAGE_LIMIT = 12;
|
|
39
51
|
|
|
40
52
|
export const DAY_MS = 86_400_000;
|
|
41
53
|
export const HOUR_MS = 3_600_000;
|
|
@@ -29,7 +29,8 @@ export const sessionEventTotals = query({
|
|
|
29
29
|
try {
|
|
30
30
|
sessionIds = args.sessionIds.map((sessionId) => {
|
|
31
31
|
const sanitized = sanitizeOpaqueId(sessionId, "sessionId");
|
|
32
|
-
if (sanitized !== sessionId)
|
|
32
|
+
if (sanitized !== sessionId)
|
|
33
|
+
throw new Error("sessionId is not canonical");
|
|
33
34
|
return sanitized;
|
|
34
35
|
});
|
|
35
36
|
} catch (error) {
|
|
@@ -49,7 +50,7 @@ export const sessionEventTotals = query({
|
|
|
49
50
|
.withIndex("by_siteId_and_sessionId", (range) =>
|
|
50
51
|
range.eq("siteId", args.siteId).eq("sessionId", sessionId),
|
|
51
52
|
)
|
|
52
|
-
.unique()
|
|
53
|
+
.unique(),
|
|
53
54
|
),
|
|
54
55
|
);
|
|
55
56
|
return {
|