@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.
- package/README.md +203 -56
- package/agent/integration.md +17 -15
- package/agent/manifest.json +6 -4
- 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 +486 -31
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +209 -41
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +8 -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 +82 -8
- 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 +6 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +12 -0
- 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 +3 -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 +71 -37
- package/dist/component/http.js.map +1 -1
- package/dist/component/identity.d.ts +13 -0
- package/dist/component/identity.d.ts.map +1 -0
- package/dist/component/identity.js +58 -0
- package/dist/component/identity.js.map +1 -0
- package/dist/component/ingest.d.ts +3 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +168 -29
- 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 +70 -4
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +232 -59
- 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.map +1 -1
- package/dist/component/sanitize.js +42 -11
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +83 -10
- package/dist/component/schema.js +34 -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/useragent.d.ts +9 -0
- package/dist/component/useragent.d.ts.map +1 -0
- package/dist/component/useragent.js +152 -0
- package/dist/component/useragent.js.map +1 -0
- package/dist/component/validators.d.ts +29 -18
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +8 -2
- package/dist/component/validators.js.map +1 -1
- package/dist/component/visitors.d.ts +19 -0
- package/dist/component/visitors.d.ts.map +1 -0
- package/dist/component/visitors.js +86 -0
- package/dist/component/visitors.js.map +1 -0
- 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 +23 -11
- 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 +137 -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/federation.md +1 -1
- package/package.json +27 -9
- package/scripts/benchmark-ingest.mjs +101 -48
- package/src/component/_generated/api.ts +8 -0
- package/src/component/_generated/component.ts +87 -5
- package/src/component/affiliates.ts +20 -5
- package/src/component/cardinality.ts +8 -7
- package/src/component/constants.ts +12 -0
- 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 +126 -78
- package/src/component/identity.ts +74 -0
- package/src/component/ingest.ts +349 -130
- package/src/component/live.ts +5 -4
- package/src/component/reports.ts +572 -221
- package/src/component/retention.ts +221 -98
- package/src/component/sanitize.ts +77 -27
- package/src/component/schema.ts +46 -5
- package/src/component/sites.ts +22 -11
- package/src/component/useragent.ts +171 -0
- package/src/component/validators.ts +19 -7
- package/src/component/visitors.ts +116 -0
- 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({
|
|
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("
|
|
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: "
|
|
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) ||
|
|
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) ||
|
|
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
|
|
492
|
-
? rows[limit - 1].timestamp
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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({
|
|
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
|
|
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
|
|
805
|
+
basis: rollups
|
|
806
|
+
? "estimatedUnique"
|
|
807
|
+
: "exactLegacy",
|
|
787
808
|
exact: !rollups,
|
|
788
809
|
},
|
|
789
810
|
},
|
|
790
811
|
};
|
|
791
812
|
},
|
|
792
813
|
});
|
|
793
|
-
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"));
|
|
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 ||
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
|
1040
|
-
.
|
|
1041
|
-
|
|
1042
|
-
.
|
|
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
|
|
1197
|
+
ctx.db
|
|
1198
|
+
.query("eventBatches")
|
|
1049
1199
|
.withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
|
|
1050
|
-
.order("asc")
|
|
1051
|
-
|
|
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")
|
|
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
|
|
1212
|
+
ctx.db
|
|
1213
|
+
.query("sessions")
|
|
1060
1214
|
.withIndex("by_siteId_and_startedAt", (range) => range.eq("siteId", siteId))
|
|
1061
|
-
.order("asc")
|
|
1062
|
-
|
|
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")
|
|
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
|
|
1227
|
+
ctx.db
|
|
1228
|
+
.query("conversions")
|
|
1071
1229
|
.withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
|
|
1072
|
-
.order("asc")
|
|
1073
|
-
|
|
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")
|
|
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
|
|
1242
|
+
ctx.db
|
|
1243
|
+
.query("goalCompletions")
|
|
1082
1244
|
.withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId))
|
|
1083
|
-
.order("asc")
|
|
1084
|
-
|
|
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")
|
|
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
|
-
: "
|
|
1263
|
+
: source === "vitalRollups"
|
|
1264
|
+
? "vitalRollups"
|
|
1265
|
+
: "affiliateDailyRollups";
|
|
1099
1266
|
const [first, last] = await Promise.all([
|
|
1100
|
-
ctx.db
|
|
1267
|
+
ctx.db
|
|
1268
|
+
.query(table)
|
|
1101
1269
|
.withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId))
|
|
1102
|
-
.order("asc")
|
|
1103
|
-
|
|
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")
|
|
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 ||
|
|
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) ||
|
|
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 &&
|