@iann29/rastro 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +693 -69
- package/agent/integration.md +801 -0
- package/agent/manifest.json +205 -0
- package/agent/manifest.schema.json +444 -0
- package/dist/client/federation.d.ts +381 -0
- package/dist/client/federation.d.ts.map +1 -0
- package/dist/client/federation.js +274 -0
- package/dist/client/federation.js.map +1 -0
- package/dist/client/index.d.ts +2673 -15
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +565 -33
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +22 -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 +315 -4
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +4 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/affiliates.d.ts.map +1 -1
- package/dist/component/affiliates.js +6 -2
- package/dist/component/affiliates.js.map +1 -1
- package/dist/component/cardinality.d.ts +12 -0
- package/dist/component/cardinality.d.ts.map +1 -0
- package/dist/component/cardinality.js +94 -0
- package/dist/component/cardinality.js.map +1 -0
- package/dist/component/constants.d.ts +28 -1
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +44 -1
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +6 -1
- package/dist/component/convex.config.js +9 -1
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/coverage.d.ts +27 -0
- package/dist/component/coverage.d.ts.map +1 -0
- package/dist/component/coverage.js +56 -0
- package/dist/component/coverage.js.map +1 -0
- package/dist/component/diagnostics.d.ts +9 -0
- package/dist/component/diagnostics.d.ts.map +1 -0
- package/dist/component/diagnostics.js +47 -0
- package/dist/component/diagnostics.js.map +1 -0
- package/dist/component/errors.d.ts +1 -1
- package/dist/component/errors.d.ts.map +1 -1
- package/dist/component/errors.js.map +1 -1
- package/dist/component/eventStore.d.ts +21 -9
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/eventStore.js +142 -152
- package/dist/component/eventStore.js.map +1 -1
- package/dist/component/funnels.d.ts.map +1 -1
- package/dist/component/funnels.js +5 -3
- package/dist/component/funnels.js.map +1 -1
- package/dist/component/geo.d.ts +73 -0
- package/dist/component/geo.d.ts.map +1 -0
- package/dist/component/geo.js +648 -0
- package/dist/component/geo.js.map +1 -0
- package/dist/component/goals.d.ts.map +1 -1
- package/dist/component/goals.js +8 -5
- package/dist/component/goals.js.map +1 -1
- package/dist/component/guards.d.ts.map +1 -1
- package/dist/component/guards.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +281 -62
- package/dist/component/http.js.map +1 -1
- package/dist/component/identity.d.ts +13 -0
- package/dist/component/identity.d.ts.map +1 -0
- package/dist/component/identity.js +58 -0
- package/dist/component/identity.js.map +1 -0
- package/dist/component/ingest.d.ts +3 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +498 -95
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +7 -6
- package/dist/component/live.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 +266 -10
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +1243 -137
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts +75 -1
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +561 -38
- 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 +24 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +96 -16
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +687 -65
- package/dist/component/schema.js +187 -20
- 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 +41 -7
- package/dist/component/sites.js.map +1 -1
- package/dist/component/useragent.d.ts +9 -0
- package/dist/component/useragent.d.ts.map +1 -0
- package/dist/component/useragent.js +152 -0
- package/dist/component/useragent.js.map +1 -0
- package/dist/component/validators.d.ts +124 -69
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +35 -14
- package/dist/component/validators.js.map +1 -1
- package/dist/component/visitors.d.ts +19 -0
- package/dist/component/visitors.d.ts.map +1 -0
- package/dist/component/visitors.js +86 -0
- package/dist/component/visitors.js.map +1 -0
- package/dist/component/vitals.d.ts +41 -0
- package/dist/component/vitals.d.ts.map +1 -0
- package/dist/component/vitals.js +115 -0
- package/dist/component/vitals.js.map +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/tracker/generated.d.ts +11 -4
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +11 -4
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.d.ts +1 -1
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +57 -20
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker/vitals.d.ts +10 -0
- package/dist/tracker/vitals.d.ts.map +1 -0
- package/dist/tracker/vitals.js +140 -0
- package/dist/tracker/vitals.js.map +1 -0
- package/dist/tracker.min.js +1 -1
- package/dist/vitals.min.js +1 -0
- package/docs/benchmarks/2026-08-20-realistic.md +76 -76
- package/docs/benchmarks/2026-08-21-formal-certification.md +353 -0
- package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
- package/docs/federation-setup.md +464 -0
- package/docs/federation.md +352 -0
- package/docs/upgrading.md +344 -0
- package/llms.txt +72 -0
- package/package.json +55 -11
- package/scripts/benchmark-ingest.mjs +175 -73
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +22 -0
- package/src/component/_generated/component.ts +381 -4
- package/src/component/_generated/server.ts +4 -0
- package/src/component/affiliates.ts +20 -5
- package/src/component/cardinality.ts +117 -0
- package/src/component/constants.ts +44 -1
- package/src/component/convex.config.ts +11 -1
- package/src/component/coverage.ts +71 -0
- package/src/component/diagnostics.ts +65 -0
- package/src/component/errors.ts +2 -1
- package/src/component/eventStore.ts +217 -193
- package/src/component/funnels.ts +19 -16
- package/src/component/geo.ts +835 -0
- package/src/component/goals.ts +29 -21
- package/src/component/guards.ts +3 -1
- package/src/component/http.ts +404 -70
- package/src/component/identity.ts +74 -0
- package/src/component/ingest.ts +894 -188
- package/src/component/live.ts +13 -7
- package/src/component/localTime.ts +167 -0
- package/src/component/reports.ts +1872 -197
- package/src/component/retention.ts +788 -96
- package/src/component/rollupStore.ts +799 -0
- package/src/component/rollups.ts +82 -0
- package/src/component/sanitize.ts +144 -29
- package/src/component/schema.ts +217 -21
- package/src/component/sites.ts +59 -12
- package/src/component/useragent.ts +171 -0
- package/src/component/validators.ts +49 -14
- package/src/component/visitors.ts +116 -0
- package/src/component/vitals.ts +146 -0
package/dist/component/ingest.js
CHANGED
|
@@ -1,11 +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_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, SECONDARY_AGGREGATE_SHARDS, SITE_INGEST_SHARDS, } from "./constants.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, MAX_GOALS_PER_SITE, RATE_LIMIT_WINDOW_MS, REPORT_ROLLUP_SHARDS, SECONDARY_AGGREGATE_SHARDS, SITE_INGEST_SHARDS, } from "./constants.js";
|
|
5
|
+
import { addVisitor, createVisitorSketch, unionVisitorSketches, } from "./cardinality.js";
|
|
6
|
+
import { addToVitalHistogram, emptyVitalHistogram, vitalDevice, VITAL_ALL, } from "./vitals.js";
|
|
5
7
|
import { fail } from "./errors.js";
|
|
8
|
+
import { ensureAnalyticsControl } from "./coverage.js";
|
|
6
9
|
import { bufferEventBatch, eventBatchKey, eventBucketStart, findTelemetry, flushEventBatches, insertEventBatch, inspectTelemetryMinute, } from "./eventStore.js";
|
|
7
|
-
import { cleanString,
|
|
10
|
+
import { cleanString, isValidMoney, measureBatchBytes, normalizeRoute, originAllowed, sanitizeContext, sanitizeCurrency, sanitizeEvent, sanitizeOpaqueId, sanitizePath, sanitizeProperties, sanitizeSlug, sourceFromReferrer, stableHash, } from "./sanitize.js";
|
|
8
11
|
import { ingestContextValidator, ingestResultValidator, trackerEventValidator, trustedConversionValidator, } from "./validators.js";
|
|
12
|
+
import { createReportRollupDeltas, enqueueReportRollups, reportRollupCoordinates, } from "./rollupStore.js";
|
|
13
|
+
import { localDayStart, localDayTimezone } from "./localTime.js";
|
|
9
14
|
export const ingestBatch = mutation({
|
|
10
15
|
args: {
|
|
11
16
|
siteId: v.id("sites"),
|
|
@@ -28,16 +33,20 @@ export const ingestBatch = mutation({
|
|
|
28
33
|
limit: MAX_BATCH_EVENTS,
|
|
29
34
|
});
|
|
30
35
|
}
|
|
36
|
+
let eventByteCounts;
|
|
31
37
|
try {
|
|
32
|
-
|
|
38
|
+
({ eventByteCounts } = measureBatchBytes(String(args.siteId), args.events));
|
|
33
39
|
}
|
|
34
40
|
catch {
|
|
35
41
|
fail("BATCH_TOO_LARGE", "batch exceeds 64 KiB", { limitBytes: 65_536 });
|
|
36
42
|
}
|
|
37
43
|
const now = Date.now();
|
|
38
|
-
|
|
44
|
+
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
45
|
+
await consumeSiteRateLimit(ctx, args.siteId, args.events, eventByteCounts, now);
|
|
39
46
|
const context = sanitizeContext(args.context);
|
|
47
|
+
const timezone = localDayTimezone(site.timezone);
|
|
40
48
|
const aggregateDeltas = new Map();
|
|
49
|
+
const reportRollupDeltas = createReportRollupDeltas();
|
|
41
50
|
const sessionRateWindows = new Map();
|
|
42
51
|
const batchState = {
|
|
43
52
|
sessions: new Map(),
|
|
@@ -71,7 +80,9 @@ export const ingestBatch = mutation({
|
|
|
71
80
|
const bucketStart = eventBucketStart(event.timestamp);
|
|
72
81
|
const groupKey = eventBatchKey(event.sessionId, bucketStart);
|
|
73
82
|
requestGroups.add(groupKey);
|
|
74
|
-
if (event.type === "heartbeat"
|
|
83
|
+
if (event.type === "heartbeat" ||
|
|
84
|
+
event.type === "vital" ||
|
|
85
|
+
event.type === "leave")
|
|
75
86
|
continue;
|
|
76
87
|
const eventIds = eventIdsByGroup.get(groupKey) ?? new Set();
|
|
77
88
|
if (eventIds.has(event.eventId)) {
|
|
@@ -97,7 +108,6 @@ export const ingestBatch = mutation({
|
|
|
97
108
|
siteId: args.siteId,
|
|
98
109
|
sessionId: group.sessionId,
|
|
99
110
|
bucketStart: group.bucketStart,
|
|
100
|
-
candidateEventIds: group.eventIds,
|
|
101
111
|
});
|
|
102
112
|
const newCount = [...group.eventIds].filter((eventId) => !eventIds.has(eventId)).length;
|
|
103
113
|
if (newCount > 0 &&
|
|
@@ -136,7 +146,52 @@ export const ingestBatch = mutation({
|
|
|
136
146
|
rejected += 1;
|
|
137
147
|
continue;
|
|
138
148
|
}
|
|
139
|
-
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);
|
|
154
|
+
accepted += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (event.type === "leave") {
|
|
158
|
+
// A departure only shortens live presence: it stores no telemetry,
|
|
159
|
+
// touches no aggregates, and never extends anything, so a replayed
|
|
160
|
+
// or late leave is inert. The read side filters by expiresAt and the
|
|
161
|
+
// sweep deletes the row, so the dashboard sees the exit in seconds.
|
|
162
|
+
if (!existingSession) {
|
|
163
|
+
rejected += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!(await consumeRateLimit(ctx, args.siteId, event.sessionId, eventByteCounts[eventIndex], now, sessionRateWindows))) {
|
|
167
|
+
rejected += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const liveState = await loadLiveSession(ctx, args.siteId, event.sessionId, batchState);
|
|
171
|
+
const liveSession = liveState.current;
|
|
172
|
+
if (liveSession) {
|
|
173
|
+
const expiresAt = Math.min(liveSession.expiresAt, now + LIVE_LEAVE_GRACE_MS);
|
|
174
|
+
if (expiresAt < liveSession.expiresAt) {
|
|
175
|
+
await ctx.db.patch("liveSessions", liveSession._id, { expiresAt });
|
|
176
|
+
liveState.current = { ...liveSession, expiresAt };
|
|
177
|
+
}
|
|
178
|
+
await ensureLiveSweep(ctx, args.siteId, now, batchState);
|
|
179
|
+
}
|
|
180
|
+
accepted += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (event.type === "vital") {
|
|
184
|
+
// A vital annotates telemetry its session already produced; it never
|
|
185
|
+
// creates sessions, live presence, event batches, or aggregate counts.
|
|
186
|
+
if (!existingSession) {
|
|
187
|
+
rejected += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!(await consumeRateLimit(ctx, args.siteId, event.sessionId, eventByteCounts[eventIndex], now, sessionRateWindows))) {
|
|
191
|
+
rejected += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
foldVitalRollups(reportRollupDeltas, args.siteId, event, vitalDevice(context.device));
|
|
140
195
|
accepted += 1;
|
|
141
196
|
continue;
|
|
142
197
|
}
|
|
@@ -164,26 +219,29 @@ export const ingestBatch = mutation({
|
|
|
164
219
|
.withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
|
|
165
220
|
.take(MAX_FUNNELS_PER_SITE);
|
|
166
221
|
}
|
|
167
|
-
await accountEvent(ctx, {
|
|
222
|
+
const session = await accountEvent(ctx, {
|
|
168
223
|
siteId: args.siteId,
|
|
169
224
|
event,
|
|
170
225
|
context,
|
|
171
226
|
existingSession,
|
|
172
|
-
goals: canMatchDefinition ? goals ?? [] : [],
|
|
173
|
-
funnels: canMatchDefinition ? funnels ?? [] : [],
|
|
227
|
+
goals: canMatchDefinition ? (goals ?? []) : [],
|
|
228
|
+
funnels: canMatchDefinition ? (funnels ?? []) : [],
|
|
174
229
|
aggregateDeltas,
|
|
230
|
+
reportRollupDeltas,
|
|
175
231
|
recordTelemetry: true,
|
|
232
|
+
timezone,
|
|
176
233
|
financialConversion: false,
|
|
177
234
|
trustedGoalKey: undefined,
|
|
178
235
|
now,
|
|
179
236
|
batchState,
|
|
180
237
|
});
|
|
181
|
-
bufferEventBatch(args.siteId, event, batchState.eventBatches);
|
|
238
|
+
bufferEventBatch(args.siteId, event, batchState.eventBatches, session.country ?? "unknown");
|
|
182
239
|
accepted += 1;
|
|
183
240
|
}
|
|
184
241
|
await flushBatchState(ctx, batchState);
|
|
185
242
|
await flushSessionRateLimits(ctx, sessionRateWindows, now);
|
|
186
243
|
await flushAggregates(ctx, aggregateDeltas);
|
|
244
|
+
await enqueueReportRollups(ctx, args.siteId, reportRollupDeltas, now);
|
|
187
245
|
return { accepted, duplicates, rejected };
|
|
188
246
|
},
|
|
189
247
|
});
|
|
@@ -261,7 +319,8 @@ export const trackConversion = mutation({
|
|
|
261
319
|
device: existingTelemetry?.device ?? existingSession?.device,
|
|
262
320
|
};
|
|
263
321
|
const aggregateDeltas = new Map();
|
|
264
|
-
|
|
322
|
+
const reportRollupDeltas = createReportRollupDeltas();
|
|
323
|
+
const session = await accountEvent(ctx, {
|
|
265
324
|
siteId: args.siteId,
|
|
266
325
|
event,
|
|
267
326
|
context: conversionContext,
|
|
@@ -269,15 +328,20 @@ export const trackConversion = mutation({
|
|
|
269
328
|
goals: [],
|
|
270
329
|
funnels: [],
|
|
271
330
|
aggregateDeltas,
|
|
331
|
+
reportRollupDeltas,
|
|
272
332
|
recordTelemetry: existingTelemetry === null && !telemetry.saturated,
|
|
333
|
+
timezone: localDayTimezone(site.timezone),
|
|
273
334
|
financialConversion: true,
|
|
274
335
|
trustedGoalKey,
|
|
275
336
|
now: Date.now(),
|
|
276
337
|
});
|
|
277
338
|
if (!existingTelemetry && !telemetry.saturated) {
|
|
278
|
-
await insertEventBatch(ctx, args.siteId, event);
|
|
339
|
+
await insertEventBatch(ctx, args.siteId, event, session.country ?? "unknown");
|
|
279
340
|
}
|
|
280
341
|
await flushAggregates(ctx, aggregateDeltas);
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
await enqueueReportRollups(ctx, args.siteId, reportRollupDeltas, now);
|
|
344
|
+
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
281
345
|
return { accepted: 1, duplicates: 0, rejected: 0 };
|
|
282
346
|
},
|
|
283
347
|
});
|
|
@@ -347,8 +411,13 @@ function withoutSystemFields(document) {
|
|
|
347
411
|
async function accountEvent(ctx, input) {
|
|
348
412
|
const { event } = input;
|
|
349
413
|
const session = input.recordTelemetry
|
|
350
|
-
? await upsertSession(ctx, input.siteId, event, input.context, input.existingSession, input.financialConversion, input.batchState)
|
|
414
|
+
? await upsertSession(ctx, input.siteId, event, input.context, input.existingSession, input.financialConversion, input.reportRollupDeltas, input.batchState)
|
|
351
415
|
: await accountTrustedSession(ctx, input.existingSession, event);
|
|
416
|
+
if (input.recordTelemetry && event.type === "pageview") {
|
|
417
|
+
// `existingSession` is the snapshot from before `upsertSession`, so it
|
|
418
|
+
// still carries the route the visitor is leaving.
|
|
419
|
+
foldRouteRollups(input.reportRollupDeltas, input.siteId, event, session, input.existingSession);
|
|
420
|
+
}
|
|
352
421
|
const matchedGoalKeys = [];
|
|
353
422
|
const matchingGoals = input.goals.filter((goal) => goal.active && goalMatches(goal, event));
|
|
354
423
|
if (matchingGoals.length > 1) {
|
|
@@ -366,8 +435,7 @@ async function accountEvent(ctx, input) {
|
|
|
366
435
|
.eq("siteId", input.siteId)
|
|
367
436
|
.eq("goalId", goal._id)
|
|
368
437
|
.eq("eventId", completionEventId))
|
|
369
|
-
.take(2))
|
|
370
|
-
.find((completion) => completion.idempotencyVersion === 2);
|
|
438
|
+
.take(2)).find((completion) => completion.idempotencyVersion === 2);
|
|
371
439
|
const legacyPrior = scopedPrior
|
|
372
440
|
? undefined
|
|
373
441
|
: (await ctx.db
|
|
@@ -376,8 +444,7 @@ async function accountEvent(ctx, input) {
|
|
|
376
444
|
.eq("siteId", input.siteId)
|
|
377
445
|
.eq("goalId", goal._id)
|
|
378
446
|
.eq("eventId", event.eventId))
|
|
379
|
-
.take(2))
|
|
380
|
-
.find((completion) => completion.idempotencyVersion === undefined &&
|
|
447
|
+
.take(2)).find((completion) => completion.idempotencyVersion === undefined &&
|
|
381
448
|
completion.sessionId === event.sessionId &&
|
|
382
449
|
eventBucketStart(completion.timestamp) ===
|
|
383
450
|
eventBucketStart(event.timestamp));
|
|
@@ -394,33 +461,39 @@ async function accountEvent(ctx, input) {
|
|
|
394
461
|
timestamp: event.timestamp,
|
|
395
462
|
valueCents: goal.valueCents ?? 0,
|
|
396
463
|
});
|
|
464
|
+
foldGoalRollup(input.reportRollupDeltas, input.siteId, event, goal);
|
|
397
465
|
matchedGoalKeys.push(goal.key);
|
|
398
466
|
}
|
|
399
467
|
for (const funnel of input.funnels) {
|
|
400
468
|
if (funnel.active) {
|
|
401
|
-
await advanceFunnel(ctx, input.siteId, funnel, event, input.batchState);
|
|
469
|
+
await advanceFunnel(ctx, input.siteId, funnel, event, input.reportRollupDeltas, input.batchState);
|
|
402
470
|
}
|
|
403
471
|
}
|
|
404
472
|
if (input.financialConversion) {
|
|
405
|
-
await accountConversion(ctx, input.siteId, session, event, input.trustedGoalKey);
|
|
473
|
+
await accountConversion(ctx, input.siteId, session, event, input.trustedGoalKey, input.reportRollupDeltas);
|
|
406
474
|
}
|
|
475
|
+
const campaignDimensions = session.utmCampaign
|
|
476
|
+
? [{ type: "campaign", value: session.utmCampaign }]
|
|
477
|
+
: [];
|
|
407
478
|
const dimensions = [
|
|
408
479
|
{ type: "source", value: session.source },
|
|
480
|
+
...campaignDimensions,
|
|
409
481
|
{ type: "page", value: event.path },
|
|
410
|
-
{ type: "country", value:
|
|
482
|
+
{ type: "country", value: session.country ?? "unknown" },
|
|
411
483
|
{ type: "device", value: input.context.device ?? "unknown" },
|
|
412
484
|
{ type: "browser", value: input.context.browser ?? "Other" },
|
|
413
485
|
{
|
|
414
486
|
type: "event",
|
|
415
|
-
value: event.type === "custom" ? event.name ?? "custom" : event.type,
|
|
487
|
+
value: event.type === "custom" ? (event.name ?? "custom") : event.type,
|
|
416
488
|
},
|
|
417
489
|
...matchedGoalKeys.map((value) => ({ type: "goal", value })),
|
|
418
490
|
];
|
|
419
|
-
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);
|
|
420
492
|
if (input.recordTelemetry &&
|
|
421
493
|
event.timestamp >= input.now - LIVE_SESSION_TTL_MS) {
|
|
422
494
|
await updateLiveSession(ctx, input.siteId, event, input.context, session, input.now, input.batchState);
|
|
423
495
|
}
|
|
496
|
+
return session;
|
|
424
497
|
}
|
|
425
498
|
async function accountTrustedSession(ctx, existing, event) {
|
|
426
499
|
if (!existing) {
|
|
@@ -436,8 +509,17 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
436
509
|
isNewSession: false,
|
|
437
510
|
isNewVisitorHour: false,
|
|
438
511
|
isNewVisitorDay: false,
|
|
512
|
+
isNewVisitor: false,
|
|
513
|
+
durationDeltaMs: 0,
|
|
514
|
+
continued: false,
|
|
439
515
|
startedAt: existing.startedAt,
|
|
440
516
|
source: existing.source,
|
|
517
|
+
utmCampaign: existing.utmCampaign,
|
|
518
|
+
visitorKey: existing.visitorKey ?? existing.visitorId,
|
|
519
|
+
country: existing.country,
|
|
520
|
+
city: existing.city,
|
|
521
|
+
latitude: existing.latitude,
|
|
522
|
+
longitude: existing.longitude,
|
|
441
523
|
pageviewCount: existing.pageviewCount,
|
|
442
524
|
eventCount: existing.eventCount,
|
|
443
525
|
affiliateId: existing.affiliateId,
|
|
@@ -446,13 +528,23 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
446
528
|
affiliateExpiresAt: existing.affiliateExpiresAt,
|
|
447
529
|
};
|
|
448
530
|
}
|
|
449
|
-
async function upsertSession(ctx, siteId, event, context, existing, financialConversion, batchState) {
|
|
450
|
-
|
|
531
|
+
async function upsertSession(ctx, siteId, event, context, existing, financialConversion, reportRollupDeltas, batchState) {
|
|
532
|
+
// A host-supplied id is the identity; anonymous sessions count through the
|
|
533
|
+
// daily key derived by the HTTP action, or once per session without one.
|
|
534
|
+
const visitorKey = existing?.visitorKey ??
|
|
535
|
+
(event.visitorId !== event.sessionId
|
|
536
|
+
? event.visitorId
|
|
537
|
+
: (context.visitorKey ?? event.sessionId));
|
|
451
538
|
const utmSource = stringProperty(event.properties, "utm_source");
|
|
539
|
+
// Campaign traffic names its own source; the referrer host is the fallback.
|
|
540
|
+
const source = utmSource?.toLowerCase() ?? sourceFromReferrer(event.referrer);
|
|
452
541
|
const utmMedium = stringProperty(event.properties, "utm_medium");
|
|
453
542
|
const utmCampaign = stringProperty(event.properties, "utm_campaign");
|
|
454
543
|
const attribution = await attributionForEvent(ctx, siteId, event, existing, batchState);
|
|
455
544
|
if (existing) {
|
|
545
|
+
if (attribution && existing.affiliateId !== attribution.affiliateId) {
|
|
546
|
+
foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution, visitorKey);
|
|
547
|
+
}
|
|
456
548
|
const startedAt = Math.min(existing.startedAt, event.timestamp);
|
|
457
549
|
const lastSeenAt = Math.max(existing.lastSeenAt, event.timestamp);
|
|
458
550
|
const updates = {
|
|
@@ -462,10 +554,15 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
462
554
|
exitPath: event.timestamp >= existing.lastSeenAt ? event.path : existing.exitPath,
|
|
463
555
|
eventCount: existing.eventCount + 1,
|
|
464
556
|
pageviewCount: existing.pageviewCount + (event.type === "pageview" ? 1 : 0),
|
|
557
|
+
...(event.type === "pageview"
|
|
558
|
+
? {
|
|
559
|
+
lastPageviewAt: Math.max(existing.lastPageviewAt ?? 0, event.timestamp),
|
|
560
|
+
}
|
|
561
|
+
: {}),
|
|
465
562
|
durationMs: Math.max(0, lastSeenAt - startedAt),
|
|
466
563
|
conversionCount: existing.conversionCount + Number(financialConversion),
|
|
467
564
|
revenueCents: existing.revenueCents +
|
|
468
|
-
(financialConversion ? event.revenueCents ?? 0 : 0),
|
|
565
|
+
(financialConversion ? (event.revenueCents ?? 0) : 0),
|
|
469
566
|
...(financialConversion && event.currency
|
|
470
567
|
? { currency: event.currency }
|
|
471
568
|
: {}),
|
|
@@ -476,7 +573,9 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
476
573
|
...(context.country ? { country: context.country } : {}),
|
|
477
574
|
...(context.city ? { city: context.city } : {}),
|
|
478
575
|
...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
|
|
479
|
-
...(context.longitude !== undefined
|
|
576
|
+
...(context.longitude !== undefined
|
|
577
|
+
? { longitude: context.longitude }
|
|
578
|
+
: {}),
|
|
480
579
|
browser: context.browser ?? existing.browser,
|
|
481
580
|
os: context.os ?? existing.os,
|
|
482
581
|
device: context.device ?? existing.device,
|
|
@@ -497,8 +596,17 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
497
596
|
Math.floor(event.timestamp / HOUR_MS),
|
|
498
597
|
isNewVisitorDay: Math.floor(existing.lastSeenAt / DAY_MS) <
|
|
499
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,
|
|
500
602
|
startedAt,
|
|
501
603
|
source: existing.source,
|
|
604
|
+
utmCampaign: existing.utmCampaign,
|
|
605
|
+
visitorKey,
|
|
606
|
+
country: context.country ?? existing.country,
|
|
607
|
+
city: context.city ?? existing.city,
|
|
608
|
+
latitude: context.latitude ?? existing.latitude,
|
|
609
|
+
longitude: context.longitude ?? existing.longitude,
|
|
502
610
|
pageviewCount: existing.pageviewCount + (event.type === "pageview" ? 1 : 0),
|
|
503
611
|
eventCount: existing.eventCount + 1,
|
|
504
612
|
...(attribution ?? {}),
|
|
@@ -520,10 +628,23 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
520
628
|
}
|
|
521
629
|
}
|
|
522
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
|
+
}
|
|
523
643
|
const fields = {
|
|
524
644
|
siteId,
|
|
525
645
|
sessionId: event.sessionId,
|
|
526
646
|
visitorId: event.visitorId,
|
|
647
|
+
visitorKey,
|
|
527
648
|
startedAt: event.timestamp,
|
|
528
649
|
lastSeenAt: event.timestamp,
|
|
529
650
|
entryPath: event.path,
|
|
@@ -536,21 +657,27 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
536
657
|
...(context.country ? { country: context.country } : {}),
|
|
537
658
|
...(context.city ? { city: context.city } : {}),
|
|
538
659
|
...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
|
|
539
|
-
...(context.longitude !== undefined
|
|
660
|
+
...(context.longitude !== undefined
|
|
661
|
+
? { longitude: context.longitude }
|
|
662
|
+
: {}),
|
|
540
663
|
browser: context.browser ?? "Other",
|
|
541
664
|
os: context.os ?? "Other",
|
|
542
665
|
device: context.device ?? "unknown",
|
|
543
666
|
eventCount: 1,
|
|
544
667
|
pageviewCount: event.type === "pageview" ? 1 : 0,
|
|
668
|
+
...(event.type === "pageview" ? { lastPageviewAt: event.timestamp } : {}),
|
|
545
669
|
durationMs: 0,
|
|
546
670
|
conversionCount: Number(financialConversion),
|
|
547
|
-
revenueCents: financialConversion ? event.revenueCents ?? 0 : 0,
|
|
671
|
+
revenueCents: financialConversion ? (event.revenueCents ?? 0) : 0,
|
|
548
672
|
...(financialConversion && event.currency
|
|
549
673
|
? { currency: event.currency }
|
|
550
674
|
: {}),
|
|
551
675
|
...(attribution ?? {}),
|
|
552
676
|
};
|
|
553
677
|
const sessionId = await ctx.db.insert("sessions", fields);
|
|
678
|
+
if (attribution) {
|
|
679
|
+
foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution, visitorKey);
|
|
680
|
+
}
|
|
554
681
|
if (batchState) {
|
|
555
682
|
const state = batchState.sessions.get(event.sessionId);
|
|
556
683
|
if (!state)
|
|
@@ -568,8 +695,17 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
568
695
|
isNewVisitorDay: priorVisit === null ||
|
|
569
696
|
Math.floor(priorVisit.lastSeenAt / DAY_MS) <
|
|
570
697
|
Math.floor(event.timestamp / DAY_MS),
|
|
698
|
+
isNewVisitor,
|
|
699
|
+
durationDeltaMs: 0,
|
|
700
|
+
continued: false,
|
|
571
701
|
startedAt: event.timestamp,
|
|
572
702
|
source,
|
|
703
|
+
utmCampaign,
|
|
704
|
+
visitorKey,
|
|
705
|
+
country: context.country,
|
|
706
|
+
city: context.city,
|
|
707
|
+
latitude: context.latitude,
|
|
708
|
+
longitude: context.longitude,
|
|
573
709
|
pageviewCount: event.type === "pageview" ? 1 : 0,
|
|
574
710
|
eventCount: 1,
|
|
575
711
|
...(attribution ?? {}),
|
|
@@ -689,14 +825,14 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
689
825
|
const updates = {
|
|
690
826
|
startedAt,
|
|
691
827
|
lastSeenAt,
|
|
692
|
-
exitPath: event.timestamp >= session.lastSeenAt
|
|
693
|
-
? event.path
|
|
694
|
-
: session.exitPath,
|
|
828
|
+
exitPath: event.timestamp >= session.lastSeenAt ? event.path : session.exitPath,
|
|
695
829
|
durationMs: Math.max(0, lastSeenAt - startedAt),
|
|
696
830
|
...(context.country ? { country: context.country } : {}),
|
|
697
831
|
...(context.city ? { city: context.city } : {}),
|
|
698
832
|
...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
|
|
699
|
-
...(context.longitude !== undefined
|
|
833
|
+
...(context.longitude !== undefined
|
|
834
|
+
? { longitude: context.longitude }
|
|
835
|
+
: {}),
|
|
700
836
|
browser: context.browser ?? session.browser,
|
|
701
837
|
os: context.os ?? session.os,
|
|
702
838
|
device: context.device ?? session.device,
|
|
@@ -711,26 +847,33 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
711
847
|
else {
|
|
712
848
|
await ctx.db.patch("sessions", session._id, updates);
|
|
713
849
|
}
|
|
850
|
+
const durationDeltaMs = Math.max(0, updates.durationMs - session.durationMs);
|
|
714
851
|
await updateLiveSession(ctx, siteId, event, context, {
|
|
715
852
|
isNewSession: false,
|
|
716
853
|
isNewVisitorHour: false,
|
|
717
854
|
isNewVisitorDay: false,
|
|
855
|
+
isNewVisitor: false,
|
|
856
|
+
durationDeltaMs: 0,
|
|
857
|
+
continued: false,
|
|
718
858
|
startedAt,
|
|
719
859
|
source: session.source,
|
|
860
|
+
visitorKey: session.visitorKey ?? session.visitorId,
|
|
861
|
+
country: context.country ?? session.country,
|
|
862
|
+
city: context.city ?? session.city,
|
|
863
|
+
latitude: context.latitude ?? session.latitude,
|
|
864
|
+
longitude: context.longitude ?? session.longitude,
|
|
720
865
|
pageviewCount: session.pageviewCount,
|
|
721
866
|
eventCount: session.eventCount,
|
|
722
867
|
affiliateSlug: session.affiliateSlug,
|
|
723
868
|
}, now, batchState);
|
|
869
|
+
return durationDeltaMs;
|
|
724
870
|
}
|
|
725
|
-
async function consumeSiteRateLimit(ctx, siteId, events, now) {
|
|
871
|
+
async function consumeSiteRateLimit(ctx, siteId, events, eventByteCounts, now) {
|
|
726
872
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
727
873
|
const increments = new Map();
|
|
728
|
-
const eventByteCounts = [];
|
|
729
|
-
const encoder = new TextEncoder();
|
|
730
874
|
for (let index = 0; index < events.length; index += 1) {
|
|
731
875
|
const event = events[index];
|
|
732
|
-
const byteCount =
|
|
733
|
-
eventByteCounts.push(byteCount);
|
|
876
|
+
const byteCount = eventByteCounts[index];
|
|
734
877
|
const shard = stableHash(event.sessionId) % SITE_INGEST_SHARDS;
|
|
735
878
|
const current = increments.get(shard) ?? { eventCount: 0, byteCount: 0 };
|
|
736
879
|
current.eventCount += 1;
|
|
@@ -745,9 +888,7 @@ async function consumeSiteRateLimit(ctx, siteId, events, now) {
|
|
|
745
888
|
.order("desc")
|
|
746
889
|
.take(1);
|
|
747
890
|
const storedWindow = rows[0] ?? null;
|
|
748
|
-
const window = storedWindow?.windowStart === windowStart
|
|
749
|
-
? storedWindow
|
|
750
|
-
: null;
|
|
891
|
+
const window = storedWindow?.windowStart === windowStart ? storedWindow : null;
|
|
751
892
|
if ((window?.eventCount ?? 0) + increment.eventCount >
|
|
752
893
|
MAX_SITE_EVENTS_PER_SHARD_WINDOW ||
|
|
753
894
|
(window?.byteCount ?? 0) + increment.byteCount >
|
|
@@ -787,7 +928,6 @@ async function consumeSiteRateLimit(ctx, siteId, events, now) {
|
|
|
787
928
|
});
|
|
788
929
|
}
|
|
789
930
|
}
|
|
790
|
-
return eventByteCounts;
|
|
791
931
|
}
|
|
792
932
|
async function consumeRateLimit(ctx, siteId, sessionId, byteCount, now, states) {
|
|
793
933
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
@@ -855,6 +995,10 @@ async function updateLiveSession(ctx, siteId, event, context, session, now, batc
|
|
|
855
995
|
.withIndex("by_siteId_and_sessionId", (range) => range.eq("siteId", siteId).eq("sessionId", event.sessionId))
|
|
856
996
|
.unique();
|
|
857
997
|
const deadline = now + LIVE_SESSION_TTL_MS;
|
|
998
|
+
const country = context.country ?? session.country;
|
|
999
|
+
const city = context.city ?? session.city;
|
|
1000
|
+
const latitude = context.latitude ?? session.latitude;
|
|
1001
|
+
const longitude = context.longitude ?? session.longitude;
|
|
858
1002
|
if (existing) {
|
|
859
1003
|
const recentHeartbeatEventIds = event.type === "heartbeat"
|
|
860
1004
|
? [
|
|
@@ -870,16 +1014,21 @@ async function updateLiveSession(ctx, siteId, event, context, session, now, batc
|
|
|
870
1014
|
path: event.path,
|
|
871
1015
|
lastSeenAt: Math.max(existing.lastSeenAt, event.timestamp),
|
|
872
1016
|
expiresAt: deadline,
|
|
873
|
-
...(
|
|
874
|
-
...(
|
|
875
|
-
...(
|
|
876
|
-
...(
|
|
1017
|
+
...(country ? { country } : {}),
|
|
1018
|
+
...(city ? { city } : {}),
|
|
1019
|
+
...(latitude !== undefined ? { latitude } : {}),
|
|
1020
|
+
...(longitude !== undefined ? { longitude } : {}),
|
|
877
1021
|
browser: context.browser ?? existing.browser,
|
|
878
1022
|
os: context.os ?? existing.os,
|
|
879
1023
|
source: session.source,
|
|
880
1024
|
device: context.device ?? existing.device,
|
|
881
1025
|
pageviewCount: session.pageviewCount,
|
|
882
1026
|
eventCount: session.eventCount,
|
|
1027
|
+
// The live map animates a visitor from the route they left to the one
|
|
1028
|
+
// they reached; a heartbeat or click on the same page is not a move.
|
|
1029
|
+
...(event.type === "pageview" && event.path !== existing.path
|
|
1030
|
+
? { previousPath: existing.path, movedAt: event.timestamp }
|
|
1031
|
+
: {}),
|
|
883
1032
|
...(event.type === "heartbeat"
|
|
884
1033
|
? {
|
|
885
1034
|
lastHeartbeatEventId: event.eventId,
|
|
@@ -904,10 +1053,10 @@ async function updateLiveSession(ctx, siteId, event, context, session, now, batc
|
|
|
904
1053
|
startedAt: event.timestamp,
|
|
905
1054
|
lastSeenAt: event.timestamp,
|
|
906
1055
|
expiresAt: deadline,
|
|
907
|
-
...(
|
|
908
|
-
...(
|
|
909
|
-
...(
|
|
910
|
-
...(
|
|
1056
|
+
...(country ? { country } : {}),
|
|
1057
|
+
...(city ? { city } : {}),
|
|
1058
|
+
...(latitude !== undefined ? { latitude } : {}),
|
|
1059
|
+
...(longitude !== undefined ? { longitude } : {}),
|
|
911
1060
|
browser: context.browser ?? "Other",
|
|
912
1061
|
os: context.os ?? "Other",
|
|
913
1062
|
source: session.source,
|
|
@@ -937,9 +1086,14 @@ async function ensureLiveSweep(ctx, siteId, now, batchState) {
|
|
|
937
1086
|
.query("liveSweepStates")
|
|
938
1087
|
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
939
1088
|
.unique();
|
|
940
|
-
if (!existing) {
|
|
1089
|
+
if (!existing || existing.scheduledAt <= now) {
|
|
941
1090
|
const scheduledAt = now + LIVE_SWEEP_INTERVAL_MS;
|
|
942
|
-
|
|
1091
|
+
if (existing) {
|
|
1092
|
+
await ctx.db.patch("liveSweepStates", existing._id, { scheduledAt });
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
await ctx.db.insert("liveSweepStates", { siteId, scheduledAt });
|
|
1096
|
+
}
|
|
943
1097
|
await ctx.scheduler.runAt(scheduledAt, internal.live.sweep, {
|
|
944
1098
|
siteId,
|
|
945
1099
|
scheduledAt,
|
|
@@ -953,7 +1107,7 @@ function goalMatches(goal, event) {
|
|
|
953
1107
|
}
|
|
954
1108
|
return event.type === "custom" && event.name === goal.matchValue;
|
|
955
1109
|
}
|
|
956
|
-
async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
1110
|
+
async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, batchState) {
|
|
957
1111
|
const cacheKey = JSON.stringify([funnel._id, event.visitorId]);
|
|
958
1112
|
let progressState = batchState?.funnelProgress.get(cacheKey);
|
|
959
1113
|
if (batchState && !progressState) {
|
|
@@ -996,10 +1150,12 @@ async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
|
996
1150
|
_id: progressId,
|
|
997
1151
|
};
|
|
998
1152
|
}
|
|
999
|
-
|
|
1153
|
+
foldFunnelStepDelta(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1154
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1000
1155
|
return;
|
|
1001
1156
|
}
|
|
1002
|
-
if (progress.completedAt !== undefined ||
|
|
1157
|
+
if (progress.completedAt !== undefined ||
|
|
1158
|
+
event.timestamp < progress.lastStepAt)
|
|
1003
1159
|
return;
|
|
1004
1160
|
if (event.timestamp - progress.startedAt > funnel.conversionWindowMs) {
|
|
1005
1161
|
if (!matchesFirst)
|
|
@@ -1025,7 +1181,8 @@ async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
|
1025
1181
|
completedAt: undefined,
|
|
1026
1182
|
});
|
|
1027
1183
|
}
|
|
1028
|
-
|
|
1184
|
+
foldFunnelStepDelta(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1185
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1029
1186
|
return;
|
|
1030
1187
|
}
|
|
1031
1188
|
const nextStep = funnel.steps[progress.currentStep];
|
|
@@ -1046,44 +1203,15 @@ async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
|
1046
1203
|
else {
|
|
1047
1204
|
await ctx.db.patch("funnelProgress", progress._id, updates);
|
|
1048
1205
|
}
|
|
1049
|
-
|
|
1206
|
+
foldFunnelStepDelta(reportRollupDeltas, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1207
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1050
1208
|
}
|
|
1051
1209
|
function stepMatches(step, event) {
|
|
1052
1210
|
return step.type === "pageview"
|
|
1053
1211
|
? event.type === "pageview" && event.path === step.value
|
|
1054
1212
|
: event.type === "custom" && event.name === step.value;
|
|
1055
1213
|
}
|
|
1056
|
-
async function
|
|
1057
|
-
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1058
|
-
const shard = stableHash(event.visitorId) % SECONDARY_AGGREGATE_SHARDS;
|
|
1059
|
-
const bucket = await ctx.db
|
|
1060
|
-
.query("funnelStepBuckets")
|
|
1061
|
-
.withIndex("by_siteId_and_funnelId_and_bucketStart_and_step_and_shard", (range) => range
|
|
1062
|
-
.eq("siteId", siteId)
|
|
1063
|
-
.eq("funnelId", funnelId)
|
|
1064
|
-
.eq("bucketStart", bucketStart)
|
|
1065
|
-
.eq("step", step)
|
|
1066
|
-
.eq("shard", shard))
|
|
1067
|
-
.unique();
|
|
1068
|
-
if (bucket) {
|
|
1069
|
-
await ctx.db.patch("funnelStepBuckets", bucket._id, {
|
|
1070
|
-
entrants: bucket.entrants + 1,
|
|
1071
|
-
completions: bucket.completions + (completed ? 1 : 0),
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
else {
|
|
1075
|
-
await ctx.db.insert("funnelStepBuckets", {
|
|
1076
|
-
siteId,
|
|
1077
|
-
funnelId,
|
|
1078
|
-
bucketStart,
|
|
1079
|
-
step,
|
|
1080
|
-
shard,
|
|
1081
|
-
entrants: 1,
|
|
1082
|
-
completions: completed ? 1 : 0,
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
async function accountConversion(ctx, siteId, session, event, trustedGoalKey) {
|
|
1214
|
+
async function accountConversion(ctx, siteId, session, event, trustedGoalKey, reportRollupDeltas) {
|
|
1087
1215
|
const revenueCents = event.revenueCents ?? 0;
|
|
1088
1216
|
const currency = event.currency ?? "USD";
|
|
1089
1217
|
const candidate = attributionSnapshot(session);
|
|
@@ -1112,6 +1240,7 @@ async function accountConversion(ctx, siteId, session, event, trustedGoalKey) {
|
|
|
1112
1240
|
commissionCents,
|
|
1113
1241
|
});
|
|
1114
1242
|
if (commissionAffiliate && attribution) {
|
|
1243
|
+
foldAffiliateConversionRollup(reportRollupDeltas, siteId, event, commissionAffiliate, attribution.affiliateSlug, revenueCents, commissionCents);
|
|
1115
1244
|
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1116
1245
|
const shard = stableHash(event.sessionId) % SECONDARY_AGGREGATE_SHARDS;
|
|
1117
1246
|
const stats = await ctx.db
|
|
@@ -1143,18 +1272,200 @@ async function accountConversion(ctx, siteId, session, event, trustedGoalKey) {
|
|
|
1143
1272
|
}
|
|
1144
1273
|
}
|
|
1145
1274
|
}
|
|
1146
|
-
function
|
|
1147
|
-
const
|
|
1275
|
+
function foldGoalRollup(deltas, siteId, event, goal) {
|
|
1276
|
+
const coordinates = reportRollupCoordinates(siteId, String(goal._id), event.timestamp, event.visitorId);
|
|
1277
|
+
const current = deltas.goals.get(coordinates.key) ?? {
|
|
1278
|
+
siteId,
|
|
1279
|
+
goalId: goal._id,
|
|
1280
|
+
bucketStart: coordinates.bucketStart,
|
|
1281
|
+
shard: coordinates.shard,
|
|
1282
|
+
goalKey: goal.key,
|
|
1283
|
+
completions: 0,
|
|
1284
|
+
valueCents: 0,
|
|
1285
|
+
};
|
|
1286
|
+
current.completions += 1;
|
|
1287
|
+
current.valueCents += goal.valueCents ?? 0;
|
|
1288
|
+
deltas.goals.set(coordinates.key, current);
|
|
1289
|
+
}
|
|
1290
|
+
function foldFunnelRollup(deltas, siteId, funnelId, event, step, completed) {
|
|
1291
|
+
const coordinates = reportRollupCoordinates(siteId, String(funnelId), event.timestamp, event.visitorId);
|
|
1292
|
+
const rollup = deltas.funnels.get(coordinates.key) ?? {
|
|
1293
|
+
siteId,
|
|
1294
|
+
funnelId,
|
|
1295
|
+
bucketStart: coordinates.bucketStart,
|
|
1296
|
+
shard: coordinates.shard,
|
|
1297
|
+
steps: new Map(),
|
|
1298
|
+
};
|
|
1299
|
+
const current = rollup.steps.get(step) ?? {
|
|
1300
|
+
step,
|
|
1301
|
+
entrants: 0,
|
|
1302
|
+
completions: 0,
|
|
1303
|
+
};
|
|
1304
|
+
current.entrants += 1;
|
|
1305
|
+
current.completions += Number(completed);
|
|
1306
|
+
rollup.steps.set(step, current);
|
|
1307
|
+
deltas.funnels.set(coordinates.key, rollup);
|
|
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
|
+
}
|
|
1326
|
+
function foldAffiliateVisitRollup(deltas, siteId, event, attribution, visitorKey) {
|
|
1327
|
+
const coordinates = reportRollupCoordinates(siteId, String(attribution.affiliateId), event.timestamp, event.visitorId);
|
|
1328
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1329
|
+
siteId,
|
|
1330
|
+
affiliateId: attribution.affiliateId,
|
|
1331
|
+
bucketStart: coordinates.bucketStart,
|
|
1332
|
+
shard: coordinates.shard,
|
|
1333
|
+
affiliateSlug: attribution.affiliateSlug,
|
|
1334
|
+
visits: 0,
|
|
1335
|
+
visitorKeys: new Set(),
|
|
1336
|
+
conversions: 0,
|
|
1337
|
+
revenueCents: 0,
|
|
1338
|
+
commissionCents: 0,
|
|
1339
|
+
};
|
|
1340
|
+
current.visits += 1;
|
|
1341
|
+
current.visitorKeys.add(visitorKey);
|
|
1342
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1343
|
+
}
|
|
1344
|
+
function foldAffiliateConversionRollup(deltas, siteId, event, affiliate, affiliateSlug, revenueCents, commissionCents) {
|
|
1345
|
+
const coordinates = reportRollupCoordinates(siteId, String(affiliate._id), event.timestamp, event.visitorId);
|
|
1346
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1347
|
+
siteId,
|
|
1348
|
+
affiliateId: affiliate._id,
|
|
1349
|
+
bucketStart: coordinates.bucketStart,
|
|
1350
|
+
shard: coordinates.shard,
|
|
1351
|
+
affiliateSlug,
|
|
1352
|
+
visits: 0,
|
|
1353
|
+
visitorKeys: new Set(),
|
|
1354
|
+
conversions: 0,
|
|
1355
|
+
revenueCents: 0,
|
|
1356
|
+
commissionCents: 0,
|
|
1357
|
+
};
|
|
1358
|
+
current.conversions += 1;
|
|
1359
|
+
current.revenueCents += revenueCents;
|
|
1360
|
+
current.commissionCents += commissionCents;
|
|
1361
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1362
|
+
}
|
|
1363
|
+
function foldVitalRollups(deltas, siteId, event, device) {
|
|
1364
|
+
const metric = event.name;
|
|
1365
|
+
const value = event.value ?? 0;
|
|
1366
|
+
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1367
|
+
const shard = stableHash(event.sessionId) % REPORT_ROLLUP_SHARDS;
|
|
1368
|
+
// Each measurement lands twice: on its page (across devices) and on its
|
|
1369
|
+
// device class (across pages), so both breakdowns stay exact and bounded.
|
|
1370
|
+
for (const target of [
|
|
1371
|
+
{ device: VITAL_ALL, page: event.path },
|
|
1372
|
+
{ device, page: VITAL_ALL },
|
|
1373
|
+
]) {
|
|
1374
|
+
const key = JSON.stringify([
|
|
1375
|
+
siteId,
|
|
1376
|
+
bucketStart,
|
|
1377
|
+
target.device,
|
|
1378
|
+
target.page,
|
|
1379
|
+
shard,
|
|
1380
|
+
]);
|
|
1381
|
+
const row = deltas.vitals.get(key) ?? {
|
|
1382
|
+
siteId,
|
|
1383
|
+
bucketStart,
|
|
1384
|
+
device: target.device,
|
|
1385
|
+
page: target.page,
|
|
1386
|
+
shard,
|
|
1387
|
+
metrics: new Map(),
|
|
1388
|
+
};
|
|
1389
|
+
const entry = row.metrics.get(metric) ?? {
|
|
1390
|
+
count: 0,
|
|
1391
|
+
sum: 0,
|
|
1392
|
+
histogram: emptyVitalHistogram(),
|
|
1393
|
+
};
|
|
1394
|
+
entry.count += 1;
|
|
1395
|
+
entry.sum += value;
|
|
1396
|
+
entry.histogram = addToVitalHistogram(entry.histogram, value);
|
|
1397
|
+
row.metrics.set(metric, entry);
|
|
1398
|
+
deltas.vitals.set(key, row);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function foldRouteDelta(deltas, siteId, route, timestamp, visitorId, apply) {
|
|
1402
|
+
const coordinates = reportRollupCoordinates(siteId, `route:${route}`, timestamp, visitorId);
|
|
1403
|
+
const current = deltas.routes.get(coordinates.key) ?? {
|
|
1404
|
+
siteId,
|
|
1405
|
+
bucketStart: coordinates.bucketStart,
|
|
1406
|
+
shard: coordinates.shard,
|
|
1407
|
+
route,
|
|
1408
|
+
pageviews: 0,
|
|
1409
|
+
entries: 0,
|
|
1410
|
+
continued: 0,
|
|
1411
|
+
dwellMs: 0,
|
|
1412
|
+
dwellSamples: 0,
|
|
1413
|
+
visitorKeys: new Set(),
|
|
1414
|
+
outgoing: new Map(),
|
|
1415
|
+
};
|
|
1416
|
+
apply(current);
|
|
1417
|
+
deltas.routes.set(coordinates.key, current);
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* One pageview feeds the site map three ways: the route it lands on gains a
|
|
1421
|
+
* view (and an entry when it opens the session), the route it leaves gains
|
|
1422
|
+
* the transition and the time spent there, and the session's entry route is
|
|
1423
|
+
* marked as continued the first time its visitor moves on — which is what
|
|
1424
|
+
* lets the report derive bounces and exits without a session-end hook.
|
|
1425
|
+
*/
|
|
1426
|
+
function foldRouteRollups(deltas, siteId, event, session, existing) {
|
|
1427
|
+
const route = normalizeRoute(event.path);
|
|
1428
|
+
const priorPageviews = existing?.pageviewCount ?? 0;
|
|
1429
|
+
const firstPageview = existing === null || priorPageviews === 0;
|
|
1430
|
+
foldRouteDelta(deltas, siteId, route, event.timestamp, event.visitorId, (delta) => {
|
|
1431
|
+
delta.pageviews += 1;
|
|
1432
|
+
if (firstPageview)
|
|
1433
|
+
delta.entries += 1;
|
|
1434
|
+
delta.visitorKeys.add(session.visitorKey);
|
|
1435
|
+
});
|
|
1436
|
+
// Out-of-order telemetry keeps its view but cannot say where it came from.
|
|
1437
|
+
if (existing === null || firstPageview)
|
|
1438
|
+
return;
|
|
1439
|
+
if (event.timestamp < existing.lastSeenAt)
|
|
1440
|
+
return;
|
|
1441
|
+
const previous = normalizeRoute(existing.exitPath);
|
|
1442
|
+
const lastPageviewAt = existing.lastPageviewAt;
|
|
1443
|
+
foldRouteDelta(deltas, siteId, previous, event.timestamp, event.visitorId, (delta) => {
|
|
1444
|
+
delta.outgoing.set(route, (delta.outgoing.get(route) ?? 0) + 1);
|
|
1445
|
+
if (lastPageviewAt !== undefined && event.timestamp >= lastPageviewAt) {
|
|
1446
|
+
delta.dwellMs += event.timestamp - lastPageviewAt;
|
|
1447
|
+
delta.dwellSamples += 1;
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
if (priorPageviews === 1) {
|
|
1451
|
+
foldRouteDelta(deltas, siteId, normalizeRoute(existing.entryPath), existing.startedAt, event.visitorId, (delta) => {
|
|
1452
|
+
delta.continued += 1;
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, recordTelemetry, financialConversion, timezone) {
|
|
1457
|
+
const revenueCents = financialConversion ? (event.revenueCents ?? 0) : 0;
|
|
1148
1458
|
const dimensionDeltas = dimensions.map((dimension) => ({
|
|
1149
1459
|
...dimension,
|
|
1150
1460
|
count: Number(recordTelemetry),
|
|
1151
1461
|
revenueCents,
|
|
1462
|
+
pageviews: Number(recordTelemetry && event.type === "pageview"),
|
|
1463
|
+
sessions: Number(recordTelemetry && session.isNewSession),
|
|
1152
1464
|
}));
|
|
1153
|
-
for (const granularity of
|
|
1465
|
+
for (const granularity of aggregateGranularities(timezone)) {
|
|
1154
1466
|
const shard = stableHash(event.sessionId) %
|
|
1155
1467
|
(granularity === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
1156
|
-
const
|
|
1157
|
-
const bucketStart = Math.floor(event.timestamp / interval) * interval;
|
|
1468
|
+
const bucketStart = aggregateBucketStart(granularity, event.timestamp, timezone);
|
|
1158
1469
|
const key = `${siteId}:${granularity}:${bucketStart}:${shard}`;
|
|
1159
1470
|
let delta = aggregateDeltas.get(key);
|
|
1160
1471
|
if (!delta) {
|
|
@@ -1170,9 +1481,13 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1170
1481
|
outboundClicks: 0,
|
|
1171
1482
|
sessions: 0,
|
|
1172
1483
|
visitors: 0,
|
|
1484
|
+
visitorSketch: createVisitorSketch(),
|
|
1173
1485
|
conversions: 0,
|
|
1174
1486
|
revenueCents: 0,
|
|
1175
1487
|
dimensions: [],
|
|
1488
|
+
durationMs: 0,
|
|
1489
|
+
continuedSessions: 0,
|
|
1490
|
+
newVisitors: 0,
|
|
1176
1491
|
};
|
|
1177
1492
|
aggregateDeltas.set(key, delta);
|
|
1178
1493
|
}
|
|
@@ -1188,9 +1503,64 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1188
1503
|
? Number(session.isNewVisitorHour)
|
|
1189
1504
|
: Number(session.isNewVisitorDay)
|
|
1190
1505
|
: 0;
|
|
1506
|
+
if (recordTelemetry) {
|
|
1507
|
+
delta.visitorSketch = addVisitor(delta.visitorSketch, session.visitorKey);
|
|
1508
|
+
}
|
|
1191
1509
|
delta.conversions += Number(financialConversion);
|
|
1192
1510
|
delta.revenueCents += revenueCents;
|
|
1193
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);
|
|
1194
1564
|
}
|
|
1195
1565
|
}
|
|
1196
1566
|
async function flushAggregates(ctx, aggregateDeltas) {
|
|
@@ -1203,7 +1573,18 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1203
1573
|
.eq("bucketStart", delta.bucketStart)
|
|
1204
1574
|
.eq("shard", delta.shard))
|
|
1205
1575
|
.unique();
|
|
1206
|
-
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
|
+
})));
|
|
1581
|
+
const priorSketch = bucket?.visitorSketch;
|
|
1582
|
+
const visitorSketch = priorSketch
|
|
1583
|
+
? unionVisitorSketches(priorSketch, delta.visitorSketch)
|
|
1584
|
+
: delta.visitorSketch;
|
|
1585
|
+
const visitorSketchComplete = bucket
|
|
1586
|
+
? bucket.visitorSketchComplete === true && priorSketch !== undefined
|
|
1587
|
+
: true;
|
|
1207
1588
|
if (bucket) {
|
|
1208
1589
|
await ctx.db.patch("aggregateBuckets", bucket._id, {
|
|
1209
1590
|
events: bucket.events + delta.events,
|
|
@@ -1213,9 +1594,14 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1213
1594
|
outboundClicks: bucket.outboundClicks + delta.outboundClicks,
|
|
1214
1595
|
sessions: bucket.sessions + delta.sessions,
|
|
1215
1596
|
visitors: bucket.visitors + delta.visitors,
|
|
1597
|
+
visitorSketch,
|
|
1598
|
+
visitorSketchComplete,
|
|
1216
1599
|
conversions: bucket.conversions + delta.conversions,
|
|
1217
1600
|
revenueCents: bucket.revenueCents + delta.revenueCents,
|
|
1218
1601
|
dimensions,
|
|
1602
|
+
durationMs: (bucket.durationMs ?? 0) + delta.durationMs,
|
|
1603
|
+
continuedSessions: (bucket.continuedSessions ?? 0) + delta.continuedSessions,
|
|
1604
|
+
newVisitors: (bucket.newVisitors ?? 0) + delta.newVisitors,
|
|
1219
1605
|
});
|
|
1220
1606
|
}
|
|
1221
1607
|
else {
|
|
@@ -1231,9 +1617,14 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1231
1617
|
outboundClicks: delta.outboundClicks,
|
|
1232
1618
|
sessions: delta.sessions,
|
|
1233
1619
|
visitors: delta.visitors,
|
|
1620
|
+
visitorSketch,
|
|
1621
|
+
visitorSketchComplete,
|
|
1234
1622
|
conversions: delta.conversions,
|
|
1235
1623
|
revenueCents: delta.revenueCents,
|
|
1236
1624
|
dimensions,
|
|
1625
|
+
durationMs: delta.durationMs,
|
|
1626
|
+
continuedSessions: delta.continuedSessions,
|
|
1627
|
+
newVisitors: delta.newVisitors,
|
|
1237
1628
|
});
|
|
1238
1629
|
}
|
|
1239
1630
|
}
|
|
@@ -1245,6 +1636,8 @@ function mergeDimensions(current, additions) {
|
|
|
1245
1636
|
if (exact) {
|
|
1246
1637
|
exact.count += addition.count;
|
|
1247
1638
|
exact.revenueCents += addition.revenueCents;
|
|
1639
|
+
exact.pageviews = (exact.pageviews ?? 0) + addition.pageviews;
|
|
1640
|
+
exact.sessions = (exact.sessions ?? 0) + addition.sessions;
|
|
1248
1641
|
continue;
|
|
1249
1642
|
}
|
|
1250
1643
|
const typeSlots = result.filter((candidate) => candidate.type === addition.type);
|
|
@@ -1261,6 +1654,8 @@ function mergeDimensions(current, additions) {
|
|
|
1261
1654
|
value: addition.value,
|
|
1262
1655
|
count: addition.count,
|
|
1263
1656
|
revenueCents: addition.revenueCents,
|
|
1657
|
+
pageviews: addition.pageviews,
|
|
1658
|
+
sessions: addition.sessions,
|
|
1264
1659
|
});
|
|
1265
1660
|
continue;
|
|
1266
1661
|
}
|
|
@@ -1268,6 +1663,8 @@ function mergeDimensions(current, additions) {
|
|
|
1268
1663
|
if (overflow) {
|
|
1269
1664
|
overflow.count += addition.count;
|
|
1270
1665
|
overflow.revenueCents += addition.revenueCents;
|
|
1666
|
+
overflow.pageviews = (overflow.pageviews ?? 0) + addition.pageviews;
|
|
1667
|
+
overflow.sessions = (overflow.sessions ?? 0) + addition.sessions;
|
|
1271
1668
|
continue;
|
|
1272
1669
|
}
|
|
1273
1670
|
if (addition.count === 0 && addition.revenueCents === 0)
|
|
@@ -1283,6 +1680,8 @@ function mergeDimensions(current, additions) {
|
|
|
1283
1680
|
value: "(other)",
|
|
1284
1681
|
count: addition.count,
|
|
1285
1682
|
revenueCents: addition.revenueCents,
|
|
1683
|
+
pageviews: addition.pageviews,
|
|
1684
|
+
sessions: addition.sessions,
|
|
1286
1685
|
});
|
|
1287
1686
|
continue;
|
|
1288
1687
|
}
|
|
@@ -1294,11 +1693,15 @@ function mergeDimensions(current, additions) {
|
|
|
1294
1693
|
replacement.value = "(other)";
|
|
1295
1694
|
replacement.count += addition.count;
|
|
1296
1695
|
replacement.revenueCents += addition.revenueCents;
|
|
1696
|
+
replacement.pageviews = (replacement.pageviews ?? 0) + addition.pageviews;
|
|
1697
|
+
replacement.sessions = (replacement.sessions ?? 0) + addition.sessions;
|
|
1297
1698
|
}
|
|
1298
1699
|
return result;
|
|
1299
1700
|
}
|
|
1300
1701
|
function stringProperty(properties, key) {
|
|
1301
1702
|
const value = properties?.[key];
|
|
1302
|
-
return typeof value === "string"
|
|
1703
|
+
return typeof value === "string"
|
|
1704
|
+
? cleanString(value, 120) || undefined
|
|
1705
|
+
: undefined;
|
|
1303
1706
|
}
|
|
1304
1707
|
//# sourceMappingURL=ingest.js.map
|