@iann29/rastro 0.1.0-alpha.9 → 0.1.0
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 +246 -57
- package/agent/integration.md +223 -65
- package/agent/manifest.json +34 -13
- package/agent/manifest.schema.json +53 -11
- package/dist/client/federation.d.ts +196 -11
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +160 -6
- package/dist/client/federation.js.map +1 -1
- package/dist/client/heat.d.ts +37 -0
- package/dist/client/heat.d.ts.map +1 -0
- package/dist/client/heat.js +43 -0
- package/dist/client/heat.js.map +1 -0
- package/dist/client/index.d.ts +441 -98
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +207 -17
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +6 -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 +50 -13
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +3 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +6 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +9 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +36 -6
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/geo.d.ts +2 -0
- package/dist/component/geo.d.ts.map +1 -1
- package/dist/component/geo.js +56 -19
- package/dist/component/geo.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +48 -27
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +223 -304
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/localTime.d.ts +25 -0
- package/dist/component/localTime.d.ts.map +1 -0
- package/dist/component/localTime.js +126 -0
- package/dist/component/localTime.js.map +1 -0
- package/dist/component/reports.d.ts +44 -13
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +193 -38
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.js +26 -8
- package/dist/component/retention.js.map +1 -1
- package/dist/component/rollupStore.d.ts +320 -0
- package/dist/component/rollupStore.d.ts.map +1 -0
- package/dist/component/rollupStore.js +596 -0
- package/dist/component/rollupStore.js.map +1 -0
- package/dist/component/rollups.d.ts +20 -0
- package/dist/component/rollups.d.ts.map +1 -0
- package/dist/component/rollups.js +73 -0
- package/dist/component/rollups.js.map +1 -0
- package/dist/component/sanitize.d.ts +5 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +15 -0
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +345 -11
- package/dist/component/schema.js +25 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts +12 -0
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +30 -0
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +34 -13
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +23 -2
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +3 -0
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +3 -0
- package/dist/tracker/generated.js.map +1 -1
- package/docs/federation-setup.md +104 -27
- package/docs/federation.md +168 -31
- package/docs/upgrading.md +158 -21
- package/llms.txt +8 -6
- package/package.json +2 -1
- package/src/component/_generated/api.ts +6 -0
- package/src/component/_generated/component.ts +47 -12
- package/src/component/constants.ts +6 -0
- package/src/component/coverage.ts +46 -6
- package/src/component/geo.ts +82 -28
- package/src/component/http.ts +61 -26
- package/src/component/ingest.ts +327 -439
- package/src/component/localTime.ts +167 -0
- package/src/component/reports.ts +261 -39
- package/src/component/retention.ts +25 -7
- package/src/component/rollupStore.ts +799 -0
- package/src/component/rollups.ts +82 -0
- package/src/component/sanitize.ts +14 -0
- package/src/component/schema.ts +35 -0
- package/src/component/sites.ts +32 -0
- package/src/component/validators.ts +27 -1
- package/src/tracker/generated.ts +12 -0
|
@@ -21,6 +21,9 @@ export declare const overview: import("convex/server").RegisteredQuery<"public",
|
|
|
21
21
|
visitors: number;
|
|
22
22
|
conversions: number;
|
|
23
23
|
revenueCents: number;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
continuedSessions: number;
|
|
26
|
+
newVisitors: number;
|
|
24
27
|
}[];
|
|
25
28
|
topSources: {
|
|
26
29
|
value: string;
|
|
@@ -62,6 +65,16 @@ export declare const overview: import("convex/server").RegisteredQuery<"public",
|
|
|
62
65
|
count: number;
|
|
63
66
|
revenueCents: number;
|
|
64
67
|
}[];
|
|
68
|
+
topMediums: {
|
|
69
|
+
value: string;
|
|
70
|
+
count: number;
|
|
71
|
+
revenueCents: number;
|
|
72
|
+
}[];
|
|
73
|
+
topOutbound: {
|
|
74
|
+
value: string;
|
|
75
|
+
count: number;
|
|
76
|
+
revenueCents: number;
|
|
77
|
+
}[];
|
|
65
78
|
metadata: {
|
|
66
79
|
visitors: {
|
|
67
80
|
basis: "bucketOccurrences" | "estimatedUnique";
|
|
@@ -71,15 +84,19 @@ export declare const overview: import("convex/server").RegisteredQuery<"public",
|
|
|
71
84
|
precision: number | null;
|
|
72
85
|
};
|
|
73
86
|
breakdowns: {
|
|
74
|
-
topSources: "eventVolume";
|
|
75
|
-
topCampaigns: "eventVolume";
|
|
76
|
-
topPages: "eventVolume";
|
|
77
|
-
topCountries: "eventVolume";
|
|
78
|
-
topDevices: "eventVolume";
|
|
79
|
-
topBrowsers: "eventVolume";
|
|
87
|
+
topSources: "eventVolume" | "sessions";
|
|
88
|
+
topCampaigns: "eventVolume" | "sessions";
|
|
89
|
+
topPages: "eventVolume" | "pageviews";
|
|
90
|
+
topCountries: "eventVolume" | "sessions";
|
|
91
|
+
topDevices: "eventVolume" | "sessions";
|
|
92
|
+
topBrowsers: "eventVolume" | "sessions";
|
|
80
93
|
topEvents: "eventVolume";
|
|
81
94
|
topGoals: "goalCompletionVolume";
|
|
95
|
+
topMediums: "eventVolume" | "sessions";
|
|
96
|
+
topOutbound: "eventVolume";
|
|
82
97
|
};
|
|
98
|
+
dimensionsSince: number | null;
|
|
99
|
+
engagement: "counted" | "unavailable";
|
|
83
100
|
coverage: {
|
|
84
101
|
state: "complete" | "partial" | "retained" | "unavailable";
|
|
85
102
|
generation: 1 | null;
|
|
@@ -91,7 +108,7 @@ export declare const overview: import("convex/server").RegisteredQuery<"public",
|
|
|
91
108
|
from: number;
|
|
92
109
|
to: number;
|
|
93
110
|
boundary: "inclusive";
|
|
94
|
-
timezone:
|
|
111
|
+
timezone: string;
|
|
95
112
|
};
|
|
96
113
|
features: {
|
|
97
114
|
sessionReplay: "unsupported";
|
|
@@ -127,6 +144,16 @@ export declare const liveVisitors: import("convex/server").RegisteredQuery<"publ
|
|
|
127
144
|
eventCount: number;
|
|
128
145
|
previousPath?: string | undefined;
|
|
129
146
|
movedAt?: number | undefined;
|
|
147
|
+
entryPath?: string | undefined;
|
|
148
|
+
returning?: boolean | undefined;
|
|
149
|
+
utmCampaign?: string | undefined;
|
|
150
|
+
affiliateSlug?: string | undefined;
|
|
151
|
+
conversionCount?: number | undefined;
|
|
152
|
+
funnel?: {
|
|
153
|
+
funnelId: import("convex/values").GenericId<"funnels">;
|
|
154
|
+
step: number;
|
|
155
|
+
steps: number;
|
|
156
|
+
} | undefined;
|
|
130
157
|
}[]>>;
|
|
131
158
|
/**
|
|
132
159
|
* Every route the sites served in the range and the transitions between
|
|
@@ -187,7 +214,7 @@ export declare const siteMap: import("convex/server").RegisteredQuery<"public",
|
|
|
187
214
|
from: number;
|
|
188
215
|
to: number;
|
|
189
216
|
boundary: "inclusive";
|
|
190
|
-
timezone:
|
|
217
|
+
timezone: string;
|
|
191
218
|
};
|
|
192
219
|
visitors: {
|
|
193
220
|
basis: "estimatedUnique";
|
|
@@ -233,6 +260,7 @@ export declare const listSessions: import("convex/server").RegisteredQuery<"publ
|
|
|
233
260
|
lastSeenAt: number;
|
|
234
261
|
latitude?: number | undefined;
|
|
235
262
|
longitude?: number | undefined;
|
|
263
|
+
newVisitor?: boolean | undefined;
|
|
236
264
|
os: string;
|
|
237
265
|
pageviewCount: number;
|
|
238
266
|
referrer?: string | undefined;
|
|
@@ -399,7 +427,7 @@ export declare const goalsReport: import("convex/server").RegisteredQuery<"publi
|
|
|
399
427
|
from: number;
|
|
400
428
|
to: number;
|
|
401
429
|
boundary: "inclusive";
|
|
402
|
-
timezone:
|
|
430
|
+
timezone: string;
|
|
403
431
|
};
|
|
404
432
|
};
|
|
405
433
|
}>>;
|
|
@@ -430,7 +458,7 @@ export declare const funnelsReport: import("convex/server").RegisteredQuery<"pub
|
|
|
430
458
|
from: number;
|
|
431
459
|
to: number;
|
|
432
460
|
boundary: "inclusive";
|
|
433
|
-
timezone:
|
|
461
|
+
timezone: string;
|
|
434
462
|
};
|
|
435
463
|
};
|
|
436
464
|
active: boolean;
|
|
@@ -482,7 +510,7 @@ export declare const affiliatesReport: import("convex/server").RegisteredQuery<"
|
|
|
482
510
|
from: number;
|
|
483
511
|
to: number;
|
|
484
512
|
boundary: "inclusive";
|
|
485
|
-
timezone:
|
|
513
|
+
timezone: string;
|
|
486
514
|
};
|
|
487
515
|
uniqueVisitors: {
|
|
488
516
|
basis: "estimatedUnique" | "exactLegacy";
|
|
@@ -541,7 +569,7 @@ export declare const vitalsReport: import("convex/server").RegisteredQuery<"publ
|
|
|
541
569
|
from: number;
|
|
542
570
|
to: number;
|
|
543
571
|
boundary: "inclusive";
|
|
544
|
-
timezone:
|
|
572
|
+
timezone: string;
|
|
545
573
|
};
|
|
546
574
|
};
|
|
547
575
|
}>>;
|
|
@@ -554,7 +582,7 @@ export declare const dataCoverage: import("convex/server").RegisteredQuery<"publ
|
|
|
554
582
|
from: number;
|
|
555
583
|
to: number;
|
|
556
584
|
datasets: {
|
|
557
|
-
dataset: "affiliates" | "conversions" | "events" | "funnels" | "goals" | "overviewDay" | "overviewHour" | "sessions" | "siteMap" | "vitals";
|
|
585
|
+
dataset: "affiliates" | "conversions" | "events" | "funnels" | "goals" | "overviewDay" | "overviewHour" | "overviewLocalDay" | "sessions" | "siteMap" | "vitals";
|
|
558
586
|
coverage: {
|
|
559
587
|
state: "complete" | "partial" | "retained" | "unavailable";
|
|
560
588
|
generation: 1 | null;
|
|
@@ -574,6 +602,9 @@ type MetricTotals = {
|
|
|
574
602
|
visitors: number;
|
|
575
603
|
conversions: number;
|
|
576
604
|
revenueCents: number;
|
|
605
|
+
durationMs: number;
|
|
606
|
+
continuedSessions: number;
|
|
607
|
+
newVisitors: number;
|
|
577
608
|
};
|
|
578
609
|
export {};
|
|
579
610
|
//# sourceMappingURL=reports.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reports.d.ts","sourceRoot":"","sources":["../../src/component/reports.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAO,EAAE,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"reports.d.ts","sourceRoot":"","sources":["../../src/component/reports.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ import { query } from "./_generated/server.js";
|
|
|
4
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_SITE_MAP_ROUTES_PER_DAY, MAX_SITE_MAP_ROWS, MAX_VITALS_REPORT_ROWS, REPORT_ROLLUP_SHARDS, ROUTE_TRANSITION_SLOTS, SITE_MAP_OTHER_ROUTE, VITALS_REPORT_PAGE_LIMIT, } from "./constants.js";
|
|
5
5
|
import { createVisitorSketch, estimateVisitors, unionVisitorSketches, } from "./cardinality.js";
|
|
6
6
|
import { fail } from "./errors.js";
|
|
7
|
+
import { isLocalDayAligned, localDayStart, localDayStarts, localDayTimezone, sharedLocalTimezone, } from "./localTime.js";
|
|
7
8
|
import { compareJourneyEvents, hydrateEventContexts, loadSessionJourneyEvents, loadVisitorJourneyEvents, } from "./eventStore.js";
|
|
8
9
|
import { isPlainRecord } from "./guards.js";
|
|
9
10
|
import { affiliateFieldsValidator, eventFieldsValidator, funnelFieldsValidator, goalFieldsValidator, sessionFieldsValidator, vitalMetricValidator, } from "./validators.js";
|
|
@@ -39,6 +40,16 @@ const liveSessionDocumentValidator = v.object({
|
|
|
39
40
|
eventCount: v.number(),
|
|
40
41
|
previousPath: v.optional(v.string()),
|
|
41
42
|
movedAt: v.optional(v.number()),
|
|
43
|
+
entryPath: v.optional(v.string()),
|
|
44
|
+
returning: v.optional(v.boolean()),
|
|
45
|
+
utmCampaign: v.optional(v.string()),
|
|
46
|
+
affiliateSlug: v.optional(v.string()),
|
|
47
|
+
conversionCount: v.optional(v.number()),
|
|
48
|
+
funnel: v.optional(v.object({
|
|
49
|
+
funnelId: v.id("funnels"),
|
|
50
|
+
step: v.number(),
|
|
51
|
+
steps: v.number(),
|
|
52
|
+
})),
|
|
42
53
|
});
|
|
43
54
|
const conversionDocumentValidator = v.object({
|
|
44
55
|
_id: v.id("conversions"),
|
|
@@ -69,6 +80,12 @@ const metricTotalsValidator = v.object({
|
|
|
69
80
|
visitors: v.number(),
|
|
70
81
|
conversions: v.number(),
|
|
71
82
|
revenueCents: v.number(),
|
|
83
|
+
// Engagement: summed session duration, sessions that reached a second
|
|
84
|
+
// pageview, and first-ever sessions of a visitor. Zero for buckets older
|
|
85
|
+
// than the counters; metadata.engagement says whether the range is covered.
|
|
86
|
+
durationMs: v.number(),
|
|
87
|
+
continuedSessions: v.number(),
|
|
88
|
+
newVisitors: v.number(),
|
|
72
89
|
});
|
|
73
90
|
const timePointValidator = metricTotalsValidator.extend({
|
|
74
91
|
timestamp: v.number(),
|
|
@@ -78,6 +95,10 @@ const topItemValidator = v.object({
|
|
|
78
95
|
count: v.number(),
|
|
79
96
|
revenueCents: v.number(),
|
|
80
97
|
});
|
|
98
|
+
// What a breakdown's `count` is: pageviews for pages and sessions for the
|
|
99
|
+
// other dimensions once every bucket in the range carries those counters,
|
|
100
|
+
// raw event volume for ranges that reach back before they existed.
|
|
101
|
+
const breakdownBasisValidator = v.union(v.literal("pageviews"), v.literal("sessions"), v.literal("eventVolume"));
|
|
81
102
|
const coverageStateValidator = v.union(v.literal("complete"), v.literal("partial"), v.literal("retained"), v.literal("unavailable"));
|
|
82
103
|
const resultCoverageValidator = v.object({
|
|
83
104
|
state: coverageStateValidator,
|
|
@@ -90,7 +111,8 @@ const rangeSemanticsValidator = v.object({
|
|
|
90
111
|
from: v.number(),
|
|
91
112
|
to: v.number(),
|
|
92
113
|
boundary: v.literal("inclusive"),
|
|
93
|
-
|
|
114
|
+
// "UTC", or the site timezone whose calendar days a daily overview used.
|
|
115
|
+
timezone: v.string(),
|
|
94
116
|
});
|
|
95
117
|
const reportMetadataValidator = v.object({
|
|
96
118
|
basis: v.union(v.literal("dailyRollup"), v.literal("boundedLegacy")),
|
|
@@ -119,6 +141,10 @@ export const overview = query({
|
|
|
119
141
|
topBrowsers: v.array(topItemValidator),
|
|
120
142
|
topEvents: v.array(topItemValidator),
|
|
121
143
|
topGoals: v.array(topItemValidator),
|
|
144
|
+
// The session's lowercase utm_medium, ranked like campaigns.
|
|
145
|
+
topMediums: v.array(topItemValidator),
|
|
146
|
+
// Destination hosts of outbound clicks, ranked by clicks.
|
|
147
|
+
topOutbound: v.array(topItemValidator),
|
|
122
148
|
metadata: v.object({
|
|
123
149
|
visitors: v.object({
|
|
124
150
|
basis: v.union(v.literal("estimatedUnique"), v.literal("bucketOccurrences")),
|
|
@@ -128,15 +154,25 @@ export const overview = query({
|
|
|
128
154
|
precision: v.union(v.number(), v.null()),
|
|
129
155
|
}),
|
|
130
156
|
breakdowns: v.object({
|
|
131
|
-
topSources:
|
|
132
|
-
topCampaigns:
|
|
133
|
-
topPages:
|
|
134
|
-
topCountries:
|
|
135
|
-
topDevices:
|
|
136
|
-
topBrowsers:
|
|
157
|
+
topSources: breakdownBasisValidator,
|
|
158
|
+
topCampaigns: breakdownBasisValidator,
|
|
159
|
+
topPages: breakdownBasisValidator,
|
|
160
|
+
topCountries: breakdownBasisValidator,
|
|
161
|
+
topDevices: breakdownBasisValidator,
|
|
162
|
+
topBrowsers: breakdownBasisValidator,
|
|
137
163
|
topEvents: v.literal("eventVolume"),
|
|
138
164
|
topGoals: v.literal("goalCompletionVolume"),
|
|
165
|
+
topMediums: breakdownBasisValidator,
|
|
166
|
+
topOutbound: v.literal("eventVolume"),
|
|
139
167
|
}),
|
|
168
|
+
// Since when every site in the request records the medium and outbound
|
|
169
|
+
// slots; null while one of them has not stamped it. A range that starts
|
|
170
|
+
// earlier ranks those two breakdowns from partial data.
|
|
171
|
+
dimensionsSince: v.union(v.number(), v.null()),
|
|
172
|
+
// "counted" when every bucket in the range carries the engagement
|
|
173
|
+
// counters, so bounce rate, duration, pages per session and new
|
|
174
|
+
// visitors can be derived from the totals; "unavailable" otherwise.
|
|
175
|
+
engagement: v.union(v.literal("counted"), v.literal("unavailable")),
|
|
140
176
|
coverage: resultCoverageValidator,
|
|
141
177
|
range: rangeSemanticsValidator,
|
|
142
178
|
features: v.object({
|
|
@@ -150,11 +186,13 @@ export const overview = query({
|
|
|
150
186
|
handler: async (ctx, args) => {
|
|
151
187
|
const siteIds = validateSiteIds(args.siteIds);
|
|
152
188
|
const currencies = new Set();
|
|
189
|
+
const siteTimezones = [];
|
|
153
190
|
for (const siteId of siteIds) {
|
|
154
191
|
const site = await ctx.db.get("sites", siteId);
|
|
155
192
|
if (!site)
|
|
156
193
|
fail("NOT_FOUND", "site not found", { siteId: String(siteId) });
|
|
157
194
|
currencies.add(site.currency ?? "USD");
|
|
195
|
+
siteTimezones.push(localDayTimezone(site.timezone));
|
|
158
196
|
}
|
|
159
197
|
if (currencies.size !== 1) {
|
|
160
198
|
fail("CURRENCY_MISMATCH", "overview sites must use the same currency", {
|
|
@@ -168,10 +206,47 @@ export const overview = query({
|
|
|
168
206
|
fail("INVALID_ARGUMENT", "hourly reports are limited to 24 hours");
|
|
169
207
|
}
|
|
170
208
|
const intervalMs = interval === "hour" ? HOUR_MS : DAY_MS;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
209
|
+
// Sites that share a timezone answer a daily range that covers whole
|
|
210
|
+
// calendar days in that zone from their local-day buckets; any other
|
|
211
|
+
// daily range must cover whole UTC days, as before.
|
|
212
|
+
const siteTimezone = interval === "day" ? sharedLocalTimezone(siteTimezones) : undefined;
|
|
213
|
+
const localDays = siteTimezone !== undefined && isLocalDayAligned(from, to, siteTimezone);
|
|
214
|
+
if (!localDays) {
|
|
215
|
+
requireAlignedRange(from, to, intervalMs, `${interval} overview`, siteTimezone);
|
|
216
|
+
}
|
|
217
|
+
const granularity = localDays
|
|
218
|
+
? "localDay"
|
|
219
|
+
: interval;
|
|
220
|
+
const controls = await Promise.all(siteIds.map((siteId) => ctx.db
|
|
221
|
+
.query("analyticsCoverage")
|
|
222
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
223
|
+
.unique()));
|
|
224
|
+
// Local-day buckets are complete from each site's stamp (its first ingest
|
|
225
|
+
// after the upgrade or after a timezone change, plus the safety delay).
|
|
226
|
+
// Buckets before it belong to another zone's days or to no complete day
|
|
227
|
+
// and are left out; the coverage says since when the days are whole.
|
|
228
|
+
let localDaysFrom = Number.POSITIVE_INFINITY;
|
|
229
|
+
if (localDays) {
|
|
230
|
+
localDaysFrom = controls.reduce((latest, control) => control?.localDaySince === undefined
|
|
231
|
+
? Number.POSITIVE_INFINITY
|
|
232
|
+
: Math.max(latest, control.localDaySince), 0);
|
|
233
|
+
}
|
|
234
|
+
const bucketStarts = localDays
|
|
235
|
+
? localDayStarts(from, to, siteTimezone)
|
|
236
|
+
: [];
|
|
237
|
+
const bucketStartSet = new Set(bucketStarts);
|
|
238
|
+
const localDaysFirstBucket = localDays && Number.isFinite(localDaysFrom)
|
|
239
|
+
? localDayStart(localDaysFrom, siteTimezone)
|
|
240
|
+
: Number.POSITIVE_INFINITY;
|
|
241
|
+
const firstBucket = localDays
|
|
242
|
+
? bucketStarts[0]
|
|
243
|
+
: Math.floor(from / intervalMs) * intervalMs;
|
|
244
|
+
const lastBucket = localDays
|
|
245
|
+
? bucketStarts[bucketStarts.length - 1]
|
|
246
|
+
: Math.floor(to / intervalMs) * intervalMs;
|
|
247
|
+
const bucketCount = localDays
|
|
248
|
+
? bucketStarts.length
|
|
249
|
+
: Math.floor((lastBucket - firstBucket) / intervalMs) + 1;
|
|
175
250
|
const perSiteLimit = bucketCount *
|
|
176
251
|
(interval === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
177
252
|
if (perSiteLimit * siteIds.length > MAX_OVERVIEW_AGGREGATE_ROWS) {
|
|
@@ -184,21 +259,28 @@ export const overview = query({
|
|
|
184
259
|
const pointVisitorSketches = new Map();
|
|
185
260
|
let visitorsComplete = true;
|
|
186
261
|
const dimensions = new Map();
|
|
187
|
-
const overviewDataset =
|
|
262
|
+
const overviewDataset = granularity === "hour"
|
|
188
263
|
? "overviewHour"
|
|
189
|
-
: "
|
|
264
|
+
: granularity === "day"
|
|
265
|
+
? "overviewDay"
|
|
266
|
+
: "overviewLocalDay";
|
|
190
267
|
await assertSourceAvailable(ctx, siteIds, [overviewDataset], from);
|
|
191
268
|
for (const siteId of siteIds) {
|
|
192
269
|
const buckets = await ctx.db
|
|
193
270
|
.query("aggregateBuckets")
|
|
194
271
|
.withIndex("by_siteId_and_granularity_and_bucketStart", (range) => range
|
|
195
272
|
.eq("siteId", siteId)
|
|
196
|
-
.eq("granularity",
|
|
273
|
+
.eq("granularity", granularity)
|
|
197
274
|
.gte("bucketStart", firstBucket)
|
|
198
275
|
.lte("bucketStart", lastBucket))
|
|
199
276
|
.take(perSiteLimit + 1);
|
|
200
277
|
failIfIncomplete(buckets, perSiteLimit, "overview aggregate buckets");
|
|
201
278
|
for (const bucket of buckets) {
|
|
279
|
+
if (localDays &&
|
|
280
|
+
(!bucketStartSet.has(bucket.bucketStart) ||
|
|
281
|
+
bucket.bucketStart < localDaysFirstBucket)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
202
284
|
addMetrics(totals, bucket);
|
|
203
285
|
const point = points.get(bucket.bucketStart) ?? emptyTotals();
|
|
204
286
|
addMetrics(point, bucket);
|
|
@@ -217,16 +299,39 @@ export const overview = query({
|
|
|
217
299
|
value: dimension.value,
|
|
218
300
|
count: 0,
|
|
219
301
|
revenueCents: 0,
|
|
302
|
+
pageviews: 0,
|
|
303
|
+
sessions: 0,
|
|
220
304
|
};
|
|
221
305
|
current.count += dimension.count;
|
|
222
306
|
current.revenueCents += dimension.revenueCents;
|
|
307
|
+
current.pageviews += dimension.pageviews ?? 0;
|
|
308
|
+
current.sessions += dimension.sessions ?? 0;
|
|
223
309
|
dimensions.set(key, current);
|
|
224
310
|
}
|
|
225
311
|
}
|
|
226
312
|
}
|
|
227
|
-
|
|
313
|
+
// Pages rank by pageviews and the other dimensions by sessions once every
|
|
314
|
+
// site's counters are proven for the whole range; before that stamp a
|
|
315
|
+
// bucket may hold slots without them, so the ranking stays event volume.
|
|
316
|
+
const countersProven = controls.every((control) => control?.countersSince !== undefined && from >= control.countersSince);
|
|
317
|
+
const pageBasis = countersProven
|
|
318
|
+
? "pageviews"
|
|
319
|
+
: "eventVolume";
|
|
320
|
+
const sessionBasis = countersProven
|
|
321
|
+
? "sessions"
|
|
322
|
+
: "eventVolume";
|
|
323
|
+
const top = (type, basis) => [...dimensions.entries()]
|
|
228
324
|
.filter(([key]) => key.startsWith(`${type}:`))
|
|
229
|
-
.map(([,
|
|
325
|
+
.map(([, item]) => ({
|
|
326
|
+
value: item.value,
|
|
327
|
+
count: basis === "pageviews"
|
|
328
|
+
? item.pageviews
|
|
329
|
+
: basis === "sessions"
|
|
330
|
+
? item.sessions
|
|
331
|
+
: item.count,
|
|
332
|
+
revenueCents: item.revenueCents,
|
|
333
|
+
}))
|
|
334
|
+
.filter((item) => item.count > 0 || item.revenueCents > 0)
|
|
230
335
|
.sort((left, right) => right.count - left.count || left.value.localeCompare(right.value))
|
|
231
336
|
.slice(0, 10);
|
|
232
337
|
if (visitorsComplete) {
|
|
@@ -239,6 +344,17 @@ export const overview = query({
|
|
|
239
344
|
datasets: [overviewDataset],
|
|
240
345
|
complete: visitorsComplete,
|
|
241
346
|
});
|
|
347
|
+
if (localDays) {
|
|
348
|
+
// The stamp, not the oldest row, says where the site's days begin.
|
|
349
|
+
const availableFrom = Number.isFinite(localDaysFrom)
|
|
350
|
+
? Math.max(coverage.availableFrom ?? 0, localDaysFrom)
|
|
351
|
+
: null;
|
|
352
|
+
coverage.availableFrom = availableFrom;
|
|
353
|
+
if (coverage.state === "complete" &&
|
|
354
|
+
(availableFrom === null || from < availableFrom)) {
|
|
355
|
+
coverage.state = availableFrom === null ? "unavailable" : "partial";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
242
358
|
const visitorBasis = visitorsComplete ? "estimatedUnique" : "bucketOccurrences";
|
|
243
359
|
return {
|
|
244
360
|
from,
|
|
@@ -249,14 +365,16 @@ export const overview = query({
|
|
|
249
365
|
timeSeries: [...points.entries()]
|
|
250
366
|
.sort(([left], [right]) => left - right)
|
|
251
367
|
.map(([timestamp, metrics]) => ({ timestamp, ...metrics })),
|
|
252
|
-
topSources: top("source"),
|
|
253
|
-
topCampaigns: top("campaign"),
|
|
254
|
-
topPages: top("page"),
|
|
255
|
-
topCountries: top("country"),
|
|
256
|
-
topDevices: top("device"),
|
|
257
|
-
topBrowsers: top("browser"),
|
|
258
|
-
topEvents: top("event"),
|
|
259
|
-
topGoals: top("goal"),
|
|
368
|
+
topSources: top("source", sessionBasis),
|
|
369
|
+
topCampaigns: top("campaign", sessionBasis),
|
|
370
|
+
topPages: top("page", pageBasis),
|
|
371
|
+
topCountries: top("country", sessionBasis),
|
|
372
|
+
topDevices: top("device", sessionBasis),
|
|
373
|
+
topBrowsers: top("browser", sessionBasis),
|
|
374
|
+
topEvents: top("event", "eventVolume"),
|
|
375
|
+
topGoals: top("goal", "eventVolume"),
|
|
376
|
+
topMediums: top("medium", sessionBasis),
|
|
377
|
+
topOutbound: top("outbound", "eventVolume"),
|
|
260
378
|
metadata: {
|
|
261
379
|
visitors: {
|
|
262
380
|
basis: visitorBasis,
|
|
@@ -266,17 +384,25 @@ export const overview = query({
|
|
|
266
384
|
precision: visitorsComplete ? 6 : null,
|
|
267
385
|
},
|
|
268
386
|
breakdowns: {
|
|
269
|
-
topSources:
|
|
270
|
-
topCampaigns:
|
|
271
|
-
topPages:
|
|
272
|
-
topCountries:
|
|
273
|
-
topDevices:
|
|
274
|
-
topBrowsers:
|
|
387
|
+
topSources: sessionBasis,
|
|
388
|
+
topCampaigns: sessionBasis,
|
|
389
|
+
topPages: pageBasis,
|
|
390
|
+
topCountries: sessionBasis,
|
|
391
|
+
topDevices: sessionBasis,
|
|
392
|
+
topBrowsers: sessionBasis,
|
|
275
393
|
topEvents: "eventVolume",
|
|
276
394
|
topGoals: "goalCompletionVolume",
|
|
395
|
+
topMediums: sessionBasis,
|
|
396
|
+
topOutbound: "eventVolume",
|
|
277
397
|
},
|
|
398
|
+
dimensionsSince: controls.every((control) => control?.dimensionsSince !== undefined)
|
|
399
|
+
? Math.max(...controls.map((control) => control.dimensionsSince))
|
|
400
|
+
: null,
|
|
401
|
+
engagement: countersProven
|
|
402
|
+
? "counted"
|
|
403
|
+
: "unavailable",
|
|
278
404
|
coverage,
|
|
279
|
-
range: rangeSemantics(from, to),
|
|
405
|
+
range: rangeSemantics(from, to, localDays ? siteTimezone : "UTC"),
|
|
280
406
|
features: {
|
|
281
407
|
sessionReplay: "unsupported",
|
|
282
408
|
errorInsights: "unsupported",
|
|
@@ -338,6 +464,14 @@ export const liveVisitors = query({
|
|
|
338
464
|
eventCount: row.eventCount,
|
|
339
465
|
...(row.previousPath ? { previousPath: row.previousPath } : {}),
|
|
340
466
|
...(row.movedAt !== undefined ? { movedAt: row.movedAt } : {}),
|
|
467
|
+
...(row.entryPath ? { entryPath: row.entryPath } : {}),
|
|
468
|
+
...(row.returning !== undefined ? { returning: row.returning } : {}),
|
|
469
|
+
...(row.utmCampaign ? { utmCampaign: row.utmCampaign } : {}),
|
|
470
|
+
...(row.affiliateSlug ? { affiliateSlug: row.affiliateSlug } : {}),
|
|
471
|
+
...(row.conversionCount !== undefined
|
|
472
|
+
? { conversionCount: row.conversionCount }
|
|
473
|
+
: {}),
|
|
474
|
+
...(row.funnel ? { funnel: row.funnel } : {}),
|
|
341
475
|
}));
|
|
342
476
|
},
|
|
343
477
|
});
|
|
@@ -1157,7 +1291,7 @@ export const vitalsReport = query({
|
|
|
1157
1291
|
};
|
|
1158
1292
|
},
|
|
1159
1293
|
});
|
|
1160
|
-
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"), v.literal("siteMap"));
|
|
1294
|
+
const coverageDatasetValidator = v.union(v.literal("overviewHour"), v.literal("overviewDay"), v.literal("overviewLocalDay"), v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goals"), v.literal("funnels"), v.literal("affiliates"), v.literal("vitals"), v.literal("siteMap"));
|
|
1161
1295
|
export const dataCoverage = query({
|
|
1162
1296
|
args: {
|
|
1163
1297
|
siteId: v.id("sites"),
|
|
@@ -1178,6 +1312,7 @@ export const dataCoverage = query({
|
|
|
1178
1312
|
const datasets = [
|
|
1179
1313
|
"overviewHour",
|
|
1180
1314
|
"overviewDay",
|
|
1315
|
+
"overviewLocalDay",
|
|
1181
1316
|
"events",
|
|
1182
1317
|
"sessions",
|
|
1183
1318
|
"conversions",
|
|
@@ -1448,8 +1583,14 @@ async function sourceSetRange(ctx, siteId, sources) {
|
|
|
1448
1583
|
};
|
|
1449
1584
|
}
|
|
1450
1585
|
async function sourceRange(ctx, siteId, source) {
|
|
1451
|
-
if (source === "overviewHour" ||
|
|
1452
|
-
|
|
1586
|
+
if (source === "overviewHour" ||
|
|
1587
|
+
source === "overviewDay" ||
|
|
1588
|
+
source === "overviewLocalDay") {
|
|
1589
|
+
const granularity = source === "overviewHour"
|
|
1590
|
+
? "hour"
|
|
1591
|
+
: source === "overviewDay"
|
|
1592
|
+
? "day"
|
|
1593
|
+
: "localDay";
|
|
1453
1594
|
const [first, last] = await Promise.all([
|
|
1454
1595
|
ctx.db
|
|
1455
1596
|
.query("aggregateBuckets")
|
|
@@ -1572,6 +1713,9 @@ function emptyTotals() {
|
|
|
1572
1713
|
visitors: 0,
|
|
1573
1714
|
conversions: 0,
|
|
1574
1715
|
revenueCents: 0,
|
|
1716
|
+
durationMs: 0,
|
|
1717
|
+
continuedSessions: 0,
|
|
1718
|
+
newVisitors: 0,
|
|
1575
1719
|
};
|
|
1576
1720
|
}
|
|
1577
1721
|
function addMetrics(target, source) {
|
|
@@ -1584,6 +1728,9 @@ function addMetrics(target, source) {
|
|
|
1584
1728
|
target.visitors += source.visitors;
|
|
1585
1729
|
target.conversions += source.conversions;
|
|
1586
1730
|
target.revenueCents += source.revenueCents;
|
|
1731
|
+
target.durationMs += source.durationMs ?? 0;
|
|
1732
|
+
target.continuedSessions += source.continuedSessions ?? 0;
|
|
1733
|
+
target.newVisitors += source.newVisitors ?? 0;
|
|
1587
1734
|
}
|
|
1588
1735
|
function validateSiteIds(siteIds) {
|
|
1589
1736
|
const unique = [...new Set(siteIds)];
|
|
@@ -1607,17 +1754,25 @@ function validateRange(from, to, maxDays) {
|
|
|
1607
1754
|
function isAlignedRange(from, to, intervalMs) {
|
|
1608
1755
|
return from % intervalMs === 0 && (to + 1) % intervalMs === 0;
|
|
1609
1756
|
}
|
|
1610
|
-
function requireAlignedRange(from, to, intervalMs, report) {
|
|
1757
|
+
function requireAlignedRange(from, to, intervalMs, report, siteTimezone) {
|
|
1611
1758
|
if (!isAlignedRange(from, to, intervalMs)) {
|
|
1612
|
-
fail("REPORT_INCOMPLETE",
|
|
1759
|
+
fail("REPORT_INCOMPLETE", siteTimezone
|
|
1760
|
+
? `${report} requires complete inclusive UTC bucket boundaries, or whole calendar days in ${siteTimezone}`
|
|
1761
|
+
: `${report} requires complete inclusive UTC bucket boundaries`, {
|
|
1762
|
+
from,
|
|
1763
|
+
to,
|
|
1764
|
+
intervalMs,
|
|
1765
|
+
timezone: "UTC",
|
|
1766
|
+
siteTimezone: siteTimezone ?? null,
|
|
1767
|
+
});
|
|
1613
1768
|
}
|
|
1614
1769
|
}
|
|
1615
|
-
function rangeSemantics(from, to) {
|
|
1770
|
+
function rangeSemantics(from, to, timezone = "UTC") {
|
|
1616
1771
|
return {
|
|
1617
1772
|
from,
|
|
1618
1773
|
to,
|
|
1619
1774
|
boundary: "inclusive",
|
|
1620
|
-
timezone
|
|
1775
|
+
timezone,
|
|
1621
1776
|
};
|
|
1622
1777
|
}
|
|
1623
1778
|
function parseKeysetPagination(options, scope) {
|