@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
package/src/component/reports.ts
CHANGED
|
@@ -24,7 +24,9 @@ import {
|
|
|
24
24
|
MAX_REPORT_BUCKETS,
|
|
25
25
|
MAX_REPORT_RANGE_DAYS,
|
|
26
26
|
MAX_REPORT_SITES,
|
|
27
|
+
MAX_VITALS_REPORT_ROWS,
|
|
27
28
|
REPORT_ROLLUP_SHARDS,
|
|
29
|
+
VITALS_REPORT_PAGE_LIMIT,
|
|
28
30
|
} from "./constants.js";
|
|
29
31
|
import {
|
|
30
32
|
createVisitorSketch,
|
|
@@ -48,8 +50,17 @@ import {
|
|
|
48
50
|
funnelFieldsValidator,
|
|
49
51
|
goalFieldsValidator,
|
|
50
52
|
sessionFieldsValidator,
|
|
53
|
+
vitalMetricValidator,
|
|
51
54
|
} from "./validators.js";
|
|
52
55
|
import { resolveVisitorIdentities } from "./visitors.js";
|
|
56
|
+
import {
|
|
57
|
+
mergeVitalHistograms,
|
|
58
|
+
VITAL_ALL,
|
|
59
|
+
VITAL_METRICS,
|
|
60
|
+
vitalHistogramPercentile,
|
|
61
|
+
vitalRatingCounts,
|
|
62
|
+
type VitalMetric,
|
|
63
|
+
} from "./vitals.js";
|
|
53
64
|
|
|
54
65
|
const sessionDocumentValidator = sessionFieldsValidator.extend({
|
|
55
66
|
_id: v.id("sessions"),
|
|
@@ -110,7 +121,9 @@ const metricTotalsValidator = v.object({
|
|
|
110
121
|
conversions: v.number(),
|
|
111
122
|
revenueCents: v.number(),
|
|
112
123
|
});
|
|
113
|
-
const timePointValidator = metricTotalsValidator.extend({
|
|
124
|
+
const timePointValidator = metricTotalsValidator.extend({
|
|
125
|
+
timestamp: v.number(),
|
|
126
|
+
});
|
|
114
127
|
const topItemValidator = v.object({
|
|
115
128
|
value: v.string(),
|
|
116
129
|
count: v.number(),
|
|
@@ -199,7 +212,8 @@ export const overview = query({
|
|
|
199
212
|
const currencies = new Set<string>();
|
|
200
213
|
for (const siteId of siteIds) {
|
|
201
214
|
const site = await ctx.db.get("sites", siteId);
|
|
202
|
-
if (!site)
|
|
215
|
+
if (!site)
|
|
216
|
+
fail("NOT_FOUND", "site not found", { siteId: String(siteId) });
|
|
203
217
|
currencies.add(site.currency ?? "USD");
|
|
204
218
|
}
|
|
205
219
|
if (currencies.size !== 1) {
|
|
@@ -208,7 +222,11 @@ export const overview = query({
|
|
|
208
222
|
});
|
|
209
223
|
}
|
|
210
224
|
const currency = [...currencies][0];
|
|
211
|
-
const { from, to } = validateRange(
|
|
225
|
+
const { from, to } = validateRange(
|
|
226
|
+
args.from,
|
|
227
|
+
args.to,
|
|
228
|
+
MAX_REPORT_RANGE_DAYS,
|
|
229
|
+
);
|
|
212
230
|
const interval = args.interval ?? (to - from <= DAY_MS ? "hour" : "day");
|
|
213
231
|
if (interval === "hour" && to - from > DAY_MS) {
|
|
214
232
|
fail("INVALID_ARGUMENT", "hourly reports are limited to 24 hours");
|
|
@@ -218,7 +236,8 @@ export const overview = query({
|
|
|
218
236
|
const firstBucket = Math.floor(from / intervalMs) * intervalMs;
|
|
219
237
|
const lastBucket = Math.floor(to / intervalMs) * intervalMs;
|
|
220
238
|
const bucketCount = Math.floor((lastBucket - firstBucket) / intervalMs) + 1;
|
|
221
|
-
const perSiteLimit =
|
|
239
|
+
const perSiteLimit =
|
|
240
|
+
bucketCount *
|
|
222
241
|
(interval === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
223
242
|
if (perSiteLimit * siteIds.length > MAX_OVERVIEW_AGGREGATE_ROWS) {
|
|
224
243
|
fail(
|
|
@@ -233,10 +252,14 @@ export const overview = query({
|
|
|
233
252
|
let mergedVisitorSketch = visitorSketch;
|
|
234
253
|
const pointVisitorSketches = new Map<number, VisitorSketch>();
|
|
235
254
|
let visitorsComplete = true;
|
|
236
|
-
const dimensions = new Map<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
255
|
+
const dimensions = new Map<
|
|
256
|
+
string,
|
|
257
|
+
{ value: string; count: number; revenueCents: number }
|
|
258
|
+
>();
|
|
259
|
+
const overviewDataset =
|
|
260
|
+
interval === "hour"
|
|
261
|
+
? ("overviewHour" as const)
|
|
262
|
+
: ("overviewDay" as const);
|
|
240
263
|
|
|
241
264
|
await assertSourceAvailable(ctx, siteIds, [overviewDataset], from);
|
|
242
265
|
|
|
@@ -291,7 +314,10 @@ export const overview = query({
|
|
|
291
314
|
[...dimensions.entries()]
|
|
292
315
|
.filter(([key]) => key.startsWith(`${type}:`))
|
|
293
316
|
.map(([, value]) => value)
|
|
294
|
-
.sort(
|
|
317
|
+
.sort(
|
|
318
|
+
(left, right) =>
|
|
319
|
+
right.count - left.count || left.value.localeCompare(right.value),
|
|
320
|
+
)
|
|
295
321
|
.slice(0, 10);
|
|
296
322
|
if (visitorsComplete) {
|
|
297
323
|
totals.visitors = estimateVisitors(mergedVisitorSketch);
|
|
@@ -365,8 +391,15 @@ export const liveVisitors = query({
|
|
|
365
391
|
handler: async (ctx, args) => {
|
|
366
392
|
const siteIds = validateSiteIds(args.siteIds);
|
|
367
393
|
const limit = args.limit ?? 100;
|
|
368
|
-
if (
|
|
369
|
-
|
|
394
|
+
if (
|
|
395
|
+
!Number.isSafeInteger(limit) ||
|
|
396
|
+
limit < 1 ||
|
|
397
|
+
limit > MAX_LIVE_VISITORS
|
|
398
|
+
) {
|
|
399
|
+
fail(
|
|
400
|
+
"INVALID_ARGUMENT",
|
|
401
|
+
`limit must be an integer between 1 and ${MAX_LIVE_VISITORS}`,
|
|
402
|
+
);
|
|
370
403
|
}
|
|
371
404
|
if (!Number.isFinite(args.now) || args.now < 0) {
|
|
372
405
|
fail("INVALID_ARGUMENT", "now must be a positive timestamp");
|
|
@@ -593,22 +626,34 @@ export const visitorJourney = query({
|
|
|
593
626
|
validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
|
|
594
627
|
await assertSourceAvailable(ctx, siteIds, ["events"], args.from);
|
|
595
628
|
const limit = args.limit ?? 200;
|
|
596
|
-
if (
|
|
597
|
-
|
|
629
|
+
if (
|
|
630
|
+
!Number.isSafeInteger(limit) ||
|
|
631
|
+
limit < 1 ||
|
|
632
|
+
limit > MAX_JOURNEY_EVENTS
|
|
633
|
+
) {
|
|
634
|
+
fail(
|
|
635
|
+
"INVALID_ARGUMENT",
|
|
636
|
+
`limit must be an integer between 1 and ${MAX_JOURNEY_EVENTS}`,
|
|
637
|
+
);
|
|
598
638
|
}
|
|
599
639
|
const rows: RawJourneyEvent[] = [];
|
|
600
640
|
for (const siteId of siteIds) {
|
|
601
|
-
for (const visitorId of await resolveVisitorIdentities(
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
641
|
+
for (const visitorId of await resolveVisitorIdentities(
|
|
642
|
+
ctx,
|
|
643
|
+
siteId,
|
|
644
|
+
args.visitorId,
|
|
645
|
+
)) {
|
|
646
|
+
const cutoff =
|
|
647
|
+
rows.length >= limit ? rows[limit - 1]!.timestamp : args.to;
|
|
648
|
+
rows.push(
|
|
649
|
+
...(await loadVisitorJourneyEvents(ctx, {
|
|
650
|
+
siteId,
|
|
651
|
+
visitorId,
|
|
652
|
+
from: args.from,
|
|
653
|
+
to: cutoff,
|
|
654
|
+
resultLimit: limit,
|
|
655
|
+
})),
|
|
656
|
+
);
|
|
612
657
|
rows.sort(compareJourneyEvents);
|
|
613
658
|
if (rows.length > limit) rows.length = limit;
|
|
614
659
|
}
|
|
@@ -639,7 +684,10 @@ export const goalsReport = query({
|
|
|
639
684
|
.take(MAX_GOALS_PER_SITE + 1);
|
|
640
685
|
failIfIncomplete(goalRows, MAX_GOALS_PER_SITE, "goal definitions");
|
|
641
686
|
const goals = goalRows;
|
|
642
|
-
const totals = new Map<
|
|
687
|
+
const totals = new Map<
|
|
688
|
+
string,
|
|
689
|
+
{ completions: number; valueCents: number }
|
|
690
|
+
>();
|
|
643
691
|
const useRollups = await dailyRollupsAvailable(
|
|
644
692
|
ctx,
|
|
645
693
|
args.siteId,
|
|
@@ -696,12 +744,18 @@ export const goalsReport = query({
|
|
|
696
744
|
})),
|
|
697
745
|
truncated: false,
|
|
698
746
|
metadata: {
|
|
699
|
-
basis: rollups ? "dailyRollup" as const : "boundedLegacy" as const,
|
|
700
|
-
coverage: await coverageForRange(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
747
|
+
basis: rollups ? ("dailyRollup" as const) : ("boundedLegacy" as const),
|
|
748
|
+
coverage: await coverageForRange(
|
|
749
|
+
ctx,
|
|
750
|
+
[args.siteId],
|
|
751
|
+
args.from,
|
|
752
|
+
args.to,
|
|
753
|
+
{
|
|
754
|
+
datasets: [rollups ? "goalRollups" : "goalCompletions"],
|
|
755
|
+
complete: true,
|
|
756
|
+
generation: rollups ? 1 : null,
|
|
757
|
+
},
|
|
758
|
+
),
|
|
705
759
|
range: rangeSemantics(args.from, args.to),
|
|
706
760
|
},
|
|
707
761
|
};
|
|
@@ -712,7 +766,12 @@ const funnelReportItemValidator = funnelFieldsValidator.extend({
|
|
|
712
766
|
_id: v.id("funnels"),
|
|
713
767
|
_creationTime: v.number(),
|
|
714
768
|
stepsReport: v.array(
|
|
715
|
-
v.object({
|
|
769
|
+
v.object({
|
|
770
|
+
step: v.number(),
|
|
771
|
+
name: v.string(),
|
|
772
|
+
entrants: v.number(),
|
|
773
|
+
completions: v.number(),
|
|
774
|
+
}),
|
|
716
775
|
),
|
|
717
776
|
truncated: v.boolean(),
|
|
718
777
|
metadata: reportMetadataValidator,
|
|
@@ -802,7 +861,9 @@ export const funnelsReport = query({
|
|
|
802
861
|
stepsReport,
|
|
803
862
|
truncated: false,
|
|
804
863
|
metadata: {
|
|
805
|
-
basis: rollups
|
|
864
|
+
basis: rollups
|
|
865
|
+
? ("dailyRollup" as const)
|
|
866
|
+
: ("boundedLegacy" as const),
|
|
806
867
|
coverage: reportCoverage,
|
|
807
868
|
range: rangeSemantics(args.from, args.to),
|
|
808
869
|
},
|
|
@@ -842,7 +903,11 @@ export const affiliatesReport = query({
|
|
|
842
903
|
.withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
|
|
843
904
|
.order("desc")
|
|
844
905
|
.take(MAX_AFFILIATES_PER_SITE + 1);
|
|
845
|
-
failIfIncomplete(
|
|
906
|
+
failIfIncomplete(
|
|
907
|
+
affiliateRows,
|
|
908
|
+
MAX_AFFILIATES_PER_SITE,
|
|
909
|
+
"affiliate definitions",
|
|
910
|
+
);
|
|
846
911
|
const affiliates = affiliateRows;
|
|
847
912
|
const useRollups = await dailyRollupsAvailable(
|
|
848
913
|
ctx,
|
|
@@ -860,16 +925,22 @@ export const affiliatesReport = query({
|
|
|
860
925
|
args.to,
|
|
861
926
|
)
|
|
862
927
|
: null;
|
|
863
|
-
const visits = new Map<
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
928
|
+
const visits = new Map<
|
|
929
|
+
string,
|
|
930
|
+
{
|
|
931
|
+
visits: number;
|
|
932
|
+
visitors: Set<string>;
|
|
933
|
+
sketch: VisitorSketch;
|
|
934
|
+
}
|
|
935
|
+
>();
|
|
936
|
+
const totals = new Map<
|
|
937
|
+
string,
|
|
938
|
+
{
|
|
939
|
+
conversions: number;
|
|
940
|
+
revenueCents: number;
|
|
941
|
+
commissionCents: number;
|
|
942
|
+
}
|
|
943
|
+
>();
|
|
873
944
|
if (rollups) {
|
|
874
945
|
for (const row of rollups) {
|
|
875
946
|
const key = String(row.affiliateId);
|
|
@@ -960,22 +1031,30 @@ export const affiliatesReport = query({
|
|
|
960
1031
|
visits: traffic?.visits ?? 0,
|
|
961
1032
|
uniqueVisitors: rollups
|
|
962
1033
|
? estimateVisitors(traffic?.sketch ?? createVisitorSketch())
|
|
963
|
-
: traffic?.visitors.size ?? 0,
|
|
1034
|
+
: (traffic?.visitors.size ?? 0),
|
|
964
1035
|
};
|
|
965
1036
|
}),
|
|
966
1037
|
truncated: false,
|
|
967
1038
|
metadata: {
|
|
968
|
-
basis: rollups ? "dailyRollup" as const : "boundedLegacy" as const,
|
|
969
|
-
coverage: await coverageForRange(
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1039
|
+
basis: rollups ? ("dailyRollup" as const) : ("boundedLegacy" as const),
|
|
1040
|
+
coverage: await coverageForRange(
|
|
1041
|
+
ctx,
|
|
1042
|
+
[args.siteId],
|
|
1043
|
+
args.from,
|
|
1044
|
+
args.to,
|
|
1045
|
+
{
|
|
1046
|
+
datasets: rollups
|
|
1047
|
+
? ["affiliateRollups"]
|
|
1048
|
+
: ["affiliateStats", "sessions"],
|
|
1049
|
+
complete: true,
|
|
1050
|
+
generation: rollups ? 1 : null,
|
|
1051
|
+
},
|
|
1052
|
+
),
|
|
976
1053
|
range: rangeSemantics(args.from, args.to),
|
|
977
1054
|
uniqueVisitors: {
|
|
978
|
-
basis: rollups
|
|
1055
|
+
basis: rollups
|
|
1056
|
+
? ("estimatedUnique" as const)
|
|
1057
|
+
: ("exactLegacy" as const),
|
|
979
1058
|
exact: !rollups,
|
|
980
1059
|
},
|
|
981
1060
|
},
|
|
@@ -983,6 +1062,153 @@ export const affiliatesReport = query({
|
|
|
983
1062
|
},
|
|
984
1063
|
});
|
|
985
1064
|
|
|
1065
|
+
const vitalStatsValidator = v.object({
|
|
1066
|
+
count: v.number(),
|
|
1067
|
+
p75: v.number(),
|
|
1068
|
+
average: v.number(),
|
|
1069
|
+
good: v.number(),
|
|
1070
|
+
needsImprovement: v.number(),
|
|
1071
|
+
poor: v.number(),
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
export const vitalsReport = query({
|
|
1075
|
+
args: { siteId: v.id("sites"), from: v.number(), to: v.number() },
|
|
1076
|
+
returns: v.object({
|
|
1077
|
+
metrics: v.array(
|
|
1078
|
+
vitalStatsValidator.extend({
|
|
1079
|
+
metric: vitalMetricValidator,
|
|
1080
|
+
devices: v.array(vitalStatsValidator.extend({ device: v.string() })),
|
|
1081
|
+
pages: v.array(vitalStatsValidator.extend({ page: v.string() })),
|
|
1082
|
+
timeSeries: v.array(
|
|
1083
|
+
v.object({
|
|
1084
|
+
timestamp: v.number(),
|
|
1085
|
+
count: v.number(),
|
|
1086
|
+
p75: v.number(),
|
|
1087
|
+
}),
|
|
1088
|
+
),
|
|
1089
|
+
}),
|
|
1090
|
+
),
|
|
1091
|
+
truncated: v.boolean(),
|
|
1092
|
+
metadata: reportMetadataValidator,
|
|
1093
|
+
}),
|
|
1094
|
+
handler: async (ctx, args) => {
|
|
1095
|
+
validateRange(args.from, args.to, MAX_FEATURE_ROLLUP_DAYS);
|
|
1096
|
+
requireAlignedRange(args.from, args.to, DAY_MS, "vitals report");
|
|
1097
|
+
await assertSourceAvailable(
|
|
1098
|
+
ctx,
|
|
1099
|
+
[args.siteId],
|
|
1100
|
+
["vitalRollups"],
|
|
1101
|
+
args.from,
|
|
1102
|
+
);
|
|
1103
|
+
const rows = await ctx.db
|
|
1104
|
+
.query("vitalRollups")
|
|
1105
|
+
.withIndex("by_siteId_and_bucketStart", (range) =>
|
|
1106
|
+
range
|
|
1107
|
+
.eq("siteId", args.siteId)
|
|
1108
|
+
.gte("bucketStart", args.from)
|
|
1109
|
+
.lte("bucketStart", args.to),
|
|
1110
|
+
)
|
|
1111
|
+
.take(MAX_VITALS_REPORT_ROWS + 1);
|
|
1112
|
+
failIfIncomplete(rows, MAX_VITALS_REPORT_ROWS, "vital rollups");
|
|
1113
|
+
|
|
1114
|
+
type Accumulated = { count: number; sum: number; histogram: number[] };
|
|
1115
|
+
const fold = <Key>(
|
|
1116
|
+
groups: Map<VitalMetric, Map<Key, Accumulated>>,
|
|
1117
|
+
metric: VitalMetric,
|
|
1118
|
+
key: Key,
|
|
1119
|
+
entry: { count: number; sum: number; histogram: number[] },
|
|
1120
|
+
) => {
|
|
1121
|
+
const group = groups.get(metric) ?? new Map<Key, Accumulated>();
|
|
1122
|
+
const current = group.get(key) ?? { count: 0, sum: 0, histogram: [] };
|
|
1123
|
+
current.count += entry.count;
|
|
1124
|
+
current.sum += entry.sum;
|
|
1125
|
+
current.histogram = mergeVitalHistograms(
|
|
1126
|
+
current.histogram,
|
|
1127
|
+
entry.histogram,
|
|
1128
|
+
);
|
|
1129
|
+
group.set(key, current);
|
|
1130
|
+
groups.set(metric, group);
|
|
1131
|
+
};
|
|
1132
|
+
const byDevice = new Map<VitalMetric, Map<string, Accumulated>>();
|
|
1133
|
+
const byPage = new Map<VitalMetric, Map<string, Accumulated>>();
|
|
1134
|
+
const byDay = new Map<VitalMetric, Map<number, Accumulated>>();
|
|
1135
|
+
for (const row of rows) {
|
|
1136
|
+
for (const entry of row.metrics) {
|
|
1137
|
+
if (row.page === VITAL_ALL) {
|
|
1138
|
+
// A device row carries the complete signal once, so overall totals
|
|
1139
|
+
// and the time series derive from this family alone.
|
|
1140
|
+
fold(byDevice, entry.metric, row.device, entry);
|
|
1141
|
+
fold(byDay, entry.metric, row.bucketStart, entry);
|
|
1142
|
+
} else {
|
|
1143
|
+
fold(byPage, entry.metric, row.page, entry);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const stats = (metric: VitalMetric, accumulated: Accumulated) => ({
|
|
1148
|
+
count: accumulated.count,
|
|
1149
|
+
p75: vitalHistogramPercentile(accumulated.histogram, 0.75),
|
|
1150
|
+
average:
|
|
1151
|
+
accumulated.count > 0
|
|
1152
|
+
? Math.round(accumulated.sum / accumulated.count)
|
|
1153
|
+
: 0,
|
|
1154
|
+
...vitalRatingCounts(accumulated.histogram, metric),
|
|
1155
|
+
});
|
|
1156
|
+
const metrics = VITAL_METRICS.map((metric) => {
|
|
1157
|
+
const overall: Accumulated = { count: 0, sum: 0, histogram: [] };
|
|
1158
|
+
for (const accumulated of byDevice.get(metric)?.values() ?? []) {
|
|
1159
|
+
overall.count += accumulated.count;
|
|
1160
|
+
overall.sum += accumulated.sum;
|
|
1161
|
+
overall.histogram = mergeVitalHistograms(
|
|
1162
|
+
overall.histogram,
|
|
1163
|
+
accumulated.histogram,
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
metric,
|
|
1168
|
+
...stats(metric, overall),
|
|
1169
|
+
devices: [...(byDevice.get(metric) ?? [])]
|
|
1170
|
+
.map(([device, accumulated]) => ({
|
|
1171
|
+
device,
|
|
1172
|
+
...stats(metric, accumulated),
|
|
1173
|
+
}))
|
|
1174
|
+
.sort((left, right) => right.count - left.count),
|
|
1175
|
+
pages: [...(byPage.get(metric) ?? [])]
|
|
1176
|
+
.map(([page, accumulated]) => ({
|
|
1177
|
+
page,
|
|
1178
|
+
...stats(metric, accumulated),
|
|
1179
|
+
}))
|
|
1180
|
+
.sort(
|
|
1181
|
+
(left, right) =>
|
|
1182
|
+
right.count - left.count || left.page.localeCompare(right.page),
|
|
1183
|
+
)
|
|
1184
|
+
.slice(0, VITALS_REPORT_PAGE_LIMIT),
|
|
1185
|
+
timeSeries: [...(byDay.get(metric) ?? [])]
|
|
1186
|
+
.sort(([left], [right]) => left - right)
|
|
1187
|
+
.map(([timestamp, accumulated]) => ({
|
|
1188
|
+
timestamp,
|
|
1189
|
+
count: accumulated.count,
|
|
1190
|
+
p75: vitalHistogramPercentile(accumulated.histogram, 0.75),
|
|
1191
|
+
})),
|
|
1192
|
+
};
|
|
1193
|
+
});
|
|
1194
|
+
return {
|
|
1195
|
+
metrics,
|
|
1196
|
+
truncated: false,
|
|
1197
|
+
metadata: {
|
|
1198
|
+
basis: "dailyRollup" as const,
|
|
1199
|
+
coverage: await coverageForRange(
|
|
1200
|
+
ctx,
|
|
1201
|
+
[args.siteId],
|
|
1202
|
+
args.from,
|
|
1203
|
+
args.to,
|
|
1204
|
+
{ datasets: ["vitalRollups"], complete: true },
|
|
1205
|
+
),
|
|
1206
|
+
range: rangeSemantics(args.from, args.to),
|
|
1207
|
+
},
|
|
1208
|
+
};
|
|
1209
|
+
},
|
|
1210
|
+
});
|
|
1211
|
+
|
|
986
1212
|
const coverageDatasetValidator = v.union(
|
|
987
1213
|
v.literal("overviewHour"),
|
|
988
1214
|
v.literal("overviewDay"),
|
|
@@ -992,6 +1218,7 @@ const coverageDatasetValidator = v.union(
|
|
|
992
1218
|
v.literal("goals"),
|
|
993
1219
|
v.literal("funnels"),
|
|
994
1220
|
v.literal("affiliates"),
|
|
1221
|
+
v.literal("vitals"),
|
|
995
1222
|
);
|
|
996
1223
|
|
|
997
1224
|
export const dataCoverage = query({
|
|
@@ -1004,10 +1231,12 @@ export const dataCoverage = query({
|
|
|
1004
1231
|
siteId: v.id("sites"),
|
|
1005
1232
|
from: v.number(),
|
|
1006
1233
|
to: v.number(),
|
|
1007
|
-
datasets: v.array(
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1234
|
+
datasets: v.array(
|
|
1235
|
+
v.object({
|
|
1236
|
+
dataset: coverageDatasetValidator,
|
|
1237
|
+
coverage: resultCoverageValidator,
|
|
1238
|
+
}),
|
|
1239
|
+
),
|
|
1011
1240
|
}),
|
|
1012
1241
|
handler: async (ctx, args) => {
|
|
1013
1242
|
validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
|
|
@@ -1020,21 +1249,24 @@ export const dataCoverage = query({
|
|
|
1020
1249
|
"goals",
|
|
1021
1250
|
"funnels",
|
|
1022
1251
|
"affiliates",
|
|
1252
|
+
"vitals",
|
|
1023
1253
|
] as const;
|
|
1024
1254
|
return {
|
|
1025
1255
|
siteId: args.siteId,
|
|
1026
1256
|
from: args.from,
|
|
1027
1257
|
to: args.to,
|
|
1028
|
-
datasets: await Promise.all(
|
|
1029
|
-
dataset
|
|
1030
|
-
coverage: await datasetCoverageForRange(
|
|
1031
|
-
ctx,
|
|
1032
|
-
args.siteId,
|
|
1258
|
+
datasets: await Promise.all(
|
|
1259
|
+
datasets.map(async (dataset) => ({
|
|
1033
1260
|
dataset,
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1261
|
+
coverage: await datasetCoverageForRange(
|
|
1262
|
+
ctx,
|
|
1263
|
+
args.siteId,
|
|
1264
|
+
dataset,
|
|
1265
|
+
args.from,
|
|
1266
|
+
args.to,
|
|
1267
|
+
),
|
|
1268
|
+
})),
|
|
1269
|
+
),
|
|
1038
1270
|
};
|
|
1039
1271
|
},
|
|
1040
1272
|
});
|
|
@@ -1144,7 +1376,11 @@ async function loadAffiliateDailyRollups(
|
|
|
1144
1376
|
.lte("bucketStart", to),
|
|
1145
1377
|
)
|
|
1146
1378
|
.take(perAffiliateLimit + 1);
|
|
1147
|
-
failIfIncomplete(
|
|
1379
|
+
failIfIncomplete(
|
|
1380
|
+
affiliateRows,
|
|
1381
|
+
perAffiliateLimit,
|
|
1382
|
+
"affiliate daily rollups",
|
|
1383
|
+
);
|
|
1148
1384
|
rows.push(...affiliateRows);
|
|
1149
1385
|
await requireFeatureReadHeadroom(ctx, "affiliate daily rollups");
|
|
1150
1386
|
}
|
|
@@ -1172,20 +1408,28 @@ async function requireFeatureReadHeadroom(ctx: QueryCtx, dataset: string) {
|
|
|
1172
1408
|
metrics.bytesRead.remaining < FEATURE_REPORT_BYTE_RESERVE ||
|
|
1173
1409
|
metrics.documentsRead.remaining < FEATURE_REPORT_DOCUMENT_RESERVE
|
|
1174
1410
|
) {
|
|
1175
|
-
fail(
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1411
|
+
fail(
|
|
1412
|
+
"REPORT_INCOMPLETE",
|
|
1413
|
+
`${dataset} exceeds its byte or document budget`,
|
|
1414
|
+
{
|
|
1415
|
+
dataset,
|
|
1416
|
+
bytesRead: metrics.bytesRead.used,
|
|
1417
|
+
documentsRead: metrics.documentsRead.used,
|
|
1418
|
+
},
|
|
1419
|
+
);
|
|
1180
1420
|
}
|
|
1181
1421
|
}
|
|
1182
1422
|
|
|
1183
1423
|
function failIfIncomplete<T>(rows: T[], maximum: number, dataset: string) {
|
|
1184
1424
|
if (rows.length > maximum) {
|
|
1185
|
-
fail(
|
|
1186
|
-
|
|
1187
|
-
dataset
|
|
1188
|
-
|
|
1425
|
+
fail(
|
|
1426
|
+
"REPORT_INCOMPLETE",
|
|
1427
|
+
`${dataset} exceeds the complete report read budget`,
|
|
1428
|
+
{
|
|
1429
|
+
maximumRows: maximum,
|
|
1430
|
+
dataset,
|
|
1431
|
+
},
|
|
1432
|
+
);
|
|
1189
1433
|
}
|
|
1190
1434
|
}
|
|
1191
1435
|
|
|
@@ -1200,45 +1444,52 @@ async function coverageForRange(
|
|
|
1200
1444
|
generation?: 1 | null;
|
|
1201
1445
|
},
|
|
1202
1446
|
) {
|
|
1203
|
-
const controls = await Promise.all(
|
|
1204
|
-
|
|
1205
|
-
.
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1447
|
+
const controls = await Promise.all(
|
|
1448
|
+
siteIds.map((siteId) =>
|
|
1449
|
+
ctx.db
|
|
1450
|
+
.query("analyticsCoverage")
|
|
1451
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
1452
|
+
.unique(),
|
|
1453
|
+
),
|
|
1454
|
+
);
|
|
1455
|
+
const sourceRanges = await Promise.all(
|
|
1456
|
+
siteIds.map((siteId) => sourceSetRange(ctx, siteId, options.datasets)),
|
|
1457
|
+
);
|
|
1212
1458
|
const retainedBefore = controls.reduce<number | null>((latest, control) => {
|
|
1213
1459
|
const before = control?.retained
|
|
1214
|
-
.filter((item) =>
|
|
1460
|
+
.filter((item) =>
|
|
1461
|
+
options.datasets.some((dataset) => dataset === item.dataset),
|
|
1462
|
+
)
|
|
1215
1463
|
.reduce<number | undefined>(
|
|
1216
1464
|
(latest, item) => Math.max(latest ?? 0, item.before),
|
|
1217
1465
|
undefined,
|
|
1218
1466
|
);
|
|
1219
1467
|
return before === undefined ? latest : Math.max(latest ?? 0, before);
|
|
1220
1468
|
}, null);
|
|
1221
|
-
const availableFrom =
|
|
1222
|
-
|
|
1223
|
-
?
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1469
|
+
const availableFrom =
|
|
1470
|
+
options.generation === 1
|
|
1471
|
+
? controls.length > 0 && controls.every(Boolean)
|
|
1472
|
+
? Math.max(...controls.map((control) => control!.rollupCompleteFrom))
|
|
1473
|
+
: null
|
|
1474
|
+
: sourceRanges.length > 0 && sourceRanges.every(Boolean)
|
|
1475
|
+
? Math.max(...sourceRanges.map((range) => range!.from))
|
|
1476
|
+
: null;
|
|
1477
|
+
const availableThrough =
|
|
1478
|
+
sourceRanges.length > 0 && sourceRanges.every(Boolean)
|
|
1479
|
+
? Math.min(...sourceRanges.map((range) => range!.through))
|
|
1227
1480
|
: null;
|
|
1228
|
-
const availableThrough = sourceRanges.length > 0 && sourceRanges.every(Boolean)
|
|
1229
|
-
? Math.min(...sourceRanges.map((range) => range!.through))
|
|
1230
|
-
: null;
|
|
1231
1481
|
let state: "complete" | "partial" | "retained" | "unavailable";
|
|
1232
1482
|
if (retainedBefore !== null && to < retainedBefore) {
|
|
1233
1483
|
state = "unavailable";
|
|
1234
1484
|
} else if (retainedBefore !== null && from < retainedBefore) {
|
|
1235
1485
|
state = "partial";
|
|
1236
|
-
} else if (
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1486
|
+
} else if (
|
|
1487
|
+
options.complete ||
|
|
1488
|
+
(availableFrom !== null &&
|
|
1489
|
+
availableThrough !== null &&
|
|
1490
|
+
from >= availableFrom &&
|
|
1491
|
+
to <= availableThrough)
|
|
1492
|
+
) {
|
|
1242
1493
|
state = "complete";
|
|
1243
1494
|
} else if (sourceRanges.some(Boolean)) {
|
|
1244
1495
|
state = "retained";
|
|
@@ -1260,12 +1511,14 @@ async function assertSourceAvailable(
|
|
|
1260
1511
|
sources: CoverageSource[],
|
|
1261
1512
|
from: number,
|
|
1262
1513
|
) {
|
|
1263
|
-
const rows = await Promise.all(
|
|
1264
|
-
|
|
1265
|
-
.
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1514
|
+
const rows = await Promise.all(
|
|
1515
|
+
siteIds.map((siteId) =>
|
|
1516
|
+
ctx.db
|
|
1517
|
+
.query("analyticsCoverage")
|
|
1518
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
1519
|
+
.unique(),
|
|
1520
|
+
),
|
|
1521
|
+
);
|
|
1269
1522
|
const cutoff = rows.reduce<number | null>((latest, row) => {
|
|
1270
1523
|
for (const retained of row?.retained ?? []) {
|
|
1271
1524
|
if (sources.some((source) => source === retained.dataset)) {
|
|
@@ -1275,9 +1528,13 @@ async function assertSourceAvailable(
|
|
|
1275
1528
|
return latest;
|
|
1276
1529
|
}, null);
|
|
1277
1530
|
if (cutoff !== null && from < cutoff) {
|
|
1278
|
-
fail(
|
|
1279
|
-
|
|
1280
|
-
|
|
1531
|
+
fail(
|
|
1532
|
+
"REPORT_INCOMPLETE",
|
|
1533
|
+
"requested report range intersects retained data",
|
|
1534
|
+
{
|
|
1535
|
+
retainedBefore: cutoff,
|
|
1536
|
+
},
|
|
1537
|
+
);
|
|
1281
1538
|
}
|
|
1282
1539
|
}
|
|
1283
1540
|
|
|
@@ -1288,31 +1545,42 @@ async function datasetCoverageForRange(
|
|
|
1288
1545
|
from: number,
|
|
1289
1546
|
to: number,
|
|
1290
1547
|
) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
? [["funnelRollups"], ["funnelSteps"]]
|
|
1295
|
-
: dataset === "affiliates"
|
|
1296
|
-
? [["affiliateRollups"], ["affiliateStats", "sessions"]]
|
|
1297
|
-
: [[dataset]];
|
|
1298
|
-
const options = (await Promise.all(sourceOptions.map(async (sources) => {
|
|
1299
|
-
const rollupDataset = sources[0] as
|
|
1300
|
-
| "goalRollups"
|
|
1301
|
-
| "funnelRollups"
|
|
1302
|
-
| "affiliateRollups";
|
|
1303
|
-
const isRollup = rollupDataset.endsWith("Rollups");
|
|
1304
|
-
const complete = isRollup
|
|
1305
|
-
? await dailyRollupsAvailable(ctx, siteId, from, to, rollupDataset)
|
|
1306
|
-
: false;
|
|
1307
|
-
if (isRollup && !complete) return null;
|
|
1548
|
+
// Vitals have no legacy fallback source; the rollup either covers the range
|
|
1549
|
+
// or the coverage states say how much of it exists.
|
|
1550
|
+
if (dataset === "vitals") {
|
|
1308
1551
|
return coverageForRange(ctx, [siteId], from, to, {
|
|
1309
|
-
datasets:
|
|
1310
|
-
complete,
|
|
1311
|
-
generation: complete ? 1 : null,
|
|
1552
|
+
datasets: ["vitalRollups"],
|
|
1553
|
+
complete: false,
|
|
1312
1554
|
});
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1555
|
+
}
|
|
1556
|
+
const sourceOptions: CoverageSource[][] =
|
|
1557
|
+
dataset === "goals"
|
|
1558
|
+
? [["goalRollups"], ["goalCompletions"]]
|
|
1559
|
+
: dataset === "funnels"
|
|
1560
|
+
? [["funnelRollups"], ["funnelSteps"]]
|
|
1561
|
+
: dataset === "affiliates"
|
|
1562
|
+
? [["affiliateRollups"], ["affiliateStats", "sessions"]]
|
|
1563
|
+
: [[dataset]];
|
|
1564
|
+
const options = (
|
|
1565
|
+
await Promise.all(
|
|
1566
|
+
sourceOptions.map(async (sources) => {
|
|
1567
|
+
const rollupDataset = sources[0] as
|
|
1568
|
+
"goalRollups" | "funnelRollups" | "affiliateRollups";
|
|
1569
|
+
const isRollup = rollupDataset.endsWith("Rollups");
|
|
1570
|
+
const complete = isRollup
|
|
1571
|
+
? await dailyRollupsAvailable(ctx, siteId, from, to, rollupDataset)
|
|
1572
|
+
: false;
|
|
1573
|
+
if (isRollup && !complete) return null;
|
|
1574
|
+
return coverageForRange(ctx, [siteId], from, to, {
|
|
1575
|
+
datasets: sources,
|
|
1576
|
+
complete,
|
|
1577
|
+
generation: complete ? 1 : null,
|
|
1578
|
+
});
|
|
1579
|
+
}),
|
|
1580
|
+
)
|
|
1581
|
+
).filter((option): option is NonNullable<typeof option> => option !== null);
|
|
1582
|
+
return options.sort(
|
|
1583
|
+
(left, right) => coverageRank(right.state) - coverageRank(left.state),
|
|
1316
1584
|
)[0]!;
|
|
1317
1585
|
}
|
|
1318
1586
|
|
|
@@ -1324,7 +1592,8 @@ type PublicCoverageDataset =
|
|
|
1324
1592
|
| "conversions"
|
|
1325
1593
|
| "goals"
|
|
1326
1594
|
| "funnels"
|
|
1327
|
-
| "affiliates"
|
|
1595
|
+
| "affiliates"
|
|
1596
|
+
| "vitals";
|
|
1328
1597
|
|
|
1329
1598
|
type CoverageSource =
|
|
1330
1599
|
| "overviewHour"
|
|
@@ -1337,7 +1606,8 @@ type CoverageSource =
|
|
|
1337
1606
|
| "affiliateStats"
|
|
1338
1607
|
| "goalRollups"
|
|
1339
1608
|
| "funnelRollups"
|
|
1340
|
-
| "affiliateRollups"
|
|
1609
|
+
| "affiliateRollups"
|
|
1610
|
+
| "vitalRollups";
|
|
1341
1611
|
|
|
1342
1612
|
type SourceRange = { from: number; through: number };
|
|
1343
1613
|
|
|
@@ -1346,9 +1616,9 @@ async function sourceSetRange(
|
|
|
1346
1616
|
siteId: Id<"sites">,
|
|
1347
1617
|
sources: CoverageSource[],
|
|
1348
1618
|
): Promise<SourceRange | null> {
|
|
1349
|
-
const ranges = (
|
|
1350
|
-
sources.map((source) => sourceRange(ctx, siteId, source))
|
|
1351
|
-
)
|
|
1619
|
+
const ranges = (
|
|
1620
|
+
await Promise.all(sources.map((source) => sourceRange(ctx, siteId, source)))
|
|
1621
|
+
).filter((range): range is SourceRange => range !== null);
|
|
1352
1622
|
if (ranges.length === 0) return null;
|
|
1353
1623
|
return {
|
|
1354
1624
|
from: Math.min(...ranges.map((range) => range.from)),
|
|
@@ -1362,79 +1632,129 @@ async function sourceRange(
|
|
|
1362
1632
|
source: CoverageSource,
|
|
1363
1633
|
): Promise<SourceRange | null> {
|
|
1364
1634
|
if (source === "overviewHour" || source === "overviewDay") {
|
|
1365
|
-
const granularity =
|
|
1635
|
+
const granularity =
|
|
1636
|
+
source === "overviewHour" ? ("hour" as const) : ("day" as const);
|
|
1366
1637
|
const [first, last] = await Promise.all([
|
|
1367
|
-
ctx.db
|
|
1638
|
+
ctx.db
|
|
1639
|
+
.query("aggregateBuckets")
|
|
1368
1640
|
.withIndex("by_siteId_and_granularity_and_bucketStart", (range) =>
|
|
1369
1641
|
range.eq("siteId", siteId).eq("granularity", granularity),
|
|
1370
|
-
)
|
|
1371
|
-
|
|
1642
|
+
)
|
|
1643
|
+
.order("asc")
|
|
1644
|
+
.take(1),
|
|
1645
|
+
ctx.db
|
|
1646
|
+
.query("aggregateBuckets")
|
|
1372
1647
|
.withIndex("by_siteId_and_granularity_and_bucketStart", (range) =>
|
|
1373
1648
|
range.eq("siteId", siteId).eq("granularity", granularity),
|
|
1374
|
-
)
|
|
1649
|
+
)
|
|
1650
|
+
.order("desc")
|
|
1651
|
+
.take(1),
|
|
1375
1652
|
]);
|
|
1376
1653
|
return bounds(first[0]?.bucketStart, last[0]?.bucketStart);
|
|
1377
1654
|
}
|
|
1378
1655
|
if (source === "events") {
|
|
1379
1656
|
const [first, last] = await Promise.all([
|
|
1380
|
-
ctx.db
|
|
1381
|
-
.
|
|
1382
|
-
.
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
.order("
|
|
1657
|
+
ctx.db
|
|
1658
|
+
.query("eventBatches")
|
|
1659
|
+
.withIndex("by_siteId_and_bucketStart", (range) =>
|
|
1660
|
+
range.eq("siteId", siteId),
|
|
1661
|
+
)
|
|
1662
|
+
.order("asc")
|
|
1663
|
+
.take(1),
|
|
1664
|
+
ctx.db
|
|
1665
|
+
.query("eventBatches")
|
|
1666
|
+
.withIndex("by_siteId_and_bucketStart", (range) =>
|
|
1667
|
+
range.eq("siteId", siteId),
|
|
1668
|
+
)
|
|
1669
|
+
.order("desc")
|
|
1670
|
+
.take(1),
|
|
1386
1671
|
]);
|
|
1387
1672
|
return bounds(first[0]?.bucketStart, last[0]?.bucketStart);
|
|
1388
1673
|
}
|
|
1389
1674
|
if (source === "sessions") {
|
|
1390
1675
|
const [first, last] = await Promise.all([
|
|
1391
|
-
ctx.db
|
|
1392
|
-
.
|
|
1393
|
-
.
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
.order("
|
|
1676
|
+
ctx.db
|
|
1677
|
+
.query("sessions")
|
|
1678
|
+
.withIndex("by_siteId_and_startedAt", (range) =>
|
|
1679
|
+
range.eq("siteId", siteId),
|
|
1680
|
+
)
|
|
1681
|
+
.order("asc")
|
|
1682
|
+
.take(1),
|
|
1683
|
+
ctx.db
|
|
1684
|
+
.query("sessions")
|
|
1685
|
+
.withIndex("by_siteId_and_lastSeenAt", (range) =>
|
|
1686
|
+
range.eq("siteId", siteId),
|
|
1687
|
+
)
|
|
1688
|
+
.order("desc")
|
|
1689
|
+
.take(1),
|
|
1397
1690
|
]);
|
|
1398
1691
|
return bounds(first[0]?.startedAt, last[0]?.lastSeenAt);
|
|
1399
1692
|
}
|
|
1400
1693
|
if (source === "conversions") {
|
|
1401
1694
|
const [first, last] = await Promise.all([
|
|
1402
|
-
ctx.db
|
|
1403
|
-
.
|
|
1404
|
-
.
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
.order("
|
|
1695
|
+
ctx.db
|
|
1696
|
+
.query("conversions")
|
|
1697
|
+
.withIndex("by_siteId_and_timestamp", (range) =>
|
|
1698
|
+
range.eq("siteId", siteId),
|
|
1699
|
+
)
|
|
1700
|
+
.order("asc")
|
|
1701
|
+
.take(1),
|
|
1702
|
+
ctx.db
|
|
1703
|
+
.query("conversions")
|
|
1704
|
+
.withIndex("by_siteId_and_timestamp", (range) =>
|
|
1705
|
+
range.eq("siteId", siteId),
|
|
1706
|
+
)
|
|
1707
|
+
.order("desc")
|
|
1708
|
+
.take(1),
|
|
1408
1709
|
]);
|
|
1409
1710
|
return bounds(first[0]?.timestamp, last[0]?.timestamp);
|
|
1410
1711
|
}
|
|
1411
1712
|
if (source === "goalCompletions") {
|
|
1412
1713
|
const [first, last] = await Promise.all([
|
|
1413
|
-
ctx.db
|
|
1414
|
-
.
|
|
1415
|
-
.
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
.order("
|
|
1714
|
+
ctx.db
|
|
1715
|
+
.query("goalCompletions")
|
|
1716
|
+
.withIndex("by_siteId_and_timestamp", (range) =>
|
|
1717
|
+
range.eq("siteId", siteId),
|
|
1718
|
+
)
|
|
1719
|
+
.order("asc")
|
|
1720
|
+
.take(1),
|
|
1721
|
+
ctx.db
|
|
1722
|
+
.query("goalCompletions")
|
|
1723
|
+
.withIndex("by_siteId_and_timestamp", (range) =>
|
|
1724
|
+
range.eq("siteId", siteId),
|
|
1725
|
+
)
|
|
1726
|
+
.order("desc")
|
|
1727
|
+
.take(1),
|
|
1419
1728
|
]);
|
|
1420
1729
|
return bounds(first[0]?.timestamp, last[0]?.timestamp);
|
|
1421
1730
|
}
|
|
1422
|
-
const table =
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1731
|
+
const table =
|
|
1732
|
+
source === "funnelSteps"
|
|
1733
|
+
? ("funnelStepBuckets" as const)
|
|
1734
|
+
: source === "affiliateStats"
|
|
1735
|
+
? ("affiliateDailyStats" as const)
|
|
1736
|
+
: source === "goalRollups"
|
|
1737
|
+
? ("goalDailyRollups" as const)
|
|
1738
|
+
: source === "funnelRollups"
|
|
1739
|
+
? ("funnelDailyRollups" as const)
|
|
1740
|
+
: source === "vitalRollups"
|
|
1741
|
+
? ("vitalRollups" as const)
|
|
1742
|
+
: ("affiliateDailyRollups" as const);
|
|
1431
1743
|
const [first, last] = await Promise.all([
|
|
1432
|
-
ctx.db
|
|
1433
|
-
.
|
|
1434
|
-
.
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
.order("
|
|
1744
|
+
ctx.db
|
|
1745
|
+
.query(table)
|
|
1746
|
+
.withIndex("by_siteId_and_bucketStart", (range) =>
|
|
1747
|
+
range.eq("siteId", siteId),
|
|
1748
|
+
)
|
|
1749
|
+
.order("asc")
|
|
1750
|
+
.take(1),
|
|
1751
|
+
ctx.db
|
|
1752
|
+
.query(table)
|
|
1753
|
+
.withIndex("by_siteId_and_bucketStart", (range) =>
|
|
1754
|
+
range.eq("siteId", siteId),
|
|
1755
|
+
)
|
|
1756
|
+
.order("desc")
|
|
1757
|
+
.take(1),
|
|
1438
1758
|
]);
|
|
1439
1759
|
return bounds(first[0]?.bucketStart, last[0]?.bucketStart);
|
|
1440
1760
|
}
|
|
@@ -1443,14 +1763,15 @@ function bounds(from: number | undefined, through: number | undefined) {
|
|
|
1443
1763
|
return from === undefined || through === undefined ? null : { from, through };
|
|
1444
1764
|
}
|
|
1445
1765
|
|
|
1446
|
-
function coverageRank(
|
|
1766
|
+
function coverageRank(
|
|
1767
|
+
state: "complete" | "partial" | "retained" | "unavailable",
|
|
1768
|
+
) {
|
|
1447
1769
|
if (state === "complete") return 3;
|
|
1448
1770
|
if (state === "retained") return 2;
|
|
1449
1771
|
if (state === "partial") return 1;
|
|
1450
1772
|
return 0;
|
|
1451
1773
|
}
|
|
1452
1774
|
|
|
1453
|
-
|
|
1454
1775
|
type MetricTotals = {
|
|
1455
1776
|
events: number;
|
|
1456
1777
|
pageviews: number;
|
|
@@ -1491,8 +1812,15 @@ function addMetrics(target: MetricTotals, source: MetricTotals) {
|
|
|
1491
1812
|
|
|
1492
1813
|
function validateSiteIds<T>(siteIds: T[]): T[] {
|
|
1493
1814
|
const unique = [...new Set(siteIds)];
|
|
1494
|
-
if (
|
|
1495
|
-
|
|
1815
|
+
if (
|
|
1816
|
+
unique.length < 1 ||
|
|
1817
|
+
unique.length > MAX_REPORT_SITES ||
|
|
1818
|
+
unique.length !== siteIds.length
|
|
1819
|
+
) {
|
|
1820
|
+
fail(
|
|
1821
|
+
"INVALID_ARGUMENT",
|
|
1822
|
+
`siteIds must contain 1-${MAX_REPORT_SITES} unique IDs`,
|
|
1823
|
+
);
|
|
1496
1824
|
}
|
|
1497
1825
|
return unique;
|
|
1498
1826
|
}
|
|
@@ -1667,22 +1995,23 @@ function keysetPaginationResult<T>(
|
|
|
1667
1995
|
) {
|
|
1668
1996
|
const page = rows.slice(0, pagination.numItems);
|
|
1669
1997
|
const isDone = rows.length <= pagination.numItems;
|
|
1670
|
-
const next =
|
|
1671
|
-
? cursorFor(page[page.length - 1])
|
|
1672
|
-
: pagination.cursor;
|
|
1998
|
+
const next =
|
|
1999
|
+
page.length > 0 ? cursorFor(page[page.length - 1]) : pagination.cursor;
|
|
1673
2000
|
return {
|
|
1674
2001
|
page,
|
|
1675
2002
|
isDone,
|
|
1676
|
-
continueCursor: btoa(
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2003
|
+
continueCursor: btoa(
|
|
2004
|
+
JSON.stringify({
|
|
2005
|
+
v: 2,
|
|
2006
|
+
k: pagination.scope.kind,
|
|
2007
|
+
s: pagination.scope.siteId,
|
|
2008
|
+
n: pagination.scope.sessionId ?? null,
|
|
2009
|
+
f: pagination.scope.from,
|
|
2010
|
+
t: pagination.scope.to,
|
|
2011
|
+
x: next?.timestamp ?? pagination.scope.to,
|
|
2012
|
+
c: next?.creationTime ?? Number.MAX_SAFE_INTEGER,
|
|
2013
|
+
}),
|
|
2014
|
+
),
|
|
1686
2015
|
};
|
|
1687
2016
|
}
|
|
1688
2017
|
|
|
@@ -1694,26 +2023,28 @@ function journeyPaginationResult(
|
|
|
1694
2023
|
const isDone = rows.length <= pagination.numItems;
|
|
1695
2024
|
const last = page.at(-1);
|
|
1696
2025
|
const next = last
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2026
|
+
? {
|
|
2027
|
+
timestamp: last.timestamp,
|
|
2028
|
+
creationTime: last._creationTime,
|
|
2029
|
+
id: last._id,
|
|
1701
2030
|
}
|
|
1702
2031
|
: pagination.cursor;
|
|
1703
2032
|
return {
|
|
1704
2033
|
page,
|
|
1705
2034
|
isDone,
|
|
1706
|
-
continueCursor: btoa(
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2035
|
+
continueCursor: btoa(
|
|
2036
|
+
JSON.stringify({
|
|
2037
|
+
v: 4,
|
|
2038
|
+
k: pagination.scope.kind,
|
|
2039
|
+
s: pagination.scope.siteId,
|
|
2040
|
+
n: pagination.scope.sessionId,
|
|
2041
|
+
f: pagination.scope.from,
|
|
2042
|
+
t: pagination.scope.to,
|
|
2043
|
+
x: next?.timestamp ?? pagination.scope.to,
|
|
2044
|
+
c: next?.creationTime ?? Number.MAX_SAFE_INTEGER,
|
|
2045
|
+
i: next?.id ?? "~",
|
|
2046
|
+
}),
|
|
2047
|
+
),
|
|
1717
2048
|
};
|
|
1718
2049
|
}
|
|
1719
2050
|
|
|
@@ -1722,8 +2053,15 @@ function validatePagination(options: {
|
|
|
1722
2053
|
maximumRowsRead?: number;
|
|
1723
2054
|
maximumBytesRead?: number;
|
|
1724
2055
|
}) {
|
|
1725
|
-
if (
|
|
1726
|
-
|
|
2056
|
+
if (
|
|
2057
|
+
!Number.isSafeInteger(options.numItems) ||
|
|
2058
|
+
options.numItems < 1 ||
|
|
2059
|
+
options.numItems > 100
|
|
2060
|
+
) {
|
|
2061
|
+
fail(
|
|
2062
|
+
"INVALID_ARGUMENT",
|
|
2063
|
+
"pagination numItems must be an integer between 1 and 100",
|
|
2064
|
+
);
|
|
1727
2065
|
}
|
|
1728
2066
|
if (
|
|
1729
2067
|
options.maximumRowsRead !== undefined &&
|
|
@@ -1731,7 +2069,10 @@ function validatePagination(options: {
|
|
|
1731
2069
|
options.maximumRowsRead < 1 ||
|
|
1732
2070
|
options.maximumRowsRead > 1_000)
|
|
1733
2071
|
) {
|
|
1734
|
-
fail(
|
|
2072
|
+
fail(
|
|
2073
|
+
"INVALID_ARGUMENT",
|
|
2074
|
+
"maximumRowsRead must be an integer between 1 and 1000",
|
|
2075
|
+
);
|
|
1735
2076
|
}
|
|
1736
2077
|
if (
|
|
1737
2078
|
options.maximumBytesRead !== undefined &&
|
|
@@ -1739,6 +2080,9 @@ function validatePagination(options: {
|
|
|
1739
2080
|
options.maximumBytesRead < 1 ||
|
|
1740
2081
|
options.maximumBytesRead > 8_000_000)
|
|
1741
2082
|
) {
|
|
1742
|
-
fail(
|
|
2083
|
+
fail(
|
|
2084
|
+
"INVALID_ARGUMENT",
|
|
2085
|
+
"maximumBytesRead must be an integer between 1 and 8000000",
|
|
2086
|
+
);
|
|
1743
2087
|
}
|
|
1744
2088
|
}
|