@iann29/rastro 0.1.0-alpha.4 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +203 -56
  2. package/agent/integration.md +17 -15
  3. package/agent/manifest.json +6 -4
  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 +486 -31
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +209 -41
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +8 -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 +82 -8
  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 +6 -0
  24. package/dist/component/constants.d.ts.map +1 -1
  25. package/dist/component/constants.js +12 -0
  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 +3 -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 +71 -37
  46. package/dist/component/http.js.map +1 -1
  47. package/dist/component/identity.d.ts +13 -0
  48. package/dist/component/identity.d.ts.map +1 -0
  49. package/dist/component/identity.js +58 -0
  50. package/dist/component/identity.js.map +1 -0
  51. package/dist/component/ingest.d.ts +3 -1
  52. package/dist/component/ingest.d.ts.map +1 -1
  53. package/dist/component/ingest.js +168 -29
  54. package/dist/component/ingest.js.map +1 -1
  55. package/dist/component/live.d.ts.map +1 -1
  56. package/dist/component/live.js +1 -3
  57. package/dist/component/live.js.map +1 -1
  58. package/dist/component/reports.d.ts +70 -4
  59. package/dist/component/reports.d.ts.map +1 -1
  60. package/dist/component/reports.js +232 -59
  61. package/dist/component/reports.js.map +1 -1
  62. package/dist/component/retention.d.ts +4 -4
  63. package/dist/component/retention.d.ts.map +1 -1
  64. package/dist/component/retention.js +54 -22
  65. package/dist/component/retention.js.map +1 -1
  66. package/dist/component/sanitize.d.ts.map +1 -1
  67. package/dist/component/sanitize.js +42 -11
  68. package/dist/component/sanitize.js.map +1 -1
  69. package/dist/component/schema.d.ts +83 -10
  70. package/dist/component/schema.js +34 -1
  71. package/dist/component/schema.js.map +1 -1
  72. package/dist/component/sites.d.ts.map +1 -1
  73. package/dist/component/sites.js +6 -6
  74. package/dist/component/sites.js.map +1 -1
  75. package/dist/component/useragent.d.ts +9 -0
  76. package/dist/component/useragent.d.ts.map +1 -0
  77. package/dist/component/useragent.js +152 -0
  78. package/dist/component/useragent.js.map +1 -0
  79. package/dist/component/validators.d.ts +29 -18
  80. package/dist/component/validators.d.ts.map +1 -1
  81. package/dist/component/validators.js +8 -2
  82. package/dist/component/validators.js.map +1 -1
  83. package/dist/component/visitors.d.ts +19 -0
  84. package/dist/component/visitors.d.ts.map +1 -0
  85. package/dist/component/visitors.js +86 -0
  86. package/dist/component/visitors.js.map +1 -0
  87. package/dist/component/vitals.d.ts +41 -0
  88. package/dist/component/vitals.d.ts.map +1 -0
  89. package/dist/component/vitals.js +115 -0
  90. package/dist/component/vitals.js.map +1 -0
  91. package/dist/react/index.d.ts.map +1 -1
  92. package/dist/react/index.js.map +1 -1
  93. package/dist/tracker/generated.d.ts +8 -4
  94. package/dist/tracker/generated.d.ts.map +1 -1
  95. package/dist/tracker/generated.js +8 -4
  96. package/dist/tracker/generated.js.map +1 -1
  97. package/dist/tracker/tracker.d.ts.map +1 -1
  98. package/dist/tracker/tracker.js +23 -11
  99. package/dist/tracker/tracker.js.map +1 -1
  100. package/dist/tracker/vitals.d.ts +10 -0
  101. package/dist/tracker/vitals.d.ts.map +1 -0
  102. package/dist/tracker/vitals.js +137 -0
  103. package/dist/tracker/vitals.js.map +1 -0
  104. package/dist/tracker.min.js +1 -1
  105. package/dist/vitals.min.js +1 -0
  106. package/docs/benchmarks/2026-08-20-realistic.md +71 -71
  107. package/docs/benchmarks/2026-08-21-formal-certification.md +50 -30
  108. package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
  109. package/docs/federation.md +1 -1
  110. package/package.json +27 -9
  111. package/scripts/benchmark-ingest.mjs +101 -48
  112. package/src/component/_generated/api.ts +8 -0
  113. package/src/component/_generated/component.ts +87 -5
  114. package/src/component/affiliates.ts +20 -5
  115. package/src/component/cardinality.ts +8 -7
  116. package/src/component/constants.ts +12 -0
  117. package/src/component/diagnostics.ts +3 -2
  118. package/src/component/eventStore.ts +53 -46
  119. package/src/component/funnels.ts +19 -16
  120. package/src/component/geo.ts +16 -14
  121. package/src/component/goals.ts +29 -21
  122. package/src/component/guards.ts +3 -1
  123. package/src/component/http.ts +126 -78
  124. package/src/component/identity.ts +74 -0
  125. package/src/component/ingest.ts +349 -130
  126. package/src/component/live.ts +5 -4
  127. package/src/component/reports.ts +572 -221
  128. package/src/component/retention.ts +221 -98
  129. package/src/component/sanitize.ts +77 -27
  130. package/src/component/schema.ts +46 -5
  131. package/src/component/sites.ts +22 -11
  132. package/src/component/useragent.ts +171 -0
  133. package/src/component/validators.ts +19 -7
  134. package/src/component/visitors.ts +116 -0
  135. package/src/component/vitals.ts +146 -0
@@ -1,8 +1,9 @@
1
1
  import { v } from "convex/values";
2
2
  import { mutation } from "./_generated/server.js";
3
3
  import { internal } from "./_generated/api.js";
4
- import { AGGREGATE_SHARDS, MAX_BATCH_EVENT_GROUPS, DAY_MS, HOURLY_AGGREGATE_SHARDS, DIMENSION_SLOTS, HOUR_MS, LIVE_SESSION_TTL_MS, LIVE_SWEEP_INTERVAL_MS, MAX_BATCH_EVENTS, MAX_SITE_BYTES_PER_SHARD_WINDOW, MAX_SITE_EVENTS_PER_SHARD_WINDOW, MAX_BYTES_PER_SESSION_WINDOW, MAX_EVENTS_PER_SESSION_WINDOW, MAX_FUNNELS_PER_SITE, MAX_FUNNEL_STEPS, MAX_GOALS_PER_SITE, RATE_LIMIT_WINDOW_MS, REPORT_ROLLUP_SHARDS, SECONDARY_AGGREGATE_SHARDS, SITE_INGEST_SHARDS, } from "./constants.js";
4
+ import { AGGREGATE_SHARDS, MAX_BATCH_EVENT_GROUPS, DAY_MS, HOURLY_AGGREGATE_SHARDS, DIMENSION_SLOTS, HOUR_MS, LIVE_SESSION_TTL_MS, LIVE_SWEEP_INTERVAL_MS, MAX_BATCH_EVENTS, MAX_SITE_BYTES_PER_SHARD_WINDOW, MAX_SITE_EVENTS_PER_SHARD_WINDOW, MAX_BYTES_PER_SESSION_WINDOW, MAX_EVENTS_PER_SESSION_WINDOW, MAX_FUNNELS_PER_SITE, MAX_FUNNEL_STEPS, MAX_GOALS_PER_SITE, RATE_LIMIT_WINDOW_MS, REPORT_ROLLUP_SHARDS, SECONDARY_AGGREGATE_SHARDS, SITE_INGEST_SHARDS, VITAL_PAGE_SLOTS, } from "./constants.js";
5
5
  import { addVisitor, createVisitorSketch, unionVisitorSketches, } from "./cardinality.js";
6
+ import { addToVitalHistogram, emptyVitalHistogram, mergeVitalHistograms, vitalDevice, VITAL_ALL, VITAL_OTHER_PAGES, } from "./vitals.js";
6
7
  import { fail } from "./errors.js";
7
8
  import { ensureAnalyticsControl } from "./coverage.js";
8
9
  import { bufferEventBatch, eventBatchKey, eventBucketStart, findTelemetry, flushEventBatches, insertEventBatch, inspectTelemetryMinute, } from "./eventStore.js";
@@ -76,7 +77,7 @@ export const ingestBatch = mutation({
76
77
  const bucketStart = eventBucketStart(event.timestamp);
77
78
  const groupKey = eventBatchKey(event.sessionId, bucketStart);
78
79
  requestGroups.add(groupKey);
79
- if (event.type === "heartbeat")
80
+ if (event.type === "heartbeat" || event.type === "vital")
80
81
  continue;
81
82
  const eventIds = eventIdsByGroup.get(groupKey) ?? new Set();
82
83
  if (eventIds.has(event.eventId)) {
@@ -144,6 +145,21 @@ export const ingestBatch = mutation({
144
145
  accepted += 1;
145
146
  continue;
146
147
  }
148
+ if (event.type === "vital") {
149
+ // A vital annotates telemetry its session already produced; it never
150
+ // creates sessions, live presence, event batches, or aggregate counts.
151
+ if (!existingSession) {
152
+ rejected += 1;
153
+ continue;
154
+ }
155
+ if (!(await consumeRateLimit(ctx, args.siteId, event.sessionId, eventByteCounts[eventIndex], now, sessionRateWindows))) {
156
+ rejected += 1;
157
+ continue;
158
+ }
159
+ foldVitalRollups(reportRollupDeltas, args.siteId, event, vitalDevice(context.device));
160
+ accepted += 1;
161
+ continue;
162
+ }
147
163
  if (duplicateInBatch.has(eventIndex) ||
148
164
  persistedEventIds
149
165
  .get(eventBatchKey(event.sessionId, eventBucketStart(event.timestamp)))
@@ -173,8 +189,8 @@ export const ingestBatch = mutation({
173
189
  event,
174
190
  context,
175
191
  existingSession,
176
- goals: canMatchDefinition ? goals ?? [] : [],
177
- funnels: canMatchDefinition ? funnels ?? [] : [],
192
+ goals: canMatchDefinition ? (goals ?? []) : [],
193
+ funnels: canMatchDefinition ? (funnels ?? []) : [],
178
194
  aggregateDeltas,
179
195
  reportRollupDeltas,
180
196
  recordTelemetry: true,
@@ -376,8 +392,7 @@ async function accountEvent(ctx, input) {
376
392
  .eq("siteId", input.siteId)
377
393
  .eq("goalId", goal._id)
378
394
  .eq("eventId", completionEventId))
379
- .take(2))
380
- .find((completion) => completion.idempotencyVersion === 2);
395
+ .take(2)).find((completion) => completion.idempotencyVersion === 2);
381
396
  const legacyPrior = scopedPrior
382
397
  ? undefined
383
398
  : (await ctx.db
@@ -386,8 +401,7 @@ async function accountEvent(ctx, input) {
386
401
  .eq("siteId", input.siteId)
387
402
  .eq("goalId", goal._id)
388
403
  .eq("eventId", event.eventId))
389
- .take(2))
390
- .find((completion) => completion.idempotencyVersion === undefined &&
404
+ .take(2)).find((completion) => completion.idempotencyVersion === undefined &&
391
405
  completion.sessionId === event.sessionId &&
392
406
  eventBucketStart(completion.timestamp) ===
393
407
  eventBucketStart(event.timestamp));
@@ -415,15 +429,19 @@ async function accountEvent(ctx, input) {
415
429
  if (input.financialConversion) {
416
430
  await accountConversion(ctx, input.siteId, session, event, input.trustedGoalKey, input.reportRollupDeltas);
417
431
  }
432
+ const campaignDimensions = session.utmCampaign
433
+ ? [{ type: "campaign", value: session.utmCampaign }]
434
+ : [];
418
435
  const dimensions = [
419
436
  { type: "source", value: session.source },
437
+ ...campaignDimensions,
420
438
  { type: "page", value: event.path },
421
439
  { type: "country", value: session.country ?? "unknown" },
422
440
  { type: "device", value: input.context.device ?? "unknown" },
423
441
  { type: "browser", value: input.context.browser ?? "Other" },
424
442
  {
425
443
  type: "event",
426
- value: event.type === "custom" ? event.name ?? "custom" : event.type,
444
+ value: event.type === "custom" ? (event.name ?? "custom") : event.type,
427
445
  },
428
446
  ...matchedGoalKeys.map((value) => ({ type: "goal", value })),
429
447
  ];
@@ -450,6 +468,8 @@ async function accountTrustedSession(ctx, existing, event) {
450
468
  isNewVisitorDay: false,
451
469
  startedAt: existing.startedAt,
452
470
  source: existing.source,
471
+ utmCampaign: existing.utmCampaign,
472
+ visitorKey: existing.visitorKey ?? existing.visitorId,
453
473
  country: existing.country,
454
474
  city: existing.city,
455
475
  latitude: existing.latitude,
@@ -463,14 +483,21 @@ async function accountTrustedSession(ctx, existing, event) {
463
483
  };
464
484
  }
465
485
  async function upsertSession(ctx, siteId, event, context, existing, financialConversion, reportRollupDeltas, batchState) {
466
- const source = sourceFromReferrer(event.referrer);
486
+ // A host-supplied id is the identity; anonymous sessions count through the
487
+ // daily key derived by the HTTP action, or once per session without one.
488
+ const visitorKey = existing?.visitorKey ??
489
+ (event.visitorId !== event.sessionId
490
+ ? event.visitorId
491
+ : (context.visitorKey ?? event.sessionId));
467
492
  const utmSource = stringProperty(event.properties, "utm_source");
493
+ // Campaign traffic names its own source; the referrer host is the fallback.
494
+ const source = utmSource?.toLowerCase() ?? sourceFromReferrer(event.referrer);
468
495
  const utmMedium = stringProperty(event.properties, "utm_medium");
469
496
  const utmCampaign = stringProperty(event.properties, "utm_campaign");
470
497
  const attribution = await attributionForEvent(ctx, siteId, event, existing, batchState);
471
498
  if (existing) {
472
499
  if (attribution && existing.affiliateId !== attribution.affiliateId) {
473
- foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution);
500
+ foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution, visitorKey);
474
501
  }
475
502
  const startedAt = Math.min(existing.startedAt, event.timestamp);
476
503
  const lastSeenAt = Math.max(existing.lastSeenAt, event.timestamp);
@@ -484,7 +511,7 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
484
511
  durationMs: Math.max(0, lastSeenAt - startedAt),
485
512
  conversionCount: existing.conversionCount + Number(financialConversion),
486
513
  revenueCents: existing.revenueCents +
487
- (financialConversion ? event.revenueCents ?? 0 : 0),
514
+ (financialConversion ? (event.revenueCents ?? 0) : 0),
488
515
  ...(financialConversion && event.currency
489
516
  ? { currency: event.currency }
490
517
  : {}),
@@ -495,7 +522,9 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
495
522
  ...(context.country ? { country: context.country } : {}),
496
523
  ...(context.city ? { city: context.city } : {}),
497
524
  ...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
498
- ...(context.longitude !== undefined ? { longitude: context.longitude } : {}),
525
+ ...(context.longitude !== undefined
526
+ ? { longitude: context.longitude }
527
+ : {}),
499
528
  browser: context.browser ?? existing.browser,
500
529
  os: context.os ?? existing.os,
501
530
  device: context.device ?? existing.device,
@@ -518,6 +547,8 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
518
547
  Math.floor(event.timestamp / DAY_MS),
519
548
  startedAt,
520
549
  source: existing.source,
550
+ utmCampaign: existing.utmCampaign,
551
+ visitorKey,
521
552
  country: context.country ?? existing.country,
522
553
  city: context.city ?? existing.city,
523
554
  latitude: context.latitude ?? existing.latitude,
@@ -547,6 +578,7 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
547
578
  siteId,
548
579
  sessionId: event.sessionId,
549
580
  visitorId: event.visitorId,
581
+ visitorKey,
550
582
  startedAt: event.timestamp,
551
583
  lastSeenAt: event.timestamp,
552
584
  entryPath: event.path,
@@ -559,7 +591,9 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
559
591
  ...(context.country ? { country: context.country } : {}),
560
592
  ...(context.city ? { city: context.city } : {}),
561
593
  ...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
562
- ...(context.longitude !== undefined ? { longitude: context.longitude } : {}),
594
+ ...(context.longitude !== undefined
595
+ ? { longitude: context.longitude }
596
+ : {}),
563
597
  browser: context.browser ?? "Other",
564
598
  os: context.os ?? "Other",
565
599
  device: context.device ?? "unknown",
@@ -567,7 +601,7 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
567
601
  pageviewCount: event.type === "pageview" ? 1 : 0,
568
602
  durationMs: 0,
569
603
  conversionCount: Number(financialConversion),
570
- revenueCents: financialConversion ? event.revenueCents ?? 0 : 0,
604
+ revenueCents: financialConversion ? (event.revenueCents ?? 0) : 0,
571
605
  ...(financialConversion && event.currency
572
606
  ? { currency: event.currency }
573
607
  : {}),
@@ -575,7 +609,7 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
575
609
  };
576
610
  const sessionId = await ctx.db.insert("sessions", fields);
577
611
  if (attribution) {
578
- foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution);
612
+ foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution, visitorKey);
579
613
  }
580
614
  if (batchState) {
581
615
  const state = batchState.sessions.get(event.sessionId);
@@ -596,6 +630,8 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
596
630
  Math.floor(event.timestamp / DAY_MS),
597
631
  startedAt: event.timestamp,
598
632
  source,
633
+ utmCampaign,
634
+ visitorKey,
599
635
  country: context.country,
600
636
  city: context.city,
601
637
  latitude: context.latitude,
@@ -719,14 +755,14 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
719
755
  const updates = {
720
756
  startedAt,
721
757
  lastSeenAt,
722
- exitPath: event.timestamp >= session.lastSeenAt
723
- ? event.path
724
- : session.exitPath,
758
+ exitPath: event.timestamp >= session.lastSeenAt ? event.path : session.exitPath,
725
759
  durationMs: Math.max(0, lastSeenAt - startedAt),
726
760
  ...(context.country ? { country: context.country } : {}),
727
761
  ...(context.city ? { city: context.city } : {}),
728
762
  ...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
729
- ...(context.longitude !== undefined ? { longitude: context.longitude } : {}),
763
+ ...(context.longitude !== undefined
764
+ ? { longitude: context.longitude }
765
+ : {}),
730
766
  browser: context.browser ?? session.browser,
731
767
  os: context.os ?? session.os,
732
768
  device: context.device ?? session.device,
@@ -747,6 +783,7 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
747
783
  isNewVisitorDay: false,
748
784
  startedAt,
749
785
  source: session.source,
786
+ visitorKey: session.visitorKey ?? session.visitorId,
750
787
  country: context.country ?? session.country,
751
788
  city: context.city ?? session.city,
752
789
  latitude: context.latitude ?? session.latitude,
@@ -776,9 +813,7 @@ async function consumeSiteRateLimit(ctx, siteId, events, eventByteCounts, now) {
776
813
  .order("desc")
777
814
  .take(1);
778
815
  const storedWindow = rows[0] ?? null;
779
- const window = storedWindow?.windowStart === windowStart
780
- ? storedWindow
781
- : null;
816
+ const window = storedWindow?.windowStart === windowStart ? storedWindow : null;
782
817
  if ((window?.eventCount ?? 0) + increment.eventCount >
783
818
  MAX_SITE_EVENTS_PER_SHARD_WINDOW ||
784
819
  (window?.byteCount ?? 0) + increment.byteCount >
@@ -1039,7 +1074,8 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
1039
1074
  foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
1040
1075
  return;
1041
1076
  }
1042
- if (progress.completedAt !== undefined || event.timestamp < progress.lastStepAt)
1077
+ if (progress.completedAt !== undefined ||
1078
+ event.timestamp < progress.lastStepAt)
1043
1079
  return;
1044
1080
  if (event.timestamp - progress.startedAt > funnel.conversionWindowMs) {
1045
1081
  if (!matchesFirst)
@@ -1191,6 +1227,7 @@ function createReportRollupDeltas() {
1191
1227
  goals: new Map(),
1192
1228
  funnels: new Map(),
1193
1229
  affiliates: new Map(),
1230
+ vitals: new Map(),
1194
1231
  };
1195
1232
  }
1196
1233
  function reportRollupCoordinates(siteId, definitionId, timestamp, visitorId) {
@@ -1236,7 +1273,7 @@ function foldFunnelRollup(deltas, siteId, funnelId, event, step, completed) {
1236
1273
  rollup.steps.set(step, current);
1237
1274
  deltas.funnels.set(coordinates.key, rollup);
1238
1275
  }
1239
- function foldAffiliateVisitRollup(deltas, siteId, event, attribution) {
1276
+ function foldAffiliateVisitRollup(deltas, siteId, event, attribution, visitorKey) {
1240
1277
  const coordinates = reportRollupCoordinates(siteId, String(attribution.affiliateId), event.timestamp, event.visitorId);
1241
1278
  const current = deltas.affiliates.get(coordinates.key) ?? {
1242
1279
  siteId,
@@ -1251,7 +1288,7 @@ function foldAffiliateVisitRollup(deltas, siteId, event, attribution) {
1251
1288
  commissionCents: 0,
1252
1289
  };
1253
1290
  current.visits += 1;
1254
- current.visitorSketch = addVisitor(current.visitorSketch, event.visitorId);
1291
+ current.visitorSketch = addVisitor(current.visitorSketch, visitorKey);
1255
1292
  deltas.affiliates.set(coordinates.key, current);
1256
1293
  }
1257
1294
  function foldAffiliateConversionRollup(deltas, siteId, event, affiliate, affiliateSlug, revenueCents, commissionCents) {
@@ -1273,6 +1310,103 @@ function foldAffiliateConversionRollup(deltas, siteId, event, affiliate, affilia
1273
1310
  current.commissionCents += commissionCents;
1274
1311
  deltas.affiliates.set(coordinates.key, current);
1275
1312
  }
1313
+ function foldVitalRollups(deltas, siteId, event, device) {
1314
+ const metric = event.name;
1315
+ const value = event.value ?? 0;
1316
+ const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
1317
+ const shard = stableHash(event.sessionId) % REPORT_ROLLUP_SHARDS;
1318
+ // Each measurement lands twice: on its page (across devices) and on its
1319
+ // device class (across pages), so both breakdowns stay exact and bounded.
1320
+ for (const target of [
1321
+ { device: VITAL_ALL, page: event.path },
1322
+ { device, page: VITAL_ALL },
1323
+ ]) {
1324
+ const key = JSON.stringify([
1325
+ siteId,
1326
+ bucketStart,
1327
+ target.device,
1328
+ target.page,
1329
+ shard,
1330
+ ]);
1331
+ const row = deltas.vitals.get(key) ?? {
1332
+ siteId,
1333
+ bucketStart,
1334
+ device: target.device,
1335
+ page: target.page,
1336
+ shard,
1337
+ metrics: new Map(),
1338
+ };
1339
+ const entry = row.metrics.get(metric) ?? {
1340
+ count: 0,
1341
+ sum: 0,
1342
+ histogram: emptyVitalHistogram(),
1343
+ };
1344
+ entry.count += 1;
1345
+ entry.sum += value;
1346
+ entry.histogram = addToVitalHistogram(entry.histogram, value);
1347
+ row.metrics.set(metric, entry);
1348
+ deltas.vitals.set(key, row);
1349
+ }
1350
+ }
1351
+ async function upsertVitalRollup(ctx, delta) {
1352
+ const existing = await ctx.db
1353
+ .query("vitalRollups")
1354
+ .withIndex("by_key", (range) => range
1355
+ .eq("siteId", delta.siteId)
1356
+ .eq("bucketStart", delta.bucketStart)
1357
+ .eq("device", delta.device)
1358
+ .eq("page", delta.page)
1359
+ .eq("shard", delta.shard))
1360
+ .unique();
1361
+ if (!existing &&
1362
+ delta.device === VITAL_ALL &&
1363
+ delta.page !== VITAL_OTHER_PAGES) {
1364
+ // A new page row spends one bounded read to respect the daily page slots;
1365
+ // past the cap, the day's remaining pages fold into "(other)".
1366
+ const pageRows = await ctx.db
1367
+ .query("vitalRollups")
1368
+ .withIndex("by_key", (range) => range
1369
+ .eq("siteId", delta.siteId)
1370
+ .eq("bucketStart", delta.bucketStart)
1371
+ .eq("device", VITAL_ALL))
1372
+ .take(VITAL_PAGE_SLOTS * REPORT_ROLLUP_SHARDS + 1);
1373
+ if (pageRows.length > VITAL_PAGE_SLOTS * REPORT_ROLLUP_SHARDS) {
1374
+ await upsertVitalRollup(ctx, { ...delta, page: VITAL_OTHER_PAGES });
1375
+ return;
1376
+ }
1377
+ }
1378
+ const metrics = existing?.metrics.map((entry) => ({ ...entry })) ?? [];
1379
+ for (const [metric, addition] of delta.metrics) {
1380
+ const entry = metrics.find((candidate) => candidate.metric === metric);
1381
+ if (entry) {
1382
+ entry.count += addition.count;
1383
+ entry.sum += addition.sum;
1384
+ entry.histogram = mergeVitalHistograms(entry.histogram, addition.histogram);
1385
+ }
1386
+ else {
1387
+ metrics.push({
1388
+ metric,
1389
+ count: addition.count,
1390
+ sum: addition.sum,
1391
+ histogram: [...addition.histogram],
1392
+ });
1393
+ }
1394
+ }
1395
+ const fields = {
1396
+ siteId: delta.siteId,
1397
+ bucketStart: delta.bucketStart,
1398
+ device: delta.device,
1399
+ page: delta.page,
1400
+ shard: delta.shard,
1401
+ metrics,
1402
+ };
1403
+ if (existing) {
1404
+ await ctx.db.replace("vitalRollups", existing._id, fields);
1405
+ }
1406
+ else {
1407
+ await ctx.db.insert("vitalRollups", fields);
1408
+ }
1409
+ }
1276
1410
  async function flushReportRollups(ctx, deltas) {
1277
1411
  for (const delta of deltas.goals.values()) {
1278
1412
  const existing = await ctx.db
@@ -1367,9 +1501,12 @@ async function flushReportRollups(ctx, deltas) {
1367
1501
  await ctx.db.insert("affiliateDailyRollups", fields);
1368
1502
  }
1369
1503
  }
1504
+ for (const delta of deltas.vitals.values()) {
1505
+ await upsertVitalRollup(ctx, delta);
1506
+ }
1370
1507
  }
1371
1508
  function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, recordTelemetry, financialConversion) {
1372
- const revenueCents = financialConversion ? event.revenueCents ?? 0 : 0;
1509
+ const revenueCents = financialConversion ? (event.revenueCents ?? 0) : 0;
1373
1510
  const dimensionDeltas = dimensions.map((dimension) => ({
1374
1511
  ...dimension,
1375
1512
  count: Number(recordTelemetry),
@@ -1415,7 +1552,7 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
1415
1552
  : Number(session.isNewVisitorDay)
1416
1553
  : 0;
1417
1554
  if (recordTelemetry) {
1418
- delta.visitorSketch = addVisitor(delta.visitorSketch, event.visitorId);
1555
+ delta.visitorSketch = addVisitor(delta.visitorSketch, session.visitorKey);
1419
1556
  }
1420
1557
  delta.conversions += Number(financialConversion);
1421
1558
  delta.revenueCents += revenueCents;
@@ -1539,6 +1676,8 @@ function mergeDimensions(current, additions) {
1539
1676
  }
1540
1677
  function stringProperty(properties, key) {
1541
1678
  const value = properties?.[key];
1542
- return typeof value === "string" ? cleanString(value, 120) || undefined : undefined;
1679
+ return typeof value === "string"
1680
+ ? cleanString(value, 120) || undefined
1681
+ : undefined;
1543
1682
  }
1544
1683
  //# sourceMappingURL=ingest.js.map