@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.
Files changed (125) hide show
  1. package/README.md +82 -29
  2. package/agent/integration.md +17 -15
  3. package/agent/manifest.json +7 -5
  4. package/agent/manifest.schema.json +8 -5
  5. package/dist/client/federation.d.ts +34 -8
  6. package/dist/client/federation.d.ts.map +1 -1
  7. package/dist/client/federation.js +17 -3
  8. package/dist/client/federation.js.map +1 -1
  9. package/dist/client/index.d.ts +418 -21
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +191 -41
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +2 -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 +63 -7
  17. package/dist/component/_generated/component.d.ts.map +1 -1
  18. package/dist/component/affiliates.d.ts.map +1 -1
  19. package/dist/component/affiliates.js +6 -2
  20. package/dist/component/affiliates.js.map +1 -1
  21. package/dist/component/cardinality.d.ts.map +1 -1
  22. package/dist/component/cardinality.js.map +1 -1
  23. package/dist/component/constants.d.ts +7 -1
  24. package/dist/component/constants.d.ts.map +1 -1
  25. package/dist/component/constants.js +13 -1
  26. package/dist/component/constants.js.map +1 -1
  27. package/dist/component/diagnostics.d.ts.map +1 -1
  28. package/dist/component/diagnostics.js.map +1 -1
  29. package/dist/component/eventStore.d.ts +2 -1
  30. package/dist/component/eventStore.d.ts.map +1 -1
  31. package/dist/component/eventStore.js +13 -10
  32. package/dist/component/eventStore.js.map +1 -1
  33. package/dist/component/funnels.d.ts.map +1 -1
  34. package/dist/component/funnels.js +5 -3
  35. package/dist/component/funnels.js.map +1 -1
  36. package/dist/component/geo.d.ts.map +1 -1
  37. package/dist/component/geo.js +3 -2
  38. package/dist/component/geo.js.map +1 -1
  39. package/dist/component/goals.d.ts.map +1 -1
  40. package/dist/component/goals.js +8 -5
  41. package/dist/component/goals.js.map +1 -1
  42. package/dist/component/guards.d.ts.map +1 -1
  43. package/dist/component/guards.js.map +1 -1
  44. package/dist/component/http.d.ts.map +1 -1
  45. package/dist/component/http.js +51 -17
  46. package/dist/component/http.js.map +1 -1
  47. package/dist/component/ingest.d.ts +2 -1
  48. package/dist/component/ingest.d.ts.map +1 -1
  49. package/dist/component/ingest.js +143 -23
  50. package/dist/component/ingest.js.map +1 -1
  51. package/dist/component/live.d.ts.map +1 -1
  52. package/dist/component/live.js +1 -3
  53. package/dist/component/live.js.map +1 -1
  54. package/dist/component/reports.d.ts +60 -3
  55. package/dist/component/reports.d.ts.map +1 -1
  56. package/dist/component/reports.js +215 -49
  57. package/dist/component/reports.js.map +1 -1
  58. package/dist/component/retention.d.ts +4 -4
  59. package/dist/component/retention.d.ts.map +1 -1
  60. package/dist/component/retention.js +54 -22
  61. package/dist/component/retention.js.map +1 -1
  62. package/dist/component/sanitize.d.ts +11 -0
  63. package/dist/component/sanitize.d.ts.map +1 -1
  64. package/dist/component/sanitize.js +58 -11
  65. package/dist/component/sanitize.js.map +1 -1
  66. package/dist/component/schema.d.ts +47 -5
  67. package/dist/component/schema.js +21 -1
  68. package/dist/component/schema.js.map +1 -1
  69. package/dist/component/sites.d.ts.map +1 -1
  70. package/dist/component/sites.js +6 -6
  71. package/dist/component/sites.js.map +1 -1
  72. package/dist/component/validators.d.ts +14 -9
  73. package/dist/component/validators.d.ts.map +1 -1
  74. package/dist/component/validators.js +5 -1
  75. package/dist/component/validators.js.map +1 -1
  76. package/dist/component/visitors.d.ts.map +1 -1
  77. package/dist/component/visitors.js.map +1 -1
  78. package/dist/component/vitals.d.ts +41 -0
  79. package/dist/component/vitals.d.ts.map +1 -0
  80. package/dist/component/vitals.js +115 -0
  81. package/dist/component/vitals.js.map +1 -0
  82. package/dist/react/index.d.ts.map +1 -1
  83. package/dist/react/index.js.map +1 -1
  84. package/dist/tracker/generated.d.ts +8 -4
  85. package/dist/tracker/generated.d.ts.map +1 -1
  86. package/dist/tracker/generated.js +8 -4
  87. package/dist/tracker/generated.js.map +1 -1
  88. package/dist/tracker/tracker.d.ts.map +1 -1
  89. package/dist/tracker/tracker.js +3 -1
  90. package/dist/tracker/tracker.js.map +1 -1
  91. package/dist/tracker/vitals.d.ts +10 -0
  92. package/dist/tracker/vitals.d.ts.map +1 -0
  93. package/dist/tracker/vitals.js +140 -0
  94. package/dist/tracker/vitals.js.map +1 -0
  95. package/dist/tracker.min.js +1 -1
  96. package/dist/vitals.min.js +1 -0
  97. package/docs/benchmarks/2026-08-20-realistic.md +71 -71
  98. package/docs/benchmarks/2026-08-21-formal-certification.md +50 -30
  99. package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
  100. package/docs/upgrading.md +46 -8
  101. package/llms.txt +5 -1
  102. package/package.json +17 -3
  103. package/scripts/benchmark-ingest.mjs +101 -48
  104. package/src/component/_generated/api.ts +2 -0
  105. package/src/component/_generated/component.ts +69 -4
  106. package/src/component/affiliates.ts +20 -5
  107. package/src/component/cardinality.ts +8 -7
  108. package/src/component/constants.ts +13 -1
  109. package/src/component/diagnostics.ts +3 -2
  110. package/src/component/eventStore.ts +53 -46
  111. package/src/component/funnels.ts +19 -16
  112. package/src/component/geo.ts +16 -14
  113. package/src/component/goals.ts +29 -21
  114. package/src/component/guards.ts +3 -1
  115. package/src/component/http.ts +93 -54
  116. package/src/component/ingest.ts +320 -127
  117. package/src/component/live.ts +5 -4
  118. package/src/component/reports.ts +563 -219
  119. package/src/component/retention.ts +221 -98
  120. package/src/component/sanitize.ts +99 -29
  121. package/src/component/schema.ts +31 -5
  122. package/src/component/sites.ts +22 -11
  123. package/src/component/validators.ts +16 -7
  124. package/src/component/visitors.ts +16 -5
  125. 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("--")) throw new Error(`unexpected argument: ${argument}`);
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)) throw new Error(`unknown option: --${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("--")) throw new Error(`missing value for --${rawName}`);
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(values.get("concurrency") ?? "100", "concurrency"),
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) throw new Error(`--${name} must be positive`);
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)) throw new Error(`--${name} must be an integer`);
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 = profile === "lean"
127
- ? `/benchmark/lean/${sequence % 4}`
128
- : REALISTIC_PATHS[sequence % REALISTIC_PATHS.length];
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 { ...base, type: "pageview", path: "/", affiliateSlug: "creator-club" };
163
+ return {
164
+ ...base,
165
+ type: "pageview",
166
+ path: "/",
167
+ affiliateSlug: "creator-club",
168
+ };
155
169
  }
156
170
  if (step === 1) {
157
- return { ...base, type: "pageview", path: "/pricing", affiliateSlug: "creator-club" };
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: 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,
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 { ...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
+ };
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[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
+ ];
204
232
  }
205
233
 
206
234
  function hasValidSessionTotals(value) {
207
- return value !== null &&
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 = options.profile === "lean"
253
- ? {
254
- id: `bench-${runId}-${requestIndex}`,
255
- visitorId: `visitor-${runId}-${requestIndex}`,
256
- sequence: 0,
257
- locationIndex: requestIndex % LOCATIONS.length,
258
- userAgentIndex: requestIndex % USER_AGENTS.length,
259
- }
260
- : 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];
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(`${JSON.stringify({
280
- profile: options.profile,
281
- batchSize: options.batchSize,
282
- requestBytes: Buffer.byteLength(dryRequest.body),
283
- heartbeats: dryRequest.heartbeats,
284
- location: requestContext(sessions[0]),
285
- }, 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
+ );
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("--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
+ );
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 ? new ConvexHttpClient(options.convexUrl) : null;
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(`generated request ${requestIndex} exceeds the 64 KiB HTTP limit`);
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 !== options.batchSize
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(`fetch:${error instanceof Error ? error.message : "unknown"}`);
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(0.001, (performance.now() - startedAt) / 1_000);
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
- `${accepted} accepted ${Math.round(accepted / elapsedSeconds)} events/s ` +
400
- `${failures} failures\n`,
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 ??= performance.now() >= deadline ? "duration" : "completed";
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 = options.profile === "lean"
414
- ? nextRequest
415
- : Math.min(nextRequest, options.sessionCount);
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: options.profile === "lean" ? "unique-per-request" : options.sessionCount,
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: durationCompleted && reconciliationPassed
517
- ? Math.round((storedEvents * 86_400_000) / elapsedMs)
518
- : null,
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))) fail("NOT_FOUND", "site not found");
26
- if (!Number.isSafeInteger(args.commissionBps) || args.commissionBps < 0 || args.commissionBps > 10_000) {
27
- fail("INVALID_ARGUMENT", "commissionBps must be an integer between 0 and 10000");
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("INVALID_ARGUMENT", "attributionWindowDays must be an integer between 1 and 365");
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 (args.affiliateId && existingBySlug && existingBySlug._id !== args.affiliateId) {
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 = count === 16
64
- ? 0.673
65
- : count === 32
66
- ? 0.697
67
- : count === 64
68
- ? 0.709
69
- : 0.7213 / (1 + 1.079 / count);
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) throw new Error("sessionId is not canonical");
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 {