@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
|
@@ -1,13 +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
10
|
import { resolveVisitorIdentities } from "./visitors.js";
|
|
11
|
+
import { mergeVitalHistograms, VITAL_ALL, VITAL_METRICS, vitalHistogramPercentile, vitalRatingCounts, } from "./vitals.js";
|
|
11
12
|
const sessionDocumentValidator = sessionFieldsValidator.extend({
|
|
12
13
|
_id: v.id("sessions"),
|
|
13
14
|
_creationTime: v.number(),
|
|
@@ -67,7 +68,9 @@ const metricTotalsValidator = v.object({
|
|
|
67
68
|
conversions: v.number(),
|
|
68
69
|
revenueCents: v.number(),
|
|
69
70
|
});
|
|
70
|
-
const timePointValidator = metricTotalsValidator.extend({
|
|
71
|
+
const timePointValidator = metricTotalsValidator.extend({
|
|
72
|
+
timestamp: v.number(),
|
|
73
|
+
});
|
|
71
74
|
const topItemValidator = v.object({
|
|
72
75
|
value: v.string(),
|
|
73
76
|
count: v.number(),
|
|
@@ -292,7 +295,9 @@ export const liveVisitors = query({
|
|
|
292
295
|
handler: async (ctx, args) => {
|
|
293
296
|
const siteIds = validateSiteIds(args.siteIds);
|
|
294
297
|
const limit = args.limit ?? 100;
|
|
295
|
-
if (!Number.isSafeInteger(limit) ||
|
|
298
|
+
if (!Number.isSafeInteger(limit) ||
|
|
299
|
+
limit < 1 ||
|
|
300
|
+
limit > MAX_LIVE_VISITORS) {
|
|
296
301
|
fail("INVALID_ARGUMENT", `limit must be an integer between 1 and ${MAX_LIVE_VISITORS}`);
|
|
297
302
|
}
|
|
298
303
|
if (!Number.isFinite(args.now) || args.now < 0) {
|
|
@@ -488,22 +493,22 @@ export const visitorJourney = query({
|
|
|
488
493
|
validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
|
|
489
494
|
await assertSourceAvailable(ctx, siteIds, ["events"], args.from);
|
|
490
495
|
const limit = args.limit ?? 200;
|
|
491
|
-
if (!Number.isSafeInteger(limit) ||
|
|
496
|
+
if (!Number.isSafeInteger(limit) ||
|
|
497
|
+
limit < 1 ||
|
|
498
|
+
limit > MAX_JOURNEY_EVENTS) {
|
|
492
499
|
fail("INVALID_ARGUMENT", `limit must be an integer between 1 and ${MAX_JOURNEY_EVENTS}`);
|
|
493
500
|
}
|
|
494
501
|
const rows = [];
|
|
495
502
|
for (const siteId of siteIds) {
|
|
496
503
|
for (const visitorId of await resolveVisitorIdentities(ctx, siteId, args.visitorId)) {
|
|
497
|
-
const cutoff = rows.length >= limit
|
|
498
|
-
|
|
499
|
-
: args.to;
|
|
500
|
-
rows.push(...await loadVisitorJourneyEvents(ctx, {
|
|
504
|
+
const cutoff = rows.length >= limit ? rows[limit - 1].timestamp : args.to;
|
|
505
|
+
rows.push(...(await loadVisitorJourneyEvents(ctx, {
|
|
501
506
|
siteId,
|
|
502
507
|
visitorId,
|
|
503
508
|
from: args.from,
|
|
504
509
|
to: cutoff,
|
|
505
510
|
resultLimit: limit,
|
|
506
|
-
}));
|
|
511
|
+
})));
|
|
507
512
|
rows.sort(compareJourneyEvents);
|
|
508
513
|
if (rows.length > limit)
|
|
509
514
|
rows.length = limit;
|
|
@@ -586,7 +591,12 @@ export const goalsReport = query({
|
|
|
586
591
|
const funnelReportItemValidator = funnelFieldsValidator.extend({
|
|
587
592
|
_id: v.id("funnels"),
|
|
588
593
|
_creationTime: v.number(),
|
|
589
|
-
stepsReport: v.array(v.object({
|
|
594
|
+
stepsReport: v.array(v.object({
|
|
595
|
+
step: v.number(),
|
|
596
|
+
name: v.string(),
|
|
597
|
+
entrants: v.number(),
|
|
598
|
+
completions: v.number(),
|
|
599
|
+
})),
|
|
590
600
|
truncated: v.boolean(),
|
|
591
601
|
metadata: reportMetadataValidator,
|
|
592
602
|
});
|
|
@@ -649,7 +659,9 @@ export const funnelsReport = query({
|
|
|
649
659
|
stepsReport,
|
|
650
660
|
truncated: false,
|
|
651
661
|
metadata: {
|
|
652
|
-
basis: rollups
|
|
662
|
+
basis: rollups
|
|
663
|
+
? "dailyRollup"
|
|
664
|
+
: "boundedLegacy",
|
|
653
665
|
coverage: reportCoverage,
|
|
654
666
|
range: rangeSemantics(args.from, args.to),
|
|
655
667
|
},
|
|
@@ -775,7 +787,7 @@ export const affiliatesReport = query({
|
|
|
775
787
|
visits: traffic?.visits ?? 0,
|
|
776
788
|
uniqueVisitors: rollups
|
|
777
789
|
? estimateVisitors(traffic?.sketch ?? createVisitorSketch())
|
|
778
|
-
: traffic?.visitors.size ?? 0,
|
|
790
|
+
: (traffic?.visitors.size ?? 0),
|
|
779
791
|
};
|
|
780
792
|
}),
|
|
781
793
|
truncated: false,
|
|
@@ -790,14 +802,128 @@ export const affiliatesReport = query({
|
|
|
790
802
|
}),
|
|
791
803
|
range: rangeSemantics(args.from, args.to),
|
|
792
804
|
uniqueVisitors: {
|
|
793
|
-
basis: rollups
|
|
805
|
+
basis: rollups
|
|
806
|
+
? "estimatedUnique"
|
|
807
|
+
: "exactLegacy",
|
|
794
808
|
exact: !rollups,
|
|
795
809
|
},
|
|
796
810
|
},
|
|
797
811
|
};
|
|
798
812
|
},
|
|
799
813
|
});
|
|
800
|
-
const
|
|
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"));
|
|
801
927
|
export const dataCoverage = query({
|
|
802
928
|
args: {
|
|
803
929
|
siteId: v.id("sites"),
|
|
@@ -824,6 +950,7 @@ export const dataCoverage = query({
|
|
|
824
950
|
"goals",
|
|
825
951
|
"funnels",
|
|
826
952
|
"affiliates",
|
|
953
|
+
"vitals",
|
|
827
954
|
];
|
|
828
955
|
return {
|
|
829
956
|
siteId: args.siteId,
|
|
@@ -967,10 +1094,11 @@ async function coverageForRange(ctx, siteIds, from, to, options) {
|
|
|
967
1094
|
else if (retainedBefore !== null && from < retainedBefore) {
|
|
968
1095
|
state = "partial";
|
|
969
1096
|
}
|
|
970
|
-
else if (options.complete ||
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1097
|
+
else if (options.complete ||
|
|
1098
|
+
(availableFrom !== null &&
|
|
1099
|
+
availableThrough !== null &&
|
|
1100
|
+
from >= availableFrom &&
|
|
1101
|
+
to <= availableThrough)) {
|
|
974
1102
|
state = "complete";
|
|
975
1103
|
}
|
|
976
1104
|
else if (sourceRanges.some(Boolean)) {
|
|
@@ -1007,6 +1135,14 @@ async function assertSourceAvailable(ctx, siteIds, sources, from) {
|
|
|
1007
1135
|
}
|
|
1008
1136
|
}
|
|
1009
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
|
+
}
|
|
1010
1146
|
const sourceOptions = dataset === "goals"
|
|
1011
1147
|
? [["goalRollups"], ["goalCompletions"]]
|
|
1012
1148
|
: dataset === "funnels"
|
|
@@ -1043,54 +1179,76 @@ async function sourceRange(ctx, siteId, source) {
|
|
|
1043
1179
|
if (source === "overviewHour" || source === "overviewDay") {
|
|
1044
1180
|
const granularity = source === "overviewHour" ? "hour" : "day";
|
|
1045
1181
|
const [first, last] = await Promise.all([
|
|
1046
|
-
ctx.db
|
|
1047
|
-
.
|
|
1048
|
-
|
|
1049
|
-
.
|
|
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),
|
|
1050
1192
|
]);
|
|
1051
1193
|
return bounds(first[0]?.bucketStart, last[0]?.bucketStart);
|
|
1052
1194
|
}
|
|
1053
1195
|
if (source === "events") {
|
|
1054
1196
|
const [first, last] = await Promise.all([
|
|
1055
|
-
ctx.db
|
|
1197
|
+
ctx.db
|
|
1198
|
+
.query("eventBatches")
|
|
1056
1199
|
.withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
|
|
1057
|
-
.order("asc")
|
|
1058
|
-
|
|
1200
|
+
.order("asc")
|
|
1201
|
+
.take(1),
|
|
1202
|
+
ctx.db
|
|
1203
|
+
.query("eventBatches")
|
|
1059
1204
|
.withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
|
|
1060
|
-
.order("desc")
|
|
1205
|
+
.order("desc")
|
|
1206
|
+
.take(1),
|
|
1061
1207
|
]);
|
|
1062
1208
|
return bounds(first[0]?.bucketStart, last[0]?.bucketStart);
|
|
1063
1209
|
}
|
|
1064
1210
|
if (source === "sessions") {
|
|
1065
1211
|
const [first, last] = await Promise.all([
|
|
1066
|
-
ctx.db
|
|
1212
|
+
ctx.db
|
|
1213
|
+
.query("sessions")
|
|
1067
1214
|
.withIndex("by_siteId_and_startedAt", (range) => range.eq("siteId", siteId))
|
|
1068
|
-
.order("asc")
|
|
1069
|
-
|
|
1215
|
+
.order("asc")
|
|
1216
|
+
.take(1),
|
|
1217
|
+
ctx.db
|
|
1218
|
+
.query("sessions")
|
|
1070
1219
|
.withIndex("by_siteId_and_lastSeenAt", (range) => range.eq("siteId", siteId))
|
|
1071
|
-
.order("desc")
|
|
1220
|
+
.order("desc")
|
|
1221
|
+
.take(1),
|
|
1072
1222
|
]);
|
|
1073
1223
|
return bounds(first[0]?.startedAt, last[0]?.lastSeenAt);
|
|
1074
1224
|
}
|
|
1075
1225
|
if (source === "conversions") {
|
|
1076
1226
|
const [first, last] = await Promise.all([
|
|
1077
|
-
ctx.db
|
|
1227
|
+
ctx.db
|
|
1228
|
+
.query("conversions")
|
|
1078
1229
|
.withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
|
|
1079
|
-
.order("asc")
|
|
1080
|
-
|
|
1230
|
+
.order("asc")
|
|
1231
|
+
.take(1),
|
|
1232
|
+
ctx.db
|
|
1233
|
+
.query("conversions")
|
|
1081
1234
|
.withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
|
|
1082
|
-
.order("desc")
|
|
1235
|
+
.order("desc")
|
|
1236
|
+
.take(1),
|
|
1083
1237
|
]);
|
|
1084
1238
|
return bounds(first[0]?.timestamp, last[0]?.timestamp);
|
|
1085
1239
|
}
|
|
1086
1240
|
if (source === "goalCompletions") {
|
|
1087
1241
|
const [first, last] = await Promise.all([
|
|
1088
|
-
ctx.db
|
|
1242
|
+
ctx.db
|
|
1243
|
+
.query("goalCompletions")
|
|
1089
1244
|
.withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
|
|
1090
|
-
.order("asc")
|
|
1091
|
-
|
|
1245
|
+
.order("asc")
|
|
1246
|
+
.take(1),
|
|
1247
|
+
ctx.db
|
|
1248
|
+
.query("goalCompletions")
|
|
1092
1249
|
.withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
|
|
1093
|
-
.order("desc")
|
|
1250
|
+
.order("desc")
|
|
1251
|
+
.take(1),
|
|
1094
1252
|
]);
|
|
1095
1253
|
return bounds(first[0]?.timestamp, last[0]?.timestamp);
|
|
1096
1254
|
}
|
|
@@ -1102,14 +1260,20 @@ async function sourceRange(ctx, siteId, source) {
|
|
|
1102
1260
|
? "goalDailyRollups"
|
|
1103
1261
|
: source === "funnelRollups"
|
|
1104
1262
|
? "funnelDailyRollups"
|
|
1105
|
-
: "
|
|
1263
|
+
: source === "vitalRollups"
|
|
1264
|
+
? "vitalRollups"
|
|
1265
|
+
: "affiliateDailyRollups";
|
|
1106
1266
|
const [first, last] = await Promise.all([
|
|
1107
|
-
ctx.db
|
|
1267
|
+
ctx.db
|
|
1268
|
+
.query(table)
|
|
1108
1269
|
.withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
|
|
1109
|
-
.order("asc")
|
|
1110
|
-
|
|
1270
|
+
.order("asc")
|
|
1271
|
+
.take(1),
|
|
1272
|
+
ctx.db
|
|
1273
|
+
.query(table)
|
|
1111
1274
|
.withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
|
|
1112
|
-
.order("desc")
|
|
1275
|
+
.order("desc")
|
|
1276
|
+
.take(1),
|
|
1113
1277
|
]);
|
|
1114
1278
|
return bounds(first[0]?.bucketStart, last[0]?.bucketStart);
|
|
1115
1279
|
}
|
|
@@ -1151,7 +1315,9 @@ function addMetrics(target, source) {
|
|
|
1151
1315
|
}
|
|
1152
1316
|
function validateSiteIds(siteIds) {
|
|
1153
1317
|
const unique = [...new Set(siteIds)];
|
|
1154
|
-
if (unique.length < 1 ||
|
|
1318
|
+
if (unique.length < 1 ||
|
|
1319
|
+
unique.length > MAX_REPORT_SITES ||
|
|
1320
|
+
unique.length !== siteIds.length) {
|
|
1155
1321
|
fail("INVALID_ARGUMENT", `siteIds must contain 1-${MAX_REPORT_SITES} unique IDs`);
|
|
1156
1322
|
}
|
|
1157
1323
|
return unique;
|
|
@@ -1262,9 +1428,7 @@ function parseJourneyPagination(options, scope) {
|
|
|
1262
1428
|
function keysetPaginationResult(rows, pagination, cursorFor) {
|
|
1263
1429
|
const page = rows.slice(0, pagination.numItems);
|
|
1264
1430
|
const isDone = rows.length <= pagination.numItems;
|
|
1265
|
-
const next = page.length > 0
|
|
1266
|
-
? cursorFor(page[page.length - 1])
|
|
1267
|
-
: pagination.cursor;
|
|
1431
|
+
const next = page.length > 0 ? cursorFor(page[page.length - 1]) : pagination.cursor;
|
|
1268
1432
|
return {
|
|
1269
1433
|
page,
|
|
1270
1434
|
isDone,
|
|
@@ -1308,7 +1472,9 @@ function journeyPaginationResult(rows, pagination) {
|
|
|
1308
1472
|
};
|
|
1309
1473
|
}
|
|
1310
1474
|
function validatePagination(options) {
|
|
1311
|
-
if (!Number.isSafeInteger(options.numItems) ||
|
|
1475
|
+
if (!Number.isSafeInteger(options.numItems) ||
|
|
1476
|
+
options.numItems < 1 ||
|
|
1477
|
+
options.numItems > 100) {
|
|
1312
1478
|
fail("INVALID_ARGUMENT", "pagination numItems must be an integer between 1 and 100");
|
|
1313
1479
|
}
|
|
1314
1480
|
if (options.maximumRowsRead !== undefined &&
|