@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,12 +1,14 @@
1
1
  import { paginationOptsValidator, paginationResultValidator, } from "convex/server";
2
2
  import { v } from "convex/values";
3
3
  import { query } from "./_generated/server.js";
4
- import { AGGREGATE_SHARDS, DAY_MS, FEATURE_REPORT_BYTE_RESERVE, FEATURE_REPORT_DOCUMENT_RESERVE, HOUR_MS, HOURLY_AGGREGATE_SHARDS, MAX_AFFILIATES_PER_SITE, MAX_FUNNELS_PER_SITE, MAX_FEATURE_ROLLUP_DAYS, MAX_FEATURE_ROLLUP_ROWS, MAX_GOALS_PER_SITE, MAX_JOURNEY_EVENTS, MAX_LIVE_VISITORS, MAX_OVERVIEW_AGGREGATE_ROWS, MAX_REPORT_BUCKETS, MAX_REPORT_RANGE_DAYS, MAX_REPORT_SITES, REPORT_ROLLUP_SHARDS, } from "./constants.js";
4
+ import { AGGREGATE_SHARDS, DAY_MS, FEATURE_REPORT_BYTE_RESERVE, FEATURE_REPORT_DOCUMENT_RESERVE, HOUR_MS, HOURLY_AGGREGATE_SHARDS, MAX_AFFILIATES_PER_SITE, MAX_FUNNELS_PER_SITE, MAX_FEATURE_ROLLUP_DAYS, MAX_FEATURE_ROLLUP_ROWS, MAX_GOALS_PER_SITE, MAX_JOURNEY_EVENTS, MAX_LIVE_VISITORS, MAX_OVERVIEW_AGGREGATE_ROWS, MAX_REPORT_BUCKETS, MAX_REPORT_RANGE_DAYS, MAX_REPORT_SITES, MAX_VITALS_REPORT_ROWS, REPORT_ROLLUP_SHARDS, VITALS_REPORT_PAGE_LIMIT, } from "./constants.js";
5
5
  import { createVisitorSketch, estimateVisitors, unionVisitorSketches, } from "./cardinality.js";
6
6
  import { fail } from "./errors.js";
7
7
  import { compareJourneyEvents, hydrateEventContexts, loadSessionJourneyEvents, loadVisitorJourneyEvents, } from "./eventStore.js";
8
8
  import { isPlainRecord } from "./guards.js";
9
- import { affiliateFieldsValidator, eventFieldsValidator, funnelFieldsValidator, goalFieldsValidator, sessionFieldsValidator, } from "./validators.js";
9
+ import { affiliateFieldsValidator, eventFieldsValidator, funnelFieldsValidator, goalFieldsValidator, sessionFieldsValidator, vitalMetricValidator, } from "./validators.js";
10
+ import { resolveVisitorIdentities } from "./visitors.js";
11
+ import { mergeVitalHistograms, VITAL_ALL, VITAL_METRICS, vitalHistogramPercentile, vitalRatingCounts, } from "./vitals.js";
10
12
  const sessionDocumentValidator = sessionFieldsValidator.extend({
11
13
  _id: v.id("sessions"),
12
14
  _creationTime: v.number(),
@@ -66,7 +68,9 @@ const metricTotalsValidator = v.object({
66
68
  conversions: v.number(),
67
69
  revenueCents: v.number(),
68
70
  });
69
- const timePointValidator = metricTotalsValidator.extend({ timestamp: v.number() });
71
+ const timePointValidator = metricTotalsValidator.extend({
72
+ timestamp: v.number(),
73
+ });
70
74
  const topItemValidator = v.object({
71
75
  value: v.string(),
72
76
  count: v.number(),
@@ -106,6 +110,7 @@ export const overview = query({
106
110
  totals: metricTotalsValidator,
107
111
  timeSeries: v.array(timePointValidator),
108
112
  topSources: v.array(topItemValidator),
113
+ topCampaigns: v.array(topItemValidator),
109
114
  topPages: v.array(topItemValidator),
110
115
  topCountries: v.array(topItemValidator),
111
116
  topDevices: v.array(topItemValidator),
@@ -122,6 +127,7 @@ export const overview = query({
122
127
  }),
123
128
  breakdowns: v.object({
124
129
  topSources: v.literal("eventVolume"),
130
+ topCampaigns: v.literal("eventVolume"),
125
131
  topPages: v.literal("eventVolume"),
126
132
  topCountries: v.literal("eventVolume"),
127
133
  topDevices: v.literal("eventVolume"),
@@ -135,7 +141,7 @@ export const overview = query({
135
141
  sessionReplay: v.literal("unsupported"),
136
142
  errorInsights: v.literal("unsupported"),
137
143
  outboundLinks: v.literal("eventVolume"),
138
- botDetection: v.literal("unsupported"),
144
+ botDetection: v.literal("userAgent"),
139
145
  }),
140
146
  }),
141
147
  }),
@@ -242,6 +248,7 @@ export const overview = query({
242
248
  .sort(([left], [right]) => left - right)
243
249
  .map(([timestamp, metrics]) => ({ timestamp, ...metrics })),
244
250
  topSources: top("source"),
251
+ topCampaigns: top("campaign"),
245
252
  topPages: top("page"),
246
253
  topCountries: top("country"),
247
254
  topDevices: top("device"),
@@ -258,6 +265,7 @@ export const overview = query({
258
265
  },
259
266
  breakdowns: {
260
267
  topSources: "eventVolume",
268
+ topCampaigns: "eventVolume",
261
269
  topPages: "eventVolume",
262
270
  topCountries: "eventVolume",
263
271
  topDevices: "eventVolume",
@@ -271,7 +279,7 @@ export const overview = query({
271
279
  sessionReplay: "unsupported",
272
280
  errorInsights: "unsupported",
273
281
  outboundLinks: "eventVolume",
274
- botDetection: "unsupported",
282
+ botDetection: "userAgent",
275
283
  },
276
284
  },
277
285
  };
@@ -287,7 +295,9 @@ export const liveVisitors = query({
287
295
  handler: async (ctx, args) => {
288
296
  const siteIds = validateSiteIds(args.siteIds);
289
297
  const limit = args.limit ?? 100;
290
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIVE_VISITORS) {
298
+ if (!Number.isSafeInteger(limit) ||
299
+ limit < 1 ||
300
+ limit > MAX_LIVE_VISITORS) {
291
301
  fail("INVALID_ARGUMENT", `limit must be an integer between 1 and ${MAX_LIVE_VISITORS}`);
292
302
  }
293
303
  if (!Number.isFinite(args.now) || args.now < 0) {
@@ -483,24 +493,26 @@ export const visitorJourney = query({
483
493
  validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
484
494
  await assertSourceAvailable(ctx, siteIds, ["events"], args.from);
485
495
  const limit = args.limit ?? 200;
486
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_JOURNEY_EVENTS) {
496
+ if (!Number.isSafeInteger(limit) ||
497
+ limit < 1 ||
498
+ limit > MAX_JOURNEY_EVENTS) {
487
499
  fail("INVALID_ARGUMENT", `limit must be an integer between 1 and ${MAX_JOURNEY_EVENTS}`);
488
500
  }
489
501
  const rows = [];
490
502
  for (const siteId of siteIds) {
491
- const cutoff = rows.length >= limit
492
- ? rows[limit - 1].timestamp
493
- : args.to;
494
- rows.push(...await loadVisitorJourneyEvents(ctx, {
495
- siteId,
496
- visitorId: args.visitorId,
497
- from: args.from,
498
- to: cutoff,
499
- resultLimit: limit,
500
- }));
501
- rows.sort(compareJourneyEvents);
502
- if (rows.length > limit)
503
- rows.length = limit;
503
+ for (const visitorId of await resolveVisitorIdentities(ctx, siteId, args.visitorId)) {
504
+ const cutoff = rows.length >= limit ? rows[limit - 1].timestamp : args.to;
505
+ rows.push(...(await loadVisitorJourneyEvents(ctx, {
506
+ siteId,
507
+ visitorId,
508
+ from: args.from,
509
+ to: cutoff,
510
+ resultLimit: limit,
511
+ })));
512
+ rows.sort(compareJourneyEvents);
513
+ if (rows.length > limit)
514
+ rows.length = limit;
515
+ }
504
516
  }
505
517
  return hydrateEventContexts(ctx, rows);
506
518
  },
@@ -579,7 +591,12 @@ export const goalsReport = query({
579
591
  const funnelReportItemValidator = funnelFieldsValidator.extend({
580
592
  _id: v.id("funnels"),
581
593
  _creationTime: v.number(),
582
- stepsReport: v.array(v.object({ step: v.number(), name: v.string(), entrants: v.number(), completions: v.number() })),
594
+ stepsReport: v.array(v.object({
595
+ step: v.number(),
596
+ name: v.string(),
597
+ entrants: v.number(),
598
+ completions: v.number(),
599
+ })),
583
600
  truncated: v.boolean(),
584
601
  metadata: reportMetadataValidator,
585
602
  });
@@ -642,7 +659,9 @@ export const funnelsReport = query({
642
659
  stepsReport,
643
660
  truncated: false,
644
661
  metadata: {
645
- basis: rollups ? "dailyRollup" : "boundedLegacy",
662
+ basis: rollups
663
+ ? "dailyRollup"
664
+ : "boundedLegacy",
646
665
  coverage: reportCoverage,
647
666
  range: rangeSemantics(args.from, args.to),
648
667
  },
@@ -768,7 +787,7 @@ export const affiliatesReport = query({
768
787
  visits: traffic?.visits ?? 0,
769
788
  uniqueVisitors: rollups
770
789
  ? estimateVisitors(traffic?.sketch ?? createVisitorSketch())
771
- : traffic?.visitors.size ?? 0,
790
+ : (traffic?.visitors.size ?? 0),
772
791
  };
773
792
  }),
774
793
  truncated: false,
@@ -783,14 +802,128 @@ export const affiliatesReport = query({
783
802
  }),
784
803
  range: rangeSemantics(args.from, args.to),
785
804
  uniqueVisitors: {
786
- basis: rollups ? "estimatedUnique" : "exactLegacy",
805
+ basis: rollups
806
+ ? "estimatedUnique"
807
+ : "exactLegacy",
787
808
  exact: !rollups,
788
809
  },
789
810
  },
790
811
  };
791
812
  },
792
813
  });
793
- const coverageDatasetValidator = v.union(v.literal("overviewHour"), v.literal("overviewDay"), v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goals"), v.literal("funnels"), v.literal("affiliates"));
814
+ const vitalStatsValidator = v.object({
815
+ count: v.number(),
816
+ p75: v.number(),
817
+ average: v.number(),
818
+ good: v.number(),
819
+ needsImprovement: v.number(),
820
+ poor: v.number(),
821
+ });
822
+ export const vitalsReport = query({
823
+ args: { siteId: v.id("sites"), from: v.number(), to: v.number() },
824
+ returns: v.object({
825
+ metrics: v.array(vitalStatsValidator.extend({
826
+ metric: vitalMetricValidator,
827
+ devices: v.array(vitalStatsValidator.extend({ device: v.string() })),
828
+ pages: v.array(vitalStatsValidator.extend({ page: v.string() })),
829
+ timeSeries: v.array(v.object({
830
+ timestamp: v.number(),
831
+ count: v.number(),
832
+ p75: v.number(),
833
+ })),
834
+ })),
835
+ truncated: v.boolean(),
836
+ metadata: reportMetadataValidator,
837
+ }),
838
+ handler: async (ctx, args) => {
839
+ validateRange(args.from, args.to, MAX_FEATURE_ROLLUP_DAYS);
840
+ requireAlignedRange(args.from, args.to, DAY_MS, "vitals report");
841
+ await assertSourceAvailable(ctx, [args.siteId], ["vitalRollups"], args.from);
842
+ const rows = await ctx.db
843
+ .query("vitalRollups")
844
+ .withIndex("by_siteId_and_bucketStart", (range) => range
845
+ .eq("siteId", args.siteId)
846
+ .gte("bucketStart", args.from)
847
+ .lte("bucketStart", args.to))
848
+ .take(MAX_VITALS_REPORT_ROWS + 1);
849
+ failIfIncomplete(rows, MAX_VITALS_REPORT_ROWS, "vital rollups");
850
+ const fold = (groups, metric, key, entry) => {
851
+ const group = groups.get(metric) ?? new Map();
852
+ const current = group.get(key) ?? { count: 0, sum: 0, histogram: [] };
853
+ current.count += entry.count;
854
+ current.sum += entry.sum;
855
+ current.histogram = mergeVitalHistograms(current.histogram, entry.histogram);
856
+ group.set(key, current);
857
+ groups.set(metric, group);
858
+ };
859
+ const byDevice = new Map();
860
+ const byPage = new Map();
861
+ const byDay = new Map();
862
+ for (const row of rows) {
863
+ for (const entry of row.metrics) {
864
+ if (row.page === VITAL_ALL) {
865
+ // A device row carries the complete signal once, so overall totals
866
+ // and the time series derive from this family alone.
867
+ fold(byDevice, entry.metric, row.device, entry);
868
+ fold(byDay, entry.metric, row.bucketStart, entry);
869
+ }
870
+ else {
871
+ fold(byPage, entry.metric, row.page, entry);
872
+ }
873
+ }
874
+ }
875
+ const stats = (metric, accumulated) => ({
876
+ count: accumulated.count,
877
+ p75: vitalHistogramPercentile(accumulated.histogram, 0.75),
878
+ average: accumulated.count > 0
879
+ ? Math.round(accumulated.sum / accumulated.count)
880
+ : 0,
881
+ ...vitalRatingCounts(accumulated.histogram, metric),
882
+ });
883
+ const metrics = VITAL_METRICS.map((metric) => {
884
+ const overall = { count: 0, sum: 0, histogram: [] };
885
+ for (const accumulated of byDevice.get(metric)?.values() ?? []) {
886
+ overall.count += accumulated.count;
887
+ overall.sum += accumulated.sum;
888
+ overall.histogram = mergeVitalHistograms(overall.histogram, accumulated.histogram);
889
+ }
890
+ return {
891
+ metric,
892
+ ...stats(metric, overall),
893
+ devices: [...(byDevice.get(metric) ?? [])]
894
+ .map(([device, accumulated]) => ({
895
+ device,
896
+ ...stats(metric, accumulated),
897
+ }))
898
+ .sort((left, right) => right.count - left.count),
899
+ pages: [...(byPage.get(metric) ?? [])]
900
+ .map(([page, accumulated]) => ({
901
+ page,
902
+ ...stats(metric, accumulated),
903
+ }))
904
+ .sort((left, right) => right.count - left.count || left.page.localeCompare(right.page))
905
+ .slice(0, VITALS_REPORT_PAGE_LIMIT),
906
+ timeSeries: [...(byDay.get(metric) ?? [])]
907
+ .sort(([left], [right]) => left - right)
908
+ .map(([timestamp, accumulated]) => ({
909
+ timestamp,
910
+ count: accumulated.count,
911
+ p75: vitalHistogramPercentile(accumulated.histogram, 0.75),
912
+ })),
913
+ };
914
+ });
915
+ return {
916
+ metrics,
917
+ truncated: false,
918
+ metadata: {
919
+ basis: "dailyRollup",
920
+ coverage: await coverageForRange(ctx, [args.siteId], args.from, args.to, { datasets: ["vitalRollups"], complete: true }),
921
+ range: rangeSemantics(args.from, args.to),
922
+ },
923
+ };
924
+ },
925
+ });
926
+ const coverageDatasetValidator = v.union(v.literal("overviewHour"), v.literal("overviewDay"), v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goals"), v.literal("funnels"), v.literal("affiliates"), v.literal("vitals"));
794
927
  export const dataCoverage = query({
795
928
  args: {
796
929
  siteId: v.id("sites"),
@@ -817,6 +950,7 @@ export const dataCoverage = query({
817
950
  "goals",
818
951
  "funnels",
819
952
  "affiliates",
953
+ "vitals",
820
954
  ];
821
955
  return {
822
956
  siteId: args.siteId,
@@ -960,10 +1094,11 @@ async function coverageForRange(ctx, siteIds, from, to, options) {
960
1094
  else if (retainedBefore !== null && from < retainedBefore) {
961
1095
  state = "partial";
962
1096
  }
963
- else if (options.complete || (availableFrom !== null &&
964
- availableThrough !== null &&
965
- from >= availableFrom &&
966
- to <= availableThrough)) {
1097
+ else if (options.complete ||
1098
+ (availableFrom !== null &&
1099
+ availableThrough !== null &&
1100
+ from >= availableFrom &&
1101
+ to <= availableThrough)) {
967
1102
  state = "complete";
968
1103
  }
969
1104
  else if (sourceRanges.some(Boolean)) {
@@ -1000,6 +1135,14 @@ async function assertSourceAvailable(ctx, siteIds, sources, from) {
1000
1135
  }
1001
1136
  }
1002
1137
  async function datasetCoverageForRange(ctx, siteId, dataset, from, to) {
1138
+ // Vitals have no legacy fallback source; the rollup either covers the range
1139
+ // or the coverage states say how much of it exists.
1140
+ if (dataset === "vitals") {
1141
+ return coverageForRange(ctx, [siteId], from, to, {
1142
+ datasets: ["vitalRollups"],
1143
+ complete: false,
1144
+ });
1145
+ }
1003
1146
  const sourceOptions = dataset === "goals"
1004
1147
  ? [["goalRollups"], ["goalCompletions"]]
1005
1148
  : dataset === "funnels"
@@ -1036,54 +1179,76 @@ async function sourceRange(ctx, siteId, source) {
1036
1179
  if (source === "overviewHour" || source === "overviewDay") {
1037
1180
  const granularity = source === "overviewHour" ? "hour" : "day";
1038
1181
  const [first, last] = await Promise.all([
1039
- ctx.db.query("aggregateBuckets")
1040
- .withIndex("by_siteId_and_granularity_and_bucketStart", (range) => range.eq("siteId", siteId).eq("granularity", granularity)).order("asc").take(1),
1041
- ctx.db.query("aggregateBuckets")
1042
- .withIndex("by_siteId_and_granularity_and_bucketStart", (range) => range.eq("siteId", siteId).eq("granularity", granularity)).order("desc").take(1),
1182
+ ctx.db
1183
+ .query("aggregateBuckets")
1184
+ .withIndex("by_siteId_and_granularity_and_bucketStart", (range) => range.eq("siteId", siteId).eq("granularity", granularity))
1185
+ .order("asc")
1186
+ .take(1),
1187
+ ctx.db
1188
+ .query("aggregateBuckets")
1189
+ .withIndex("by_siteId_and_granularity_and_bucketStart", (range) => range.eq("siteId", siteId).eq("granularity", granularity))
1190
+ .order("desc")
1191
+ .take(1),
1043
1192
  ]);
1044
1193
  return bounds(first[0]?.bucketStart, last[0]?.bucketStart);
1045
1194
  }
1046
1195
  if (source === "events") {
1047
1196
  const [first, last] = await Promise.all([
1048
- ctx.db.query("eventBatches")
1197
+ ctx.db
1198
+ .query("eventBatches")
1049
1199
  .withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
1050
- .order("asc").take(1),
1051
- ctx.db.query("eventBatches")
1200
+ .order("asc")
1201
+ .take(1),
1202
+ ctx.db
1203
+ .query("eventBatches")
1052
1204
  .withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
1053
- .order("desc").take(1),
1205
+ .order("desc")
1206
+ .take(1),
1054
1207
  ]);
1055
1208
  return bounds(first[0]?.bucketStart, last[0]?.bucketStart);
1056
1209
  }
1057
1210
  if (source === "sessions") {
1058
1211
  const [first, last] = await Promise.all([
1059
- ctx.db.query("sessions")
1212
+ ctx.db
1213
+ .query("sessions")
1060
1214
  .withIndex("by_siteId_and_startedAt", (range) => range.eq("siteId", siteId))
1061
- .order("asc").take(1),
1062
- ctx.db.query("sessions")
1215
+ .order("asc")
1216
+ .take(1),
1217
+ ctx.db
1218
+ .query("sessions")
1063
1219
  .withIndex("by_siteId_and_lastSeenAt", (range) => range.eq("siteId", siteId))
1064
- .order("desc").take(1),
1220
+ .order("desc")
1221
+ .take(1),
1065
1222
  ]);
1066
1223
  return bounds(first[0]?.startedAt, last[0]?.lastSeenAt);
1067
1224
  }
1068
1225
  if (source === "conversions") {
1069
1226
  const [first, last] = await Promise.all([
1070
- ctx.db.query("conversions")
1227
+ ctx.db
1228
+ .query("conversions")
1071
1229
  .withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
1072
- .order("asc").take(1),
1073
- ctx.db.query("conversions")
1230
+ .order("asc")
1231
+ .take(1),
1232
+ ctx.db
1233
+ .query("conversions")
1074
1234
  .withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
1075
- .order("desc").take(1),
1235
+ .order("desc")
1236
+ .take(1),
1076
1237
  ]);
1077
1238
  return bounds(first[0]?.timestamp, last[0]?.timestamp);
1078
1239
  }
1079
1240
  if (source === "goalCompletions") {
1080
1241
  const [first, last] = await Promise.all([
1081
- ctx.db.query("goalCompletions")
1242
+ ctx.db
1243
+ .query("goalCompletions")
1082
1244
  .withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
1083
- .order("asc").take(1),
1084
- ctx.db.query("goalCompletions")
1245
+ .order("asc")
1246
+ .take(1),
1247
+ ctx.db
1248
+ .query("goalCompletions")
1085
1249
  .withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
1086
- .order("desc").take(1),
1250
+ .order("desc")
1251
+ .take(1),
1087
1252
  ]);
1088
1253
  return bounds(first[0]?.timestamp, last[0]?.timestamp);
1089
1254
  }
@@ -1095,14 +1260,20 @@ async function sourceRange(ctx, siteId, source) {
1095
1260
  ? "goalDailyRollups"
1096
1261
  : source === "funnelRollups"
1097
1262
  ? "funnelDailyRollups"
1098
- : "affiliateDailyRollups";
1263
+ : source === "vitalRollups"
1264
+ ? "vitalRollups"
1265
+ : "affiliateDailyRollups";
1099
1266
  const [first, last] = await Promise.all([
1100
- ctx.db.query(table)
1267
+ ctx.db
1268
+ .query(table)
1101
1269
  .withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
1102
- .order("asc").take(1),
1103
- ctx.db.query(table)
1270
+ .order("asc")
1271
+ .take(1),
1272
+ ctx.db
1273
+ .query(table)
1104
1274
  .withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
1105
- .order("desc").take(1),
1275
+ .order("desc")
1276
+ .take(1),
1106
1277
  ]);
1107
1278
  return bounds(first[0]?.bucketStart, last[0]?.bucketStart);
1108
1279
  }
@@ -1144,7 +1315,9 @@ function addMetrics(target, source) {
1144
1315
  }
1145
1316
  function validateSiteIds(siteIds) {
1146
1317
  const unique = [...new Set(siteIds)];
1147
- if (unique.length < 1 || unique.length > MAX_REPORT_SITES || unique.length !== siteIds.length) {
1318
+ if (unique.length < 1 ||
1319
+ unique.length > MAX_REPORT_SITES ||
1320
+ unique.length !== siteIds.length) {
1148
1321
  fail("INVALID_ARGUMENT", `siteIds must contain 1-${MAX_REPORT_SITES} unique IDs`);
1149
1322
  }
1150
1323
  return unique;
@@ -1255,9 +1428,7 @@ function parseJourneyPagination(options, scope) {
1255
1428
  function keysetPaginationResult(rows, pagination, cursorFor) {
1256
1429
  const page = rows.slice(0, pagination.numItems);
1257
1430
  const isDone = rows.length <= pagination.numItems;
1258
- const next = page.length > 0
1259
- ? cursorFor(page[page.length - 1])
1260
- : pagination.cursor;
1431
+ const next = page.length > 0 ? cursorFor(page[page.length - 1]) : pagination.cursor;
1261
1432
  return {
1262
1433
  page,
1263
1434
  isDone,
@@ -1301,7 +1472,9 @@ function journeyPaginationResult(rows, pagination) {
1301
1472
  };
1302
1473
  }
1303
1474
  function validatePagination(options) {
1304
- if (!Number.isSafeInteger(options.numItems) || options.numItems < 1 || options.numItems > 100) {
1475
+ if (!Number.isSafeInteger(options.numItems) ||
1476
+ options.numItems < 1 ||
1477
+ options.numItems > 100) {
1305
1478
  fail("INVALID_ARGUMENT", "pagination numItems must be an integer between 1 and 100");
1306
1479
  }
1307
1480
  if (options.maximumRowsRead !== undefined &&