@iann29/rastro 0.1.0-alpha.10 → 0.1.0-alpha.12
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 +215 -46
- package/agent/integration.md +186 -46
- package/agent/manifest.json +28 -10
- package/agent/manifest.schema.json +41 -8
- package/dist/client/federation.d.ts +156 -11
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +82 -4
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +306 -97
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +116 -13
- 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 +26 -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 +8 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +32 -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 +47 -27
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +149 -295
- 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 +20 -13
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +158 -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/schema.d.ts +311 -5
- package/dist/component/schema.js +9 -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 +24 -5
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +16 -1
- 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 +93 -26
- package/docs/federation.md +120 -30
- package/docs/upgrading.md +103 -17
- package/llms.txt +6 -5
- package/package.json +2 -1
- package/src/component/_generated/api.ts +6 -0
- package/src/component/_generated/component.ts +27 -12
- package/src/component/constants.ts +6 -0
- package/src/component/coverage.ts +41 -6
- package/src/component/geo.ts +82 -28
- package/src/component/http.ts +60 -26
- package/src/component/ingest.ts +226 -425
- package/src/component/localTime.ts +167 -0
- package/src/component/reports.ts +222 -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/schema.ts +17 -0
- package/src/component/sites.ts +32 -0
- package/src/component/validators.ts +20 -1
- package/src/tracker/generated.ts +12 -0
package/dist/component/ingest.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { mutation } from "./_generated/server.js";
|
|
3
3
|
import { internal } from "./_generated/api.js";
|
|
4
|
-
import { AGGREGATE_SHARDS, MAX_BATCH_EVENT_GROUPS, DAY_MS, HOURLY_AGGREGATE_SHARDS, DIMENSION_SLOTS, HOUR_MS, LIVE_LEAVE_GRACE_MS, LIVE_SESSION_TTL_MS, LIVE_SWEEP_INTERVAL_MS, MAX_BATCH_EVENTS, MAX_SITE_BYTES_PER_SHARD_WINDOW, MAX_SITE_EVENTS_PER_SHARD_WINDOW, MAX_BYTES_PER_SESSION_WINDOW, MAX_EVENTS_PER_SESSION_WINDOW, MAX_FUNNELS_PER_SITE,
|
|
4
|
+
import { AGGREGATE_SHARDS, MAX_BATCH_EVENT_GROUPS, DAY_MS, HOURLY_AGGREGATE_SHARDS, DIMENSION_SLOTS, HOUR_MS, LIVE_LEAVE_GRACE_MS, LIVE_SESSION_TTL_MS, LIVE_SWEEP_INTERVAL_MS, MAX_BATCH_EVENTS, MAX_SITE_BYTES_PER_SHARD_WINDOW, MAX_SITE_EVENTS_PER_SHARD_WINDOW, MAX_BYTES_PER_SESSION_WINDOW, MAX_EVENTS_PER_SESSION_WINDOW, MAX_FUNNELS_PER_SITE, MAX_GOALS_PER_SITE, RATE_LIMIT_WINDOW_MS, REPORT_ROLLUP_SHARDS, SECONDARY_AGGREGATE_SHARDS, SITE_INGEST_SHARDS, } from "./constants.js";
|
|
5
5
|
import { addVisitor, createVisitorSketch, unionVisitorSketches, } from "./cardinality.js";
|
|
6
|
-
import { addToVitalHistogram, emptyVitalHistogram,
|
|
6
|
+
import { addToVitalHistogram, emptyVitalHistogram, vitalDevice, VITAL_ALL, } from "./vitals.js";
|
|
7
7
|
import { fail } from "./errors.js";
|
|
8
8
|
import { ensureAnalyticsControl } from "./coverage.js";
|
|
9
9
|
import { bufferEventBatch, eventBatchKey, eventBucketStart, findTelemetry, flushEventBatches, insertEventBatch, inspectTelemetryMinute, } from "./eventStore.js";
|
|
10
10
|
import { cleanString, isValidMoney, measureBatchBytes, normalizeRoute, originAllowed, sanitizeContext, sanitizeCurrency, sanitizeEvent, sanitizeOpaqueId, sanitizePath, sanitizeProperties, sanitizeSlug, sourceFromReferrer, stableHash, } from "./sanitize.js";
|
|
11
11
|
import { ingestContextValidator, ingestResultValidator, trackerEventValidator, trustedConversionValidator, } from "./validators.js";
|
|
12
|
+
import { createReportRollupDeltas, enqueueReportRollups, reportRollupCoordinates, } from "./rollupStore.js";
|
|
13
|
+
import { localDayStart, localDayTimezone } from "./localTime.js";
|
|
12
14
|
export const ingestBatch = mutation({
|
|
13
15
|
args: {
|
|
14
16
|
siteId: v.id("sites"),
|
|
@@ -42,6 +44,7 @@ export const ingestBatch = mutation({
|
|
|
42
44
|
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
43
45
|
await consumeSiteRateLimit(ctx, args.siteId, args.events, eventByteCounts, now);
|
|
44
46
|
const context = sanitizeContext(args.context);
|
|
47
|
+
const timezone = localDayTimezone(site.timezone);
|
|
45
48
|
const aggregateDeltas = new Map();
|
|
46
49
|
const reportRollupDeltas = createReportRollupDeltas();
|
|
47
50
|
const sessionRateWindows = new Map();
|
|
@@ -143,7 +146,11 @@ export const ingestBatch = mutation({
|
|
|
143
146
|
rejected += 1;
|
|
144
147
|
continue;
|
|
145
148
|
}
|
|
146
|
-
await accountHeartbeat(ctx, args.siteId, existingSession, event, context, now, batchState);
|
|
149
|
+
const durationDeltaMs = await accountHeartbeat(ctx, args.siteId, existingSession, event, context, now, batchState);
|
|
150
|
+
// A heartbeat is presence, not telemetry, but it is what keeps a
|
|
151
|
+
// session's duration honest while the visitor reads: the growth is
|
|
152
|
+
// folded into the buckets the heartbeat landed in.
|
|
153
|
+
foldSessionDuration(aggregateDeltas, args.siteId, event, durationDeltaMs, timezone);
|
|
147
154
|
accepted += 1;
|
|
148
155
|
continue;
|
|
149
156
|
}
|
|
@@ -222,6 +229,7 @@ export const ingestBatch = mutation({
|
|
|
222
229
|
aggregateDeltas,
|
|
223
230
|
reportRollupDeltas,
|
|
224
231
|
recordTelemetry: true,
|
|
232
|
+
timezone,
|
|
225
233
|
financialConversion: false,
|
|
226
234
|
trustedGoalKey: undefined,
|
|
227
235
|
now,
|
|
@@ -233,7 +241,7 @@ export const ingestBatch = mutation({
|
|
|
233
241
|
await flushBatchState(ctx, batchState);
|
|
234
242
|
await flushSessionRateLimits(ctx, sessionRateWindows, now);
|
|
235
243
|
await flushAggregates(ctx, aggregateDeltas);
|
|
236
|
-
await
|
|
244
|
+
await enqueueReportRollups(ctx, args.siteId, reportRollupDeltas, now);
|
|
237
245
|
return { accepted, duplicates, rejected };
|
|
238
246
|
},
|
|
239
247
|
});
|
|
@@ -322,6 +330,7 @@ export const trackConversion = mutation({
|
|
|
322
330
|
aggregateDeltas,
|
|
323
331
|
reportRollupDeltas,
|
|
324
332
|
recordTelemetry: existingTelemetry === null && !telemetry.saturated,
|
|
333
|
+
timezone: localDayTimezone(site.timezone),
|
|
325
334
|
financialConversion: true,
|
|
326
335
|
trustedGoalKey,
|
|
327
336
|
now: Date.now(),
|
|
@@ -330,8 +339,9 @@ export const trackConversion = mutation({
|
|
|
330
339
|
await insertEventBatch(ctx, args.siteId, event, session.country ?? "unknown");
|
|
331
340
|
}
|
|
332
341
|
await flushAggregates(ctx, aggregateDeltas);
|
|
333
|
-
|
|
334
|
-
await
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
await enqueueReportRollups(ctx, args.siteId, reportRollupDeltas, now);
|
|
344
|
+
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
335
345
|
return { accepted: 1, duplicates: 0, rejected: 0 };
|
|
336
346
|
},
|
|
337
347
|
});
|
|
@@ -478,7 +488,7 @@ async function accountEvent(ctx, input) {
|
|
|
478
488
|
},
|
|
479
489
|
...matchedGoalKeys.map((value) => ({ type: "goal", value })),
|
|
480
490
|
];
|
|
481
|
-
foldAggregates(input.aggregateDeltas, input.siteId, event, session, dimensions, input.recordTelemetry, input.financialConversion);
|
|
491
|
+
foldAggregates(input.aggregateDeltas, input.siteId, event, session, dimensions, input.recordTelemetry, input.financialConversion, input.timezone);
|
|
482
492
|
if (input.recordTelemetry &&
|
|
483
493
|
event.timestamp >= input.now - LIVE_SESSION_TTL_MS) {
|
|
484
494
|
await updateLiveSession(ctx, input.siteId, event, input.context, session, input.now, input.batchState);
|
|
@@ -499,6 +509,9 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
499
509
|
isNewSession: false,
|
|
500
510
|
isNewVisitorHour: false,
|
|
501
511
|
isNewVisitorDay: false,
|
|
512
|
+
isNewVisitor: false,
|
|
513
|
+
durationDeltaMs: 0,
|
|
514
|
+
continued: false,
|
|
502
515
|
startedAt: existing.startedAt,
|
|
503
516
|
source: existing.source,
|
|
504
517
|
utmCampaign: existing.utmCampaign,
|
|
@@ -583,6 +596,9 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
583
596
|
Math.floor(event.timestamp / HOUR_MS),
|
|
584
597
|
isNewVisitorDay: Math.floor(existing.lastSeenAt / DAY_MS) <
|
|
585
598
|
Math.floor(event.timestamp / DAY_MS),
|
|
599
|
+
isNewVisitor: false,
|
|
600
|
+
durationDeltaMs: Math.max(0, updates.durationMs - existing.durationMs),
|
|
601
|
+
continued: event.type === "pageview" && existing.pageviewCount === 1,
|
|
586
602
|
startedAt,
|
|
587
603
|
source: existing.source,
|
|
588
604
|
utmCampaign: existing.utmCampaign,
|
|
@@ -612,6 +628,18 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
612
628
|
}
|
|
613
629
|
}
|
|
614
630
|
}
|
|
631
|
+
// A visitor is new when nothing was ever recorded under their id. A host
|
|
632
|
+
// id whose anonymous history was linked at sign-up is a return, not a new
|
|
633
|
+
// person; a browser without a persistent id is new every session.
|
|
634
|
+
let isNewVisitor = priorVisit === null;
|
|
635
|
+
if (isNewVisitor && event.visitorId !== event.sessionId) {
|
|
636
|
+
const alias = await ctx.db
|
|
637
|
+
.query("visitorAliases")
|
|
638
|
+
.withIndex("by_siteId_and_visitorId", (range) => range.eq("siteId", siteId).eq("visitorId", event.visitorId))
|
|
639
|
+
.first();
|
|
640
|
+
if (alias)
|
|
641
|
+
isNewVisitor = false;
|
|
642
|
+
}
|
|
615
643
|
const fields = {
|
|
616
644
|
siteId,
|
|
617
645
|
sessionId: event.sessionId,
|
|
@@ -667,6 +695,9 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
667
695
|
isNewVisitorDay: priorVisit === null ||
|
|
668
696
|
Math.floor(priorVisit.lastSeenAt / DAY_MS) <
|
|
669
697
|
Math.floor(event.timestamp / DAY_MS),
|
|
698
|
+
isNewVisitor,
|
|
699
|
+
durationDeltaMs: 0,
|
|
700
|
+
continued: false,
|
|
670
701
|
startedAt: event.timestamp,
|
|
671
702
|
source,
|
|
672
703
|
utmCampaign,
|
|
@@ -816,10 +847,14 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
816
847
|
else {
|
|
817
848
|
await ctx.db.patch("sessions", session._id, updates);
|
|
818
849
|
}
|
|
850
|
+
const durationDeltaMs = Math.max(0, updates.durationMs - session.durationMs);
|
|
819
851
|
await updateLiveSession(ctx, siteId, event, context, {
|
|
820
852
|
isNewSession: false,
|
|
821
853
|
isNewVisitorHour: false,
|
|
822
854
|
isNewVisitorDay: false,
|
|
855
|
+
isNewVisitor: false,
|
|
856
|
+
durationDeltaMs: 0,
|
|
857
|
+
continued: false,
|
|
823
858
|
startedAt,
|
|
824
859
|
source: session.source,
|
|
825
860
|
visitorKey: session.visitorKey ?? session.visitorId,
|
|
@@ -831,6 +866,7 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
831
866
|
eventCount: session.eventCount,
|
|
832
867
|
affiliateSlug: session.affiliateSlug,
|
|
833
868
|
}, now, batchState);
|
|
869
|
+
return durationDeltaMs;
|
|
834
870
|
}
|
|
835
871
|
async function consumeSiteRateLimit(ctx, siteId, events, eventByteCounts, now) {
|
|
836
872
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
@@ -1114,7 +1150,7 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1114
1150
|
_id: progressId,
|
|
1115
1151
|
};
|
|
1116
1152
|
}
|
|
1117
|
-
|
|
1153
|
+
foldFunnelStepDelta(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1118
1154
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1119
1155
|
return;
|
|
1120
1156
|
}
|
|
@@ -1145,7 +1181,7 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1145
1181
|
completedAt: undefined,
|
|
1146
1182
|
});
|
|
1147
1183
|
}
|
|
1148
|
-
|
|
1184
|
+
foldFunnelStepDelta(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1149
1185
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1150
1186
|
return;
|
|
1151
1187
|
}
|
|
@@ -1167,7 +1203,7 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1167
1203
|
else {
|
|
1168
1204
|
await ctx.db.patch("funnelProgress", progress._id, updates);
|
|
1169
1205
|
}
|
|
1170
|
-
|
|
1206
|
+
foldFunnelStepDelta(reportRollupDeltas, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1171
1207
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1172
1208
|
}
|
|
1173
1209
|
function stepMatches(step, event) {
|
|
@@ -1175,36 +1211,6 @@ function stepMatches(step, event) {
|
|
|
1175
1211
|
? event.type === "pageview" && event.path === step.value
|
|
1176
1212
|
: event.type === "custom" && event.name === step.value;
|
|
1177
1213
|
}
|
|
1178
|
-
async function incrementFunnelStep(ctx, siteId, funnelId, event, step, completed) {
|
|
1179
|
-
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1180
|
-
const shard = stableHash(event.visitorId) % SECONDARY_AGGREGATE_SHARDS;
|
|
1181
|
-
const bucket = await ctx.db
|
|
1182
|
-
.query("funnelStepBuckets")
|
|
1183
|
-
.withIndex("by_siteId_and_funnelId_and_bucketStart_and_step_and_shard", (range) => range
|
|
1184
|
-
.eq("siteId", siteId)
|
|
1185
|
-
.eq("funnelId", funnelId)
|
|
1186
|
-
.eq("bucketStart", bucketStart)
|
|
1187
|
-
.eq("step", step)
|
|
1188
|
-
.eq("shard", shard))
|
|
1189
|
-
.unique();
|
|
1190
|
-
if (bucket) {
|
|
1191
|
-
await ctx.db.patch("funnelStepBuckets", bucket._id, {
|
|
1192
|
-
entrants: bucket.entrants + 1,
|
|
1193
|
-
completions: bucket.completions + (completed ? 1 : 0),
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
else {
|
|
1197
|
-
await ctx.db.insert("funnelStepBuckets", {
|
|
1198
|
-
siteId,
|
|
1199
|
-
funnelId,
|
|
1200
|
-
bucketStart,
|
|
1201
|
-
step,
|
|
1202
|
-
shard,
|
|
1203
|
-
entrants: 1,
|
|
1204
|
-
completions: completed ? 1 : 0,
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
1214
|
async function accountConversion(ctx, siteId, session, event, trustedGoalKey, reportRollupDeltas) {
|
|
1209
1215
|
const revenueCents = event.revenueCents ?? 0;
|
|
1210
1216
|
const currency = event.currency ?? "USD";
|
|
@@ -1266,24 +1272,6 @@ async function accountConversion(ctx, siteId, session, event, trustedGoalKey, re
|
|
|
1266
1272
|
}
|
|
1267
1273
|
}
|
|
1268
1274
|
}
|
|
1269
|
-
function createReportRollupDeltas() {
|
|
1270
|
-
return {
|
|
1271
|
-
goals: new Map(),
|
|
1272
|
-
funnels: new Map(),
|
|
1273
|
-
affiliates: new Map(),
|
|
1274
|
-
vitals: new Map(),
|
|
1275
|
-
routes: new Map(),
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
function reportRollupCoordinates(siteId, definitionId, timestamp, visitorId) {
|
|
1279
|
-
const bucketStart = Math.floor(timestamp / DAY_MS) * DAY_MS;
|
|
1280
|
-
const shard = stableHash(visitorId) % REPORT_ROLLUP_SHARDS;
|
|
1281
|
-
return {
|
|
1282
|
-
bucketStart,
|
|
1283
|
-
shard,
|
|
1284
|
-
key: `${siteId}:${definitionId}:${bucketStart}:${shard}`,
|
|
1285
|
-
};
|
|
1286
|
-
}
|
|
1287
1275
|
function foldGoalRollup(deltas, siteId, event, goal) {
|
|
1288
1276
|
const coordinates = reportRollupCoordinates(siteId, String(goal._id), event.timestamp, event.visitorId);
|
|
1289
1277
|
const current = deltas.goals.get(coordinates.key) ?? {
|
|
@@ -1318,6 +1306,23 @@ function foldFunnelRollup(deltas, siteId, funnelId, event, step, completed) {
|
|
|
1318
1306
|
rollup.steps.set(step, current);
|
|
1319
1307
|
deltas.funnels.set(coordinates.key, rollup);
|
|
1320
1308
|
}
|
|
1309
|
+
function foldFunnelStepDelta(deltas, siteId, funnelId, event, step, completed) {
|
|
1310
|
+
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1311
|
+
const shard = stableHash(event.visitorId) % SECONDARY_AGGREGATE_SHARDS;
|
|
1312
|
+
const key = `${siteId}:${funnelId}:${bucketStart}:${step}:${shard}`;
|
|
1313
|
+
const current = deltas.funnelSteps.get(key) ?? {
|
|
1314
|
+
siteId,
|
|
1315
|
+
funnelId,
|
|
1316
|
+
bucketStart,
|
|
1317
|
+
step,
|
|
1318
|
+
shard,
|
|
1319
|
+
entrants: 0,
|
|
1320
|
+
completions: 0,
|
|
1321
|
+
};
|
|
1322
|
+
current.entrants += 1;
|
|
1323
|
+
current.completions += Number(completed);
|
|
1324
|
+
deltas.funnelSteps.set(key, current);
|
|
1325
|
+
}
|
|
1321
1326
|
function foldAffiliateVisitRollup(deltas, siteId, event, attribution, visitorKey) {
|
|
1322
1327
|
const coordinates = reportRollupCoordinates(siteId, String(attribution.affiliateId), event.timestamp, event.visitorId);
|
|
1323
1328
|
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
@@ -1327,13 +1332,13 @@ function foldAffiliateVisitRollup(deltas, siteId, event, attribution, visitorKey
|
|
|
1327
1332
|
shard: coordinates.shard,
|
|
1328
1333
|
affiliateSlug: attribution.affiliateSlug,
|
|
1329
1334
|
visits: 0,
|
|
1330
|
-
|
|
1335
|
+
visitorKeys: new Set(),
|
|
1331
1336
|
conversions: 0,
|
|
1332
1337
|
revenueCents: 0,
|
|
1333
1338
|
commissionCents: 0,
|
|
1334
1339
|
};
|
|
1335
1340
|
current.visits += 1;
|
|
1336
|
-
current.
|
|
1341
|
+
current.visitorKeys.add(visitorKey);
|
|
1337
1342
|
deltas.affiliates.set(coordinates.key, current);
|
|
1338
1343
|
}
|
|
1339
1344
|
function foldAffiliateConversionRollup(deltas, siteId, event, affiliate, affiliateSlug, revenueCents, commissionCents) {
|
|
@@ -1345,7 +1350,7 @@ function foldAffiliateConversionRollup(deltas, siteId, event, affiliate, affilia
|
|
|
1345
1350
|
shard: coordinates.shard,
|
|
1346
1351
|
affiliateSlug,
|
|
1347
1352
|
visits: 0,
|
|
1348
|
-
|
|
1353
|
+
visitorKeys: new Set(),
|
|
1349
1354
|
conversions: 0,
|
|
1350
1355
|
revenueCents: 0,
|
|
1351
1356
|
commissionCents: 0,
|
|
@@ -1405,7 +1410,7 @@ function foldRouteDelta(deltas, siteId, route, timestamp, visitorId, apply) {
|
|
|
1405
1410
|
continued: 0,
|
|
1406
1411
|
dwellMs: 0,
|
|
1407
1412
|
dwellSamples: 0,
|
|
1408
|
-
|
|
1413
|
+
visitorKeys: new Set(),
|
|
1409
1414
|
outgoing: new Map(),
|
|
1410
1415
|
};
|
|
1411
1416
|
apply(current);
|
|
@@ -1426,7 +1431,7 @@ function foldRouteRollups(deltas, siteId, event, session, existing) {
|
|
|
1426
1431
|
delta.pageviews += 1;
|
|
1427
1432
|
if (firstPageview)
|
|
1428
1433
|
delta.entries += 1;
|
|
1429
|
-
delta.
|
|
1434
|
+
delta.visitorKeys.add(session.visitorKey);
|
|
1430
1435
|
});
|
|
1431
1436
|
// Out-of-order telemetry keeps its view but cannot say where it came from.
|
|
1432
1437
|
if (existing === null || firstPageview)
|
|
@@ -1448,245 +1453,19 @@ function foldRouteRollups(deltas, siteId, event, session, existing) {
|
|
|
1448
1453
|
});
|
|
1449
1454
|
}
|
|
1450
1455
|
}
|
|
1451
|
-
|
|
1452
|
-
const existing = await ctx.db
|
|
1453
|
-
.query("routeDailyRollups")
|
|
1454
|
-
.withIndex("by_siteId_and_route_and_bucketStart_and_shard", (range) => range
|
|
1455
|
-
.eq("siteId", delta.siteId)
|
|
1456
|
-
.eq("route", delta.route)
|
|
1457
|
-
.eq("bucketStart", delta.bucketStart)
|
|
1458
|
-
.eq("shard", delta.shard))
|
|
1459
|
-
.unique();
|
|
1460
|
-
if (!existing && delta.route !== SITE_MAP_OTHER_ROUTE) {
|
|
1461
|
-
// A new route row spends one bounded read per batch to respect the daily
|
|
1462
|
-
// cap; past it, the day's remaining routes fold into "(other)".
|
|
1463
|
-
const capRows = MAX_SITE_MAP_ROUTES_PER_DAY * REPORT_ROLLUP_SHARDS;
|
|
1464
|
-
const dayKey = `${delta.siteId}:${delta.bucketStart}`;
|
|
1465
|
-
let dayRows = dayRowCounts.get(dayKey);
|
|
1466
|
-
if (dayRows === undefined) {
|
|
1467
|
-
dayRows = (await ctx.db
|
|
1468
|
-
.query("routeDailyRollups")
|
|
1469
|
-
.withIndex("by_siteId_and_bucketStart", (range) => range
|
|
1470
|
-
.eq("siteId", delta.siteId)
|
|
1471
|
-
.eq("bucketStart", delta.bucketStart))
|
|
1472
|
-
.take(capRows + 1)).length;
|
|
1473
|
-
}
|
|
1474
|
-
if (dayRows >= capRows) {
|
|
1475
|
-
await upsertRouteRollup(ctx, { ...delta, route: SITE_MAP_OTHER_ROUTE }, dayRowCounts);
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
dayRowCounts.set(dayKey, dayRows + 1);
|
|
1479
|
-
}
|
|
1480
|
-
const outgoing = existing?.outgoing.map((entry) => ({ ...entry })) ?? [];
|
|
1481
|
-
for (const [target, count] of delta.outgoing) {
|
|
1482
|
-
let entry = outgoing.find((candidate) => candidate.route === target);
|
|
1483
|
-
if (!entry) {
|
|
1484
|
-
const named = outgoing.filter((candidate) => candidate.route !== SITE_MAP_OTHER_ROUTE).length;
|
|
1485
|
-
const route = target !== SITE_MAP_OTHER_ROUTE && named >= ROUTE_TRANSITION_SLOTS
|
|
1486
|
-
? SITE_MAP_OTHER_ROUTE
|
|
1487
|
-
: target;
|
|
1488
|
-
entry = outgoing.find((candidate) => candidate.route === route);
|
|
1489
|
-
if (!entry) {
|
|
1490
|
-
entry = { route, count: 0 };
|
|
1491
|
-
outgoing.push(entry);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
entry.count += count;
|
|
1495
|
-
}
|
|
1496
|
-
const fields = {
|
|
1497
|
-
siteId: delta.siteId,
|
|
1498
|
-
bucketStart: delta.bucketStart,
|
|
1499
|
-
shard: delta.shard,
|
|
1500
|
-
generation: 1,
|
|
1501
|
-
route: delta.route,
|
|
1502
|
-
pageviews: (existing?.pageviews ?? 0) + delta.pageviews,
|
|
1503
|
-
entries: (existing?.entries ?? 0) + delta.entries,
|
|
1504
|
-
continued: (existing?.continued ?? 0) + delta.continued,
|
|
1505
|
-
dwellMs: (existing?.dwellMs ?? 0) + delta.dwellMs,
|
|
1506
|
-
dwellSamples: (existing?.dwellSamples ?? 0) + delta.dwellSamples,
|
|
1507
|
-
visitorSketch: unionVisitorSketches(existing?.visitorSketch ?? createVisitorSketch(), delta.visitorSketch),
|
|
1508
|
-
outgoing,
|
|
1509
|
-
};
|
|
1510
|
-
if (existing) {
|
|
1511
|
-
await ctx.db.replace("routeDailyRollups", existing._id, fields);
|
|
1512
|
-
}
|
|
1513
|
-
else {
|
|
1514
|
-
await ctx.db.insert("routeDailyRollups", fields);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
async function upsertVitalRollup(ctx, delta) {
|
|
1518
|
-
const existing = await ctx.db
|
|
1519
|
-
.query("vitalRollups")
|
|
1520
|
-
.withIndex("by_key", (range) => range
|
|
1521
|
-
.eq("siteId", delta.siteId)
|
|
1522
|
-
.eq("bucketStart", delta.bucketStart)
|
|
1523
|
-
.eq("device", delta.device)
|
|
1524
|
-
.eq("page", delta.page)
|
|
1525
|
-
.eq("shard", delta.shard))
|
|
1526
|
-
.unique();
|
|
1527
|
-
if (!existing &&
|
|
1528
|
-
delta.device === VITAL_ALL &&
|
|
1529
|
-
delta.page !== VITAL_OTHER_PAGES) {
|
|
1530
|
-
// A new page row spends one bounded read to respect the daily page slots;
|
|
1531
|
-
// past the cap, the day's remaining pages fold into "(other)".
|
|
1532
|
-
const pageRows = await ctx.db
|
|
1533
|
-
.query("vitalRollups")
|
|
1534
|
-
.withIndex("by_key", (range) => range
|
|
1535
|
-
.eq("siteId", delta.siteId)
|
|
1536
|
-
.eq("bucketStart", delta.bucketStart)
|
|
1537
|
-
.eq("device", VITAL_ALL))
|
|
1538
|
-
.take(VITAL_PAGE_SLOTS * REPORT_ROLLUP_SHARDS + 1);
|
|
1539
|
-
if (pageRows.length > VITAL_PAGE_SLOTS * REPORT_ROLLUP_SHARDS) {
|
|
1540
|
-
await upsertVitalRollup(ctx, { ...delta, page: VITAL_OTHER_PAGES });
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
const metrics = existing?.metrics.map((entry) => ({ ...entry })) ?? [];
|
|
1545
|
-
for (const [metric, addition] of delta.metrics) {
|
|
1546
|
-
const entry = metrics.find((candidate) => candidate.metric === metric);
|
|
1547
|
-
if (entry) {
|
|
1548
|
-
entry.count += addition.count;
|
|
1549
|
-
entry.sum += addition.sum;
|
|
1550
|
-
entry.histogram = mergeVitalHistograms(entry.histogram, addition.histogram);
|
|
1551
|
-
}
|
|
1552
|
-
else {
|
|
1553
|
-
metrics.push({
|
|
1554
|
-
metric,
|
|
1555
|
-
count: addition.count,
|
|
1556
|
-
sum: addition.sum,
|
|
1557
|
-
histogram: [...addition.histogram],
|
|
1558
|
-
});
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
const fields = {
|
|
1562
|
-
siteId: delta.siteId,
|
|
1563
|
-
bucketStart: delta.bucketStart,
|
|
1564
|
-
device: delta.device,
|
|
1565
|
-
page: delta.page,
|
|
1566
|
-
shard: delta.shard,
|
|
1567
|
-
metrics,
|
|
1568
|
-
};
|
|
1569
|
-
if (existing) {
|
|
1570
|
-
await ctx.db.replace("vitalRollups", existing._id, fields);
|
|
1571
|
-
}
|
|
1572
|
-
else {
|
|
1573
|
-
await ctx.db.insert("vitalRollups", fields);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
async function flushReportRollups(ctx, deltas) {
|
|
1577
|
-
for (const delta of deltas.goals.values()) {
|
|
1578
|
-
const existing = await ctx.db
|
|
1579
|
-
.query("goalDailyRollups")
|
|
1580
|
-
.withIndex("by_siteId_and_goalId_and_bucketStart_and_shard", (range) => range
|
|
1581
|
-
.eq("siteId", delta.siteId)
|
|
1582
|
-
.eq("goalId", delta.goalId)
|
|
1583
|
-
.eq("bucketStart", delta.bucketStart)
|
|
1584
|
-
.eq("shard", delta.shard))
|
|
1585
|
-
.unique();
|
|
1586
|
-
const fields = {
|
|
1587
|
-
siteId: delta.siteId,
|
|
1588
|
-
goalId: delta.goalId,
|
|
1589
|
-
bucketStart: delta.bucketStart,
|
|
1590
|
-
shard: delta.shard,
|
|
1591
|
-
generation: 1,
|
|
1592
|
-
goalKey: delta.goalKey,
|
|
1593
|
-
completions: (existing?.completions ?? 0) + delta.completions,
|
|
1594
|
-
valueCents: (existing?.valueCents ?? 0) + delta.valueCents,
|
|
1595
|
-
};
|
|
1596
|
-
if (existing) {
|
|
1597
|
-
await ctx.db.replace("goalDailyRollups", existing._id, fields);
|
|
1598
|
-
}
|
|
1599
|
-
else {
|
|
1600
|
-
await ctx.db.insert("goalDailyRollups", fields);
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
for (const delta of deltas.funnels.values()) {
|
|
1604
|
-
const existing = await ctx.db
|
|
1605
|
-
.query("funnelDailyRollups")
|
|
1606
|
-
.withIndex("by_siteId_and_funnelId_and_bucketStart_and_shard", (range) => range
|
|
1607
|
-
.eq("siteId", delta.siteId)
|
|
1608
|
-
.eq("funnelId", delta.funnelId)
|
|
1609
|
-
.eq("bucketStart", delta.bucketStart)
|
|
1610
|
-
.eq("shard", delta.shard))
|
|
1611
|
-
.unique();
|
|
1612
|
-
const steps = existing?.steps.map((step) => ({ ...step })) ?? [];
|
|
1613
|
-
for (const addition of delta.steps.values()) {
|
|
1614
|
-
const current = steps.find((step) => step.step === addition.step);
|
|
1615
|
-
if (current) {
|
|
1616
|
-
current.entrants += addition.entrants;
|
|
1617
|
-
current.completions += addition.completions;
|
|
1618
|
-
}
|
|
1619
|
-
else {
|
|
1620
|
-
if (steps.length >= MAX_FUNNEL_STEPS) {
|
|
1621
|
-
fail("LIMIT_EXCEEDED", "daily funnel rollup step limit exceeded");
|
|
1622
|
-
}
|
|
1623
|
-
steps.push(addition);
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
const fields = {
|
|
1627
|
-
siteId: delta.siteId,
|
|
1628
|
-
funnelId: delta.funnelId,
|
|
1629
|
-
bucketStart: delta.bucketStart,
|
|
1630
|
-
shard: delta.shard,
|
|
1631
|
-
generation: 1,
|
|
1632
|
-
steps,
|
|
1633
|
-
};
|
|
1634
|
-
if (existing) {
|
|
1635
|
-
await ctx.db.replace("funnelDailyRollups", existing._id, fields);
|
|
1636
|
-
}
|
|
1637
|
-
else {
|
|
1638
|
-
await ctx.db.insert("funnelDailyRollups", fields);
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
for (const delta of deltas.affiliates.values()) {
|
|
1642
|
-
const existing = await ctx.db
|
|
1643
|
-
.query("affiliateDailyRollups")
|
|
1644
|
-
.withIndex("by_siteId_and_affiliateId_and_bucketStart_and_shard", (range) => range
|
|
1645
|
-
.eq("siteId", delta.siteId)
|
|
1646
|
-
.eq("affiliateId", delta.affiliateId)
|
|
1647
|
-
.eq("bucketStart", delta.bucketStart)
|
|
1648
|
-
.eq("shard", delta.shard))
|
|
1649
|
-
.unique();
|
|
1650
|
-
const fields = {
|
|
1651
|
-
siteId: delta.siteId,
|
|
1652
|
-
affiliateId: delta.affiliateId,
|
|
1653
|
-
bucketStart: delta.bucketStart,
|
|
1654
|
-
shard: delta.shard,
|
|
1655
|
-
generation: 1,
|
|
1656
|
-
affiliateSlug: delta.affiliateSlug,
|
|
1657
|
-
visits: (existing?.visits ?? 0) + delta.visits,
|
|
1658
|
-
visitorSketch: unionVisitorSketches(existing?.visitorSketch ?? createVisitorSketch(), delta.visitorSketch),
|
|
1659
|
-
conversions: (existing?.conversions ?? 0) + delta.conversions,
|
|
1660
|
-
revenueCents: (existing?.revenueCents ?? 0) + delta.revenueCents,
|
|
1661
|
-
commissionCents: (existing?.commissionCents ?? 0) + delta.commissionCents,
|
|
1662
|
-
};
|
|
1663
|
-
if (existing) {
|
|
1664
|
-
await ctx.db.replace("affiliateDailyRollups", existing._id, fields);
|
|
1665
|
-
}
|
|
1666
|
-
else {
|
|
1667
|
-
await ctx.db.insert("affiliateDailyRollups", fields);
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
const routeDayRowCounts = new Map();
|
|
1671
|
-
for (const delta of deltas.routes.values()) {
|
|
1672
|
-
await upsertRouteRollup(ctx, delta, routeDayRowCounts);
|
|
1673
|
-
}
|
|
1674
|
-
for (const delta of deltas.vitals.values()) {
|
|
1675
|
-
await upsertVitalRollup(ctx, delta);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, recordTelemetry, financialConversion) {
|
|
1456
|
+
function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, recordTelemetry, financialConversion, timezone) {
|
|
1679
1457
|
const revenueCents = financialConversion ? (event.revenueCents ?? 0) : 0;
|
|
1680
1458
|
const dimensionDeltas = dimensions.map((dimension) => ({
|
|
1681
1459
|
...dimension,
|
|
1682
1460
|
count: Number(recordTelemetry),
|
|
1683
1461
|
revenueCents,
|
|
1462
|
+
pageviews: Number(recordTelemetry && event.type === "pageview"),
|
|
1463
|
+
sessions: Number(recordTelemetry && session.isNewSession),
|
|
1684
1464
|
}));
|
|
1685
|
-
for (const granularity of
|
|
1465
|
+
for (const granularity of aggregateGranularities(timezone)) {
|
|
1686
1466
|
const shard = stableHash(event.sessionId) %
|
|
1687
1467
|
(granularity === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
1688
|
-
const
|
|
1689
|
-
const bucketStart = Math.floor(event.timestamp / interval) * interval;
|
|
1468
|
+
const bucketStart = aggregateBucketStart(granularity, event.timestamp, timezone);
|
|
1690
1469
|
const key = `${siteId}:${granularity}:${bucketStart}:${shard}`;
|
|
1691
1470
|
let delta = aggregateDeltas.get(key);
|
|
1692
1471
|
if (!delta) {
|
|
@@ -1706,6 +1485,9 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1706
1485
|
conversions: 0,
|
|
1707
1486
|
revenueCents: 0,
|
|
1708
1487
|
dimensions: [],
|
|
1488
|
+
durationMs: 0,
|
|
1489
|
+
continuedSessions: 0,
|
|
1490
|
+
newVisitors: 0,
|
|
1709
1491
|
};
|
|
1710
1492
|
aggregateDeltas.set(key, delta);
|
|
1711
1493
|
}
|
|
@@ -1727,6 +1509,58 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1727
1509
|
delta.conversions += Number(financialConversion);
|
|
1728
1510
|
delta.revenueCents += revenueCents;
|
|
1729
1511
|
delta.dimensions = mergeDimensions(delta.dimensions, dimensionDeltas);
|
|
1512
|
+
if (recordTelemetry) {
|
|
1513
|
+
delta.durationMs += session.durationDeltaMs;
|
|
1514
|
+
delta.continuedSessions += Number(session.continued);
|
|
1515
|
+
delta.newVisitors += Number(session.isNewSession && session.isNewVisitor);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* The buckets an event lands in: UTC hour and UTC day always, plus the
|
|
1521
|
+
* calendar day in the site's timezone when the site keeps one.
|
|
1522
|
+
*/
|
|
1523
|
+
function aggregateGranularities(timezone) {
|
|
1524
|
+
return timezone ? ["hour", "day", "localDay"] : ["hour", "day"];
|
|
1525
|
+
}
|
|
1526
|
+
function aggregateBucketStart(granularity, timestamp, timezone) {
|
|
1527
|
+
if (granularity === "localDay" && timezone) {
|
|
1528
|
+
return localDayStart(timestamp, timezone);
|
|
1529
|
+
}
|
|
1530
|
+
const interval = granularity === "hour" ? HOUR_MS : DAY_MS;
|
|
1531
|
+
return Math.floor(timestamp / interval) * interval;
|
|
1532
|
+
}
|
|
1533
|
+
/** Folds only session duration growth, for heartbeats. */
|
|
1534
|
+
function foldSessionDuration(aggregateDeltas, siteId, event, durationDeltaMs, timezone) {
|
|
1535
|
+
if (durationDeltaMs <= 0)
|
|
1536
|
+
return;
|
|
1537
|
+
for (const granularity of aggregateGranularities(timezone)) {
|
|
1538
|
+
const shard = stableHash(event.sessionId) %
|
|
1539
|
+
(granularity === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
1540
|
+
const bucketStart = aggregateBucketStart(granularity, event.timestamp, timezone);
|
|
1541
|
+
const key = `${siteId}:${granularity}:${bucketStart}:${shard}`;
|
|
1542
|
+
const delta = aggregateDeltas.get(key) ?? {
|
|
1543
|
+
siteId,
|
|
1544
|
+
granularity,
|
|
1545
|
+
bucketStart,
|
|
1546
|
+
shard,
|
|
1547
|
+
events: 0,
|
|
1548
|
+
pageviews: 0,
|
|
1549
|
+
clicks: 0,
|
|
1550
|
+
customEvents: 0,
|
|
1551
|
+
outboundClicks: 0,
|
|
1552
|
+
sessions: 0,
|
|
1553
|
+
visitors: 0,
|
|
1554
|
+
visitorSketch: createVisitorSketch(),
|
|
1555
|
+
conversions: 0,
|
|
1556
|
+
revenueCents: 0,
|
|
1557
|
+
dimensions: [],
|
|
1558
|
+
durationMs: 0,
|
|
1559
|
+
continuedSessions: 0,
|
|
1560
|
+
newVisitors: 0,
|
|
1561
|
+
};
|
|
1562
|
+
delta.durationMs += durationDeltaMs;
|
|
1563
|
+
aggregateDeltas.set(key, delta);
|
|
1730
1564
|
}
|
|
1731
1565
|
}
|
|
1732
1566
|
async function flushAggregates(ctx, aggregateDeltas) {
|
|
@@ -1739,7 +1573,11 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1739
1573
|
.eq("bucketStart", delta.bucketStart)
|
|
1740
1574
|
.eq("shard", delta.shard))
|
|
1741
1575
|
.unique();
|
|
1742
|
-
const dimensions = mergeDimensions(bucket?.dimensions ?? [], delta.dimensions)
|
|
1576
|
+
const dimensions = mergeDimensions(bucket?.dimensions ?? [], delta.dimensions.map((slot) => ({
|
|
1577
|
+
...slot,
|
|
1578
|
+
pageviews: slot.pageviews ?? 0,
|
|
1579
|
+
sessions: slot.sessions ?? 0,
|
|
1580
|
+
})));
|
|
1743
1581
|
const priorSketch = bucket?.visitorSketch;
|
|
1744
1582
|
const visitorSketch = priorSketch
|
|
1745
1583
|
? unionVisitorSketches(priorSketch, delta.visitorSketch)
|
|
@@ -1761,6 +1599,9 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1761
1599
|
conversions: bucket.conversions + delta.conversions,
|
|
1762
1600
|
revenueCents: bucket.revenueCents + delta.revenueCents,
|
|
1763
1601
|
dimensions,
|
|
1602
|
+
durationMs: (bucket.durationMs ?? 0) + delta.durationMs,
|
|
1603
|
+
continuedSessions: (bucket.continuedSessions ?? 0) + delta.continuedSessions,
|
|
1604
|
+
newVisitors: (bucket.newVisitors ?? 0) + delta.newVisitors,
|
|
1764
1605
|
});
|
|
1765
1606
|
}
|
|
1766
1607
|
else {
|
|
@@ -1781,6 +1622,9 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1781
1622
|
conversions: delta.conversions,
|
|
1782
1623
|
revenueCents: delta.revenueCents,
|
|
1783
1624
|
dimensions,
|
|
1625
|
+
durationMs: delta.durationMs,
|
|
1626
|
+
continuedSessions: delta.continuedSessions,
|
|
1627
|
+
newVisitors: delta.newVisitors,
|
|
1784
1628
|
});
|
|
1785
1629
|
}
|
|
1786
1630
|
}
|
|
@@ -1792,6 +1636,8 @@ function mergeDimensions(current, additions) {
|
|
|
1792
1636
|
if (exact) {
|
|
1793
1637
|
exact.count += addition.count;
|
|
1794
1638
|
exact.revenueCents += addition.revenueCents;
|
|
1639
|
+
exact.pageviews = (exact.pageviews ?? 0) + addition.pageviews;
|
|
1640
|
+
exact.sessions = (exact.sessions ?? 0) + addition.sessions;
|
|
1795
1641
|
continue;
|
|
1796
1642
|
}
|
|
1797
1643
|
const typeSlots = result.filter((candidate) => candidate.type === addition.type);
|
|
@@ -1808,6 +1654,8 @@ function mergeDimensions(current, additions) {
|
|
|
1808
1654
|
value: addition.value,
|
|
1809
1655
|
count: addition.count,
|
|
1810
1656
|
revenueCents: addition.revenueCents,
|
|
1657
|
+
pageviews: addition.pageviews,
|
|
1658
|
+
sessions: addition.sessions,
|
|
1811
1659
|
});
|
|
1812
1660
|
continue;
|
|
1813
1661
|
}
|
|
@@ -1815,6 +1663,8 @@ function mergeDimensions(current, additions) {
|
|
|
1815
1663
|
if (overflow) {
|
|
1816
1664
|
overflow.count += addition.count;
|
|
1817
1665
|
overflow.revenueCents += addition.revenueCents;
|
|
1666
|
+
overflow.pageviews = (overflow.pageviews ?? 0) + addition.pageviews;
|
|
1667
|
+
overflow.sessions = (overflow.sessions ?? 0) + addition.sessions;
|
|
1818
1668
|
continue;
|
|
1819
1669
|
}
|
|
1820
1670
|
if (addition.count === 0 && addition.revenueCents === 0)
|
|
@@ -1830,6 +1680,8 @@ function mergeDimensions(current, additions) {
|
|
|
1830
1680
|
value: "(other)",
|
|
1831
1681
|
count: addition.count,
|
|
1832
1682
|
revenueCents: addition.revenueCents,
|
|
1683
|
+
pageviews: addition.pageviews,
|
|
1684
|
+
sessions: addition.sessions,
|
|
1833
1685
|
});
|
|
1834
1686
|
continue;
|
|
1835
1687
|
}
|
|
@@ -1841,6 +1693,8 @@ function mergeDimensions(current, additions) {
|
|
|
1841
1693
|
replacement.value = "(other)";
|
|
1842
1694
|
replacement.count += addition.count;
|
|
1843
1695
|
replacement.revenueCents += addition.revenueCents;
|
|
1696
|
+
replacement.pageviews = (replacement.pageviews ?? 0) + addition.pageviews;
|
|
1697
|
+
replacement.sessions = (replacement.sessions ?? 0) + addition.sessions;
|
|
1844
1698
|
}
|
|
1845
1699
|
return result;
|
|
1846
1700
|
}
|