@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.
Files changed (98) hide show
  1. package/README.md +246 -57
  2. package/agent/integration.md +223 -65
  3. package/agent/manifest.json +34 -13
  4. package/agent/manifest.schema.json +53 -11
  5. package/dist/client/federation.d.ts +196 -11
  6. package/dist/client/federation.d.ts.map +1 -1
  7. package/dist/client/federation.js +160 -6
  8. package/dist/client/federation.js.map +1 -1
  9. package/dist/client/heat.d.ts +37 -0
  10. package/dist/client/heat.d.ts.map +1 -0
  11. package/dist/client/heat.js +43 -0
  12. package/dist/client/heat.js.map +1 -0
  13. package/dist/client/index.d.ts +441 -98
  14. package/dist/client/index.d.ts.map +1 -1
  15. package/dist/client/index.js +207 -17
  16. package/dist/client/index.js.map +1 -1
  17. package/dist/component/_generated/api.d.ts +6 -0
  18. package/dist/component/_generated/api.d.ts.map +1 -1
  19. package/dist/component/_generated/api.js.map +1 -1
  20. package/dist/component/_generated/component.d.ts +50 -13
  21. package/dist/component/_generated/component.d.ts.map +1 -1
  22. package/dist/component/constants.d.ts +3 -0
  23. package/dist/component/constants.d.ts.map +1 -1
  24. package/dist/component/constants.js +6 -0
  25. package/dist/component/constants.js.map +1 -1
  26. package/dist/component/coverage.d.ts +9 -0
  27. package/dist/component/coverage.d.ts.map +1 -1
  28. package/dist/component/coverage.js +36 -6
  29. package/dist/component/coverage.js.map +1 -1
  30. package/dist/component/geo.d.ts +2 -0
  31. package/dist/component/geo.d.ts.map +1 -1
  32. package/dist/component/geo.js +56 -19
  33. package/dist/component/geo.js.map +1 -1
  34. package/dist/component/http.d.ts.map +1 -1
  35. package/dist/component/http.js +48 -27
  36. package/dist/component/http.js.map +1 -1
  37. package/dist/component/ingest.d.ts.map +1 -1
  38. package/dist/component/ingest.js +223 -304
  39. package/dist/component/ingest.js.map +1 -1
  40. package/dist/component/localTime.d.ts +25 -0
  41. package/dist/component/localTime.d.ts.map +1 -0
  42. package/dist/component/localTime.js +126 -0
  43. package/dist/component/localTime.js.map +1 -0
  44. package/dist/component/reports.d.ts +44 -13
  45. package/dist/component/reports.d.ts.map +1 -1
  46. package/dist/component/reports.js +193 -38
  47. package/dist/component/reports.js.map +1 -1
  48. package/dist/component/retention.js +26 -8
  49. package/dist/component/retention.js.map +1 -1
  50. package/dist/component/rollupStore.d.ts +320 -0
  51. package/dist/component/rollupStore.d.ts.map +1 -0
  52. package/dist/component/rollupStore.js +596 -0
  53. package/dist/component/rollupStore.js.map +1 -0
  54. package/dist/component/rollups.d.ts +20 -0
  55. package/dist/component/rollups.d.ts.map +1 -0
  56. package/dist/component/rollups.js +73 -0
  57. package/dist/component/rollups.js.map +1 -0
  58. package/dist/component/sanitize.d.ts +5 -0
  59. package/dist/component/sanitize.d.ts.map +1 -1
  60. package/dist/component/sanitize.js +15 -0
  61. package/dist/component/sanitize.js.map +1 -1
  62. package/dist/component/schema.d.ts +345 -11
  63. package/dist/component/schema.js +25 -0
  64. package/dist/component/schema.js.map +1 -1
  65. package/dist/component/sites.d.ts +12 -0
  66. package/dist/component/sites.d.ts.map +1 -1
  67. package/dist/component/sites.js +30 -0
  68. package/dist/component/sites.js.map +1 -1
  69. package/dist/component/validators.d.ts +34 -13
  70. package/dist/component/validators.d.ts.map +1 -1
  71. package/dist/component/validators.js +23 -2
  72. package/dist/component/validators.js.map +1 -1
  73. package/dist/tracker/generated.d.ts +3 -0
  74. package/dist/tracker/generated.d.ts.map +1 -1
  75. package/dist/tracker/generated.js +3 -0
  76. package/dist/tracker/generated.js.map +1 -1
  77. package/docs/federation-setup.md +104 -27
  78. package/docs/federation.md +168 -31
  79. package/docs/upgrading.md +158 -21
  80. package/llms.txt +8 -6
  81. package/package.json +2 -1
  82. package/src/component/_generated/api.ts +6 -0
  83. package/src/component/_generated/component.ts +47 -12
  84. package/src/component/constants.ts +6 -0
  85. package/src/component/coverage.ts +46 -6
  86. package/src/component/geo.ts +82 -28
  87. package/src/component/http.ts +61 -26
  88. package/src/component/ingest.ts +327 -439
  89. package/src/component/localTime.ts +167 -0
  90. package/src/component/reports.ts +261 -39
  91. package/src/component/retention.ts +25 -7
  92. package/src/component/rollupStore.ts +799 -0
  93. package/src/component/rollups.ts +82 -0
  94. package/src/component/sanitize.ts +14 -0
  95. package/src/component/schema.ts +35 -0
  96. package/src/component/sites.ts +32 -0
  97. package/src/component/validators.ts +27 -1
  98. 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: "UTC";
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: "UTC";
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: "UTC";
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: "UTC";
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: "UTC";
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: "UTC";
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":"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"}
1
+ {"version":3,"file":"reports.d.ts","sourceRoot":"","sources":["../../src/component/reports.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAO,EAAE,EAAE,MAAM,2BAA2B,CAAC;AA+LzD,eAAO,MAAM,QAAQ;;;;;;;;;;;;;gBAulEX,MAAM;mBACH,MAAM;gBACT,MAAM;sBACA,MAAM;wBACJ,MAAM;kBACZ,MAAM;kBACN,MAAM;qBACH,MAAM;sBACL,MAAM;oBACR,MAAM;2BACC,MAAM;qBACZ,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAnxDX,UAAU;YACV,YAAY;YACZ,QAAQ;YACR,YAAY;YACZ,UAAU;YACV,WAAW;YACX,SAAS,EAAE,aAAa;YACxB,QAAQ,EAAE,sBAAsB;YAChC,UAAU;YACV,WAAW,EAAE,aAAa;;;;;;;;;;;;;;sBAs3DtB,WAAW;;;;YAz2Df,aAAa,EAAE,aAAa;YAC5B,aAAa,EAAE,aAAa;YAC5B,aAAa,EAAE,aAAa;YAC5B,YAAY,EAAE,WAAW;;;GAKjC,CAAC;AAEH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAsEvB,CAAC;AAsBH;;;;;;;;;GASG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA0KN,MAAM;YAAM,MAAM;eAAS,MAAM;;;QA+CvC,MAAM;QACN,WAAW;;;eAzKN,aAAa;;;wBAGN,CAAC;;;;;;;;sBAqsDP,WAAW;;;;YA/rDP,KAAK,EAAE,iBAAiB;YAAW,KAAK;;;;;;;;GAyKxD,CAAC;AA0CH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEvB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BzB,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiE1B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgDzB,CAAC;AASH,eAAO,MAAM,WAAW;;;;;;;;qBAiBH,MAAM;oBAAc,MAAM;;;;;;;;;;;;QA0DvC,KAAK;QACL,QAAQ;;;;;;;QAWR,KAAK;;;sBA+qCC,WAAW;;;;GA3qCvB,CAAC;AAiBH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;QAoFhB,KAAK;QAGL,QAAQ;;;;;;;QACR,KAAK;;;sBAkkCD,WAAW;;;;;;;;;;;;;;;;KA5jCvB,CAAC;AAYH,eAAO,MAAM,gBAAgB;;;;;;;;qBAqDR,MAAM;sBACL,MAAM;yBACH,MAAM;;;;;;;;;;;;;;QAkGvB,KAAK;QACL,QAAQ;;;;;;;QAaR,KAAK;;;sBAy4BC,WAAW;;;QAx4BjB,cAAc;YACZ,KAAK;YAGL,KAAK;;;GAKb,CAAC;AAWH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA4HjB,KAAK,EAAE,aAAa;QACpB,QAAQ;;;;;;;QAOR,KAAK;;;sBAgvBC,WAAW;;;;GA5uBvB,CAAC;AAgBH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;GAkDvB,CAAC;AAkjBH,KAAK,YAAY,GAAG;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC"}
@@ -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
- timezone: v.literal("UTC"),
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: v.literal("eventVolume"),
132
- topCampaigns: v.literal("eventVolume"),
133
- topPages: v.literal("eventVolume"),
134
- topCountries: v.literal("eventVolume"),
135
- topDevices: v.literal("eventVolume"),
136
- topBrowsers: v.literal("eventVolume"),
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
- requireAlignedRange(from, to, intervalMs, `${interval} overview`);
172
- const firstBucket = Math.floor(from / intervalMs) * intervalMs;
173
- const lastBucket = Math.floor(to / intervalMs) * intervalMs;
174
- const bucketCount = Math.floor((lastBucket - firstBucket) / intervalMs) + 1;
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 = interval === "hour"
262
+ const overviewDataset = granularity === "hour"
188
263
  ? "overviewHour"
189
- : "overviewDay";
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", interval)
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
- const top = (type) => [...dimensions.entries()]
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(([, value]) => value)
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: "eventVolume",
270
- topCampaigns: "eventVolume",
271
- topPages: "eventVolume",
272
- topCountries: "eventVolume",
273
- topDevices: "eventVolume",
274
- topBrowsers: "eventVolume",
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" || source === "overviewDay") {
1452
- const granularity = source === "overviewHour" ? "hour" : "day";
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", `${report} requires complete inclusive UTC bucket boundaries`, { from, to, intervalMs, timezone: "UTC" });
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: "UTC",
1775
+ timezone,
1621
1776
  };
1622
1777
  }
1623
1778
  function parseKeysetPagination(options, scope) {