@iann29/rastro 0.1.0-alpha.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +248 -58
- package/agent/integration.md +223 -65
- package/agent/manifest.json +34 -13
- package/agent/manifest.schema.json +53 -11
- package/dist/client/federation.d.ts +215 -14
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +233 -9
- package/dist/client/federation.js.map +1 -1
- package/dist/client/heat.d.ts +37 -0
- package/dist/client/heat.d.ts.map +1 -0
- package/dist/client/heat.js +43 -0
- package/dist/client/heat.js.map +1 -0
- package/dist/client/index.d.ts +443 -98
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +214 -21
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +6 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +50 -13
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +3 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +6 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +9 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +36 -6
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/geo.d.ts +2 -0
- package/dist/component/geo.d.ts.map +1 -1
- package/dist/component/geo.js +56 -19
- package/dist/component/geo.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +48 -27
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +223 -304
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/localTime.d.ts +25 -0
- package/dist/component/localTime.d.ts.map +1 -0
- package/dist/component/localTime.js +126 -0
- package/dist/component/localTime.js.map +1 -0
- package/dist/component/reports.d.ts +44 -13
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +193 -38
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.js +26 -8
- package/dist/component/retention.js.map +1 -1
- package/dist/component/rollupStore.d.ts +320 -0
- package/dist/component/rollupStore.d.ts.map +1 -0
- package/dist/component/rollupStore.js +596 -0
- package/dist/component/rollupStore.js.map +1 -0
- package/dist/component/rollups.d.ts +20 -0
- package/dist/component/rollups.d.ts.map +1 -0
- package/dist/component/rollups.js +73 -0
- package/dist/component/rollups.js.map +1 -0
- package/dist/component/sanitize.d.ts +5 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +15 -0
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +345 -11
- package/dist/component/schema.js +25 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts +12 -0
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +30 -0
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +34 -13
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +23 -2
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +3 -0
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +3 -0
- package/dist/tracker/generated.js.map +1 -1
- package/docs/federation-setup.md +165 -27
- package/docs/federation.md +170 -31
- package/docs/upgrading.md +181 -21
- package/llms.txt +8 -6
- package/package.json +2 -1
- package/src/component/_generated/api.ts +6 -0
- package/src/component/_generated/component.ts +47 -12
- package/src/component/constants.ts +6 -0
- package/src/component/coverage.ts +46 -6
- package/src/component/geo.ts +82 -28
- package/src/component/http.ts +61 -26
- package/src/component/ingest.ts +327 -439
- package/src/component/localTime.ts +167 -0
- package/src/component/reports.ts +261 -39
- package/src/component/retention.ts +25 -7
- package/src/component/rollupStore.ts +799 -0
- package/src/component/rollups.ts +82 -0
- package/src/component/sanitize.ts +14 -0
- package/src/component/schema.ts +35 -0
- package/src/component/sites.ts +32 -0
- package/src/component/validators.ts +27 -1
- package/src/tracker/generated.ts +12 -0
package/dist/component/ingest.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
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
|
-
import { cleanString, isValidMoney, measureBatchBytes, normalizeRoute, originAllowed, sanitizeContext, sanitizeCurrency, sanitizeEvent, sanitizeOpaqueId, sanitizePath, sanitizeProperties, sanitizeSlug, sourceFromReferrer, stableHash, } from "./sanitize.js";
|
|
10
|
+
import { cleanString, isValidMoney, measureBatchBytes, normalizeRoute, originAllowed, publicUrlHost, 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";
|
|
14
|
+
function furtherReach(current, next) {
|
|
15
|
+
if (!next)
|
|
16
|
+
return current;
|
|
17
|
+
if (!current)
|
|
18
|
+
return next;
|
|
19
|
+
return next.step / next.steps >= current.step / current.steps
|
|
20
|
+
? next
|
|
21
|
+
: current;
|
|
22
|
+
}
|
|
12
23
|
export const ingestBatch = mutation({
|
|
13
24
|
args: {
|
|
14
25
|
siteId: v.id("sites"),
|
|
@@ -42,6 +53,7 @@ export const ingestBatch = mutation({
|
|
|
42
53
|
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
43
54
|
await consumeSiteRateLimit(ctx, args.siteId, args.events, eventByteCounts, now);
|
|
44
55
|
const context = sanitizeContext(args.context);
|
|
56
|
+
const timezone = localDayTimezone(site.timezone);
|
|
45
57
|
const aggregateDeltas = new Map();
|
|
46
58
|
const reportRollupDeltas = createReportRollupDeltas();
|
|
47
59
|
const sessionRateWindows = new Map();
|
|
@@ -143,7 +155,11 @@ export const ingestBatch = mutation({
|
|
|
143
155
|
rejected += 1;
|
|
144
156
|
continue;
|
|
145
157
|
}
|
|
146
|
-
await accountHeartbeat(ctx, args.siteId, existingSession, event, context, now, batchState);
|
|
158
|
+
const durationDeltaMs = await accountHeartbeat(ctx, args.siteId, existingSession, event, context, now, batchState);
|
|
159
|
+
// A heartbeat is presence, not telemetry, but it is what keeps a
|
|
160
|
+
// session's duration honest while the visitor reads: the growth is
|
|
161
|
+
// folded into the buckets the heartbeat landed in.
|
|
162
|
+
foldSessionDuration(aggregateDeltas, args.siteId, event, durationDeltaMs, timezone);
|
|
147
163
|
accepted += 1;
|
|
148
164
|
continue;
|
|
149
165
|
}
|
|
@@ -222,6 +238,7 @@ export const ingestBatch = mutation({
|
|
|
222
238
|
aggregateDeltas,
|
|
223
239
|
reportRollupDeltas,
|
|
224
240
|
recordTelemetry: true,
|
|
241
|
+
timezone,
|
|
225
242
|
financialConversion: false,
|
|
226
243
|
trustedGoalKey: undefined,
|
|
227
244
|
now,
|
|
@@ -233,7 +250,7 @@ export const ingestBatch = mutation({
|
|
|
233
250
|
await flushBatchState(ctx, batchState);
|
|
234
251
|
await flushSessionRateLimits(ctx, sessionRateWindows, now);
|
|
235
252
|
await flushAggregates(ctx, aggregateDeltas);
|
|
236
|
-
await
|
|
253
|
+
await enqueueReportRollups(ctx, args.siteId, reportRollupDeltas, now);
|
|
237
254
|
return { accepted, duplicates, rejected };
|
|
238
255
|
},
|
|
239
256
|
});
|
|
@@ -322,6 +339,7 @@ export const trackConversion = mutation({
|
|
|
322
339
|
aggregateDeltas,
|
|
323
340
|
reportRollupDeltas,
|
|
324
341
|
recordTelemetry: existingTelemetry === null && !telemetry.saturated,
|
|
342
|
+
timezone: localDayTimezone(site.timezone),
|
|
325
343
|
financialConversion: true,
|
|
326
344
|
trustedGoalKey,
|
|
327
345
|
now: Date.now(),
|
|
@@ -330,8 +348,9 @@ export const trackConversion = mutation({
|
|
|
330
348
|
await insertEventBatch(ctx, args.siteId, event, session.country ?? "unknown");
|
|
331
349
|
}
|
|
332
350
|
await flushAggregates(ctx, aggregateDeltas);
|
|
333
|
-
|
|
334
|
-
await
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
await enqueueReportRollups(ctx, args.siteId, reportRollupDeltas, now);
|
|
353
|
+
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
335
354
|
return { accepted: 1, duplicates: 0, rejected: 0 };
|
|
336
355
|
},
|
|
337
356
|
});
|
|
@@ -454,20 +473,34 @@ async function accountEvent(ctx, input) {
|
|
|
454
473
|
foldGoalRollup(input.reportRollupDeltas, input.siteId, event, goal);
|
|
455
474
|
matchedGoalKeys.push(goal.key);
|
|
456
475
|
}
|
|
476
|
+
let reach;
|
|
457
477
|
for (const funnel of input.funnels) {
|
|
458
478
|
if (funnel.active) {
|
|
459
|
-
await advanceFunnel(ctx, input.siteId, funnel, event, input.reportRollupDeltas, input.batchState);
|
|
479
|
+
reach = furtherReach(reach, await advanceFunnel(ctx, input.siteId, funnel, event, input.reportRollupDeltas, input.batchState));
|
|
460
480
|
}
|
|
461
481
|
}
|
|
462
482
|
if (input.financialConversion) {
|
|
463
483
|
await accountConversion(ctx, input.siteId, session, event, input.trustedGoalKey, input.reportRollupDeltas);
|
|
464
484
|
}
|
|
465
|
-
const campaignDimensions =
|
|
466
|
-
|
|
467
|
-
:
|
|
485
|
+
const campaignDimensions = [];
|
|
486
|
+
if (session.utmCampaign) {
|
|
487
|
+
campaignDimensions.push({ type: "campaign", value: session.utmCampaign });
|
|
488
|
+
}
|
|
489
|
+
if (session.utmMedium) {
|
|
490
|
+
// A medium is a taxonomy ("social", "cpc", "email"), so its case is noise.
|
|
491
|
+
campaignDimensions.push({
|
|
492
|
+
type: "medium",
|
|
493
|
+
value: session.utmMedium.toLowerCase(),
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
// An outbound click ranks by where it went: the host alone, never the path.
|
|
497
|
+
const outboundHost = event.type === "outbound" ? publicUrlHost(event.href) : undefined;
|
|
468
498
|
const dimensions = [
|
|
469
499
|
{ type: "source", value: session.source },
|
|
470
500
|
...campaignDimensions,
|
|
501
|
+
...(outboundHost
|
|
502
|
+
? [{ type: "outbound", value: outboundHost }]
|
|
503
|
+
: []),
|
|
471
504
|
{ type: "page", value: event.path },
|
|
472
505
|
{ type: "country", value: session.country ?? "unknown" },
|
|
473
506
|
{ type: "device", value: input.context.device ?? "unknown" },
|
|
@@ -478,10 +511,10 @@ async function accountEvent(ctx, input) {
|
|
|
478
511
|
},
|
|
479
512
|
...matchedGoalKeys.map((value) => ({ type: "goal", value })),
|
|
480
513
|
];
|
|
481
|
-
foldAggregates(input.aggregateDeltas, input.siteId, event, session, dimensions, input.recordTelemetry, input.financialConversion);
|
|
514
|
+
foldAggregates(input.aggregateDeltas, input.siteId, event, session, dimensions, input.recordTelemetry, input.financialConversion, input.timezone);
|
|
482
515
|
if (input.recordTelemetry &&
|
|
483
516
|
event.timestamp >= input.now - LIVE_SESSION_TTL_MS) {
|
|
484
|
-
await updateLiveSession(ctx, input.siteId, event, input.context, session, input.now, input.batchState);
|
|
517
|
+
await updateLiveSession(ctx, input.siteId, event, input.context, session, input.now, input.batchState, reach);
|
|
485
518
|
}
|
|
486
519
|
return session;
|
|
487
520
|
}
|
|
@@ -499,9 +532,16 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
499
532
|
isNewSession: false,
|
|
500
533
|
isNewVisitorHour: false,
|
|
501
534
|
isNewVisitorDay: false,
|
|
535
|
+
isNewVisitor: false,
|
|
536
|
+
durationDeltaMs: 0,
|
|
537
|
+
continued: false,
|
|
502
538
|
startedAt: existing.startedAt,
|
|
503
539
|
source: existing.source,
|
|
504
540
|
utmCampaign: existing.utmCampaign,
|
|
541
|
+
utmMedium: existing.utmMedium,
|
|
542
|
+
entryPath: existing.entryPath,
|
|
543
|
+
conversionCount: existing.conversionCount + 1,
|
|
544
|
+
newVisitor: existing.newVisitor,
|
|
505
545
|
visitorKey: existing.visitorKey ?? existing.visitorId,
|
|
506
546
|
country: existing.country,
|
|
507
547
|
city: existing.city,
|
|
@@ -583,9 +623,16 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
583
623
|
Math.floor(event.timestamp / HOUR_MS),
|
|
584
624
|
isNewVisitorDay: Math.floor(existing.lastSeenAt / DAY_MS) <
|
|
585
625
|
Math.floor(event.timestamp / DAY_MS),
|
|
626
|
+
isNewVisitor: false,
|
|
627
|
+
durationDeltaMs: Math.max(0, updates.durationMs - existing.durationMs),
|
|
628
|
+
continued: event.type === "pageview" && existing.pageviewCount === 1,
|
|
586
629
|
startedAt,
|
|
587
630
|
source: existing.source,
|
|
588
631
|
utmCampaign: existing.utmCampaign,
|
|
632
|
+
utmMedium: existing.utmMedium,
|
|
633
|
+
entryPath: existing.entryPath,
|
|
634
|
+
conversionCount: updates.conversionCount,
|
|
635
|
+
newVisitor: existing.newVisitor,
|
|
589
636
|
visitorKey,
|
|
590
637
|
country: context.country ?? existing.country,
|
|
591
638
|
city: context.city ?? existing.city,
|
|
@@ -612,6 +659,18 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
612
659
|
}
|
|
613
660
|
}
|
|
614
661
|
}
|
|
662
|
+
// A visitor is new when nothing was ever recorded under their id. A host
|
|
663
|
+
// id whose anonymous history was linked at sign-up is a return, not a new
|
|
664
|
+
// person; a browser without a persistent id is new every session.
|
|
665
|
+
let isNewVisitor = priorVisit === null;
|
|
666
|
+
if (isNewVisitor && event.visitorId !== event.sessionId) {
|
|
667
|
+
const alias = await ctx.db
|
|
668
|
+
.query("visitorAliases")
|
|
669
|
+
.withIndex("by_siteId_and_visitorId", (range) => range.eq("siteId", siteId).eq("visitorId", event.visitorId))
|
|
670
|
+
.first();
|
|
671
|
+
if (alias)
|
|
672
|
+
isNewVisitor = false;
|
|
673
|
+
}
|
|
615
674
|
const fields = {
|
|
616
675
|
siteId,
|
|
617
676
|
sessionId: event.sessionId,
|
|
@@ -621,6 +680,7 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
621
680
|
lastSeenAt: event.timestamp,
|
|
622
681
|
entryPath: event.path,
|
|
623
682
|
exitPath: event.path,
|
|
683
|
+
newVisitor: isNewVisitor,
|
|
624
684
|
...(event.referrer ? { referrer: event.referrer } : {}),
|
|
625
685
|
source,
|
|
626
686
|
...(utmSource ? { utmSource } : {}),
|
|
@@ -667,9 +727,16 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
667
727
|
isNewVisitorDay: priorVisit === null ||
|
|
668
728
|
Math.floor(priorVisit.lastSeenAt / DAY_MS) <
|
|
669
729
|
Math.floor(event.timestamp / DAY_MS),
|
|
730
|
+
isNewVisitor,
|
|
731
|
+
durationDeltaMs: 0,
|
|
732
|
+
continued: false,
|
|
670
733
|
startedAt: event.timestamp,
|
|
671
734
|
source,
|
|
672
735
|
utmCampaign,
|
|
736
|
+
utmMedium,
|
|
737
|
+
entryPath: event.path,
|
|
738
|
+
conversionCount: Number(financialConversion),
|
|
739
|
+
newVisitor: isNewVisitor,
|
|
673
740
|
visitorKey,
|
|
674
741
|
country: context.country,
|
|
675
742
|
city: context.city,
|
|
@@ -816,10 +883,14 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
816
883
|
else {
|
|
817
884
|
await ctx.db.patch("sessions", session._id, updates);
|
|
818
885
|
}
|
|
886
|
+
const durationDeltaMs = Math.max(0, updates.durationMs - session.durationMs);
|
|
819
887
|
await updateLiveSession(ctx, siteId, event, context, {
|
|
820
888
|
isNewSession: false,
|
|
821
889
|
isNewVisitorHour: false,
|
|
822
890
|
isNewVisitorDay: false,
|
|
891
|
+
isNewVisitor: false,
|
|
892
|
+
durationDeltaMs: 0,
|
|
893
|
+
continued: false,
|
|
823
894
|
startedAt,
|
|
824
895
|
source: session.source,
|
|
825
896
|
visitorKey: session.visitorKey ?? session.visitorId,
|
|
@@ -830,7 +901,11 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
830
901
|
pageviewCount: session.pageviewCount,
|
|
831
902
|
eventCount: session.eventCount,
|
|
832
903
|
affiliateSlug: session.affiliateSlug,
|
|
904
|
+
entryPath: session.entryPath,
|
|
905
|
+
conversionCount: session.conversionCount,
|
|
906
|
+
newVisitor: session.newVisitor,
|
|
833
907
|
}, now, batchState);
|
|
908
|
+
return durationDeltaMs;
|
|
834
909
|
}
|
|
835
910
|
async function consumeSiteRateLimit(ctx, siteId, events, eventByteCounts, now) {
|
|
836
911
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
@@ -947,7 +1022,26 @@ async function flushSessionRateLimits(ctx, states, now) {
|
|
|
947
1022
|
}
|
|
948
1023
|
}
|
|
949
1024
|
}
|
|
950
|
-
|
|
1025
|
+
/**
|
|
1026
|
+
* The intent facts a live row carries for the dashboard's heat score. A
|
|
1027
|
+
* session row from before 0.1.0 has no first-visit flag, so `returning` stays
|
|
1028
|
+
* unknown rather than guessed.
|
|
1029
|
+
*/
|
|
1030
|
+
function liveIntentFields(session, funnel) {
|
|
1031
|
+
return {
|
|
1032
|
+
...(session.entryPath ? { entryPath: session.entryPath } : {}),
|
|
1033
|
+
...(session.newVisitor !== undefined
|
|
1034
|
+
? { returning: !session.newVisitor }
|
|
1035
|
+
: {}),
|
|
1036
|
+
...(session.utmCampaign ? { utmCampaign: session.utmCampaign } : {}),
|
|
1037
|
+
...(session.affiliateSlug ? { affiliateSlug: session.affiliateSlug } : {}),
|
|
1038
|
+
...(session.conversionCount !== undefined
|
|
1039
|
+
? { conversionCount: session.conversionCount }
|
|
1040
|
+
: {}),
|
|
1041
|
+
...(funnel ? { funnel } : {}),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
async function updateLiveSession(ctx, siteId, event, context, session, now, batchState, reach) {
|
|
951
1045
|
await ensureLiveSweep(ctx, siteId, now, batchState);
|
|
952
1046
|
const liveState = batchState
|
|
953
1047
|
? await loadLiveSession(ctx, siteId, event.sessionId, batchState)
|
|
@@ -988,6 +1082,7 @@ async function updateLiveSession(ctx, siteId, event, context, session, now, batc
|
|
|
988
1082
|
device: context.device ?? existing.device,
|
|
989
1083
|
pageviewCount: session.pageviewCount,
|
|
990
1084
|
eventCount: session.eventCount,
|
|
1085
|
+
...liveIntentFields(session, furtherReach(existing.funnel, reach)),
|
|
991
1086
|
// The live map animates a visitor from the route they left to the one
|
|
992
1087
|
// they reached; a heartbeat or click on the same page is not a move.
|
|
993
1088
|
...(event.type === "pageview" && event.path !== existing.path
|
|
@@ -1027,6 +1122,7 @@ async function updateLiveSession(ctx, siteId, event, context, session, now, batc
|
|
|
1027
1122
|
device: context.device ?? "unknown",
|
|
1028
1123
|
pageviewCount: session.pageviewCount,
|
|
1029
1124
|
eventCount: session.eventCount,
|
|
1125
|
+
...liveIntentFields(session, reach),
|
|
1030
1126
|
...(event.type === "heartbeat"
|
|
1031
1127
|
? {
|
|
1032
1128
|
lastHeartbeatEventId: event.eventId,
|
|
@@ -1114,9 +1210,9 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1114
1210
|
_id: progressId,
|
|
1115
1211
|
};
|
|
1116
1212
|
}
|
|
1117
|
-
|
|
1213
|
+
foldFunnelStepDelta(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1118
1214
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1119
|
-
return;
|
|
1215
|
+
return { funnelId: funnel._id, step: 1, steps: funnel.steps.length };
|
|
1120
1216
|
}
|
|
1121
1217
|
if (progress.completedAt !== undefined ||
|
|
1122
1218
|
event.timestamp < progress.lastStepAt)
|
|
@@ -1145,9 +1241,9 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1145
1241
|
completedAt: undefined,
|
|
1146
1242
|
});
|
|
1147
1243
|
}
|
|
1148
|
-
|
|
1244
|
+
foldFunnelStepDelta(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1149
1245
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1150
|
-
return;
|
|
1246
|
+
return { funnelId: funnel._id, step: 1, steps: funnel.steps.length };
|
|
1151
1247
|
}
|
|
1152
1248
|
const nextStep = funnel.steps[progress.currentStep];
|
|
1153
1249
|
if (!nextStep || !stepMatches(nextStep, event))
|
|
@@ -1167,44 +1263,19 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1167
1263
|
else {
|
|
1168
1264
|
await ctx.db.patch("funnelProgress", progress._id, updates);
|
|
1169
1265
|
}
|
|
1170
|
-
|
|
1266
|
+
foldFunnelStepDelta(reportRollupDeltas, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1171
1267
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1268
|
+
return {
|
|
1269
|
+
funnelId: funnel._id,
|
|
1270
|
+
step: currentStep,
|
|
1271
|
+
steps: funnel.steps.length,
|
|
1272
|
+
};
|
|
1172
1273
|
}
|
|
1173
1274
|
function stepMatches(step, event) {
|
|
1174
1275
|
return step.type === "pageview"
|
|
1175
1276
|
? event.type === "pageview" && event.path === step.value
|
|
1176
1277
|
: event.type === "custom" && event.name === step.value;
|
|
1177
1278
|
}
|
|
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
1279
|
async function accountConversion(ctx, siteId, session, event, trustedGoalKey, reportRollupDeltas) {
|
|
1209
1280
|
const revenueCents = event.revenueCents ?? 0;
|
|
1210
1281
|
const currency = event.currency ?? "USD";
|
|
@@ -1266,24 +1337,6 @@ async function accountConversion(ctx, siteId, session, event, trustedGoalKey, re
|
|
|
1266
1337
|
}
|
|
1267
1338
|
}
|
|
1268
1339
|
}
|
|
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
1340
|
function foldGoalRollup(deltas, siteId, event, goal) {
|
|
1288
1341
|
const coordinates = reportRollupCoordinates(siteId, String(goal._id), event.timestamp, event.visitorId);
|
|
1289
1342
|
const current = deltas.goals.get(coordinates.key) ?? {
|
|
@@ -1318,6 +1371,23 @@ function foldFunnelRollup(deltas, siteId, funnelId, event, step, completed) {
|
|
|
1318
1371
|
rollup.steps.set(step, current);
|
|
1319
1372
|
deltas.funnels.set(coordinates.key, rollup);
|
|
1320
1373
|
}
|
|
1374
|
+
function foldFunnelStepDelta(deltas, siteId, funnelId, event, step, completed) {
|
|
1375
|
+
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1376
|
+
const shard = stableHash(event.visitorId) % SECONDARY_AGGREGATE_SHARDS;
|
|
1377
|
+
const key = `${siteId}:${funnelId}:${bucketStart}:${step}:${shard}`;
|
|
1378
|
+
const current = deltas.funnelSteps.get(key) ?? {
|
|
1379
|
+
siteId,
|
|
1380
|
+
funnelId,
|
|
1381
|
+
bucketStart,
|
|
1382
|
+
step,
|
|
1383
|
+
shard,
|
|
1384
|
+
entrants: 0,
|
|
1385
|
+
completions: 0,
|
|
1386
|
+
};
|
|
1387
|
+
current.entrants += 1;
|
|
1388
|
+
current.completions += Number(completed);
|
|
1389
|
+
deltas.funnelSteps.set(key, current);
|
|
1390
|
+
}
|
|
1321
1391
|
function foldAffiliateVisitRollup(deltas, siteId, event, attribution, visitorKey) {
|
|
1322
1392
|
const coordinates = reportRollupCoordinates(siteId, String(attribution.affiliateId), event.timestamp, event.visitorId);
|
|
1323
1393
|
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
@@ -1327,13 +1397,13 @@ function foldAffiliateVisitRollup(deltas, siteId, event, attribution, visitorKey
|
|
|
1327
1397
|
shard: coordinates.shard,
|
|
1328
1398
|
affiliateSlug: attribution.affiliateSlug,
|
|
1329
1399
|
visits: 0,
|
|
1330
|
-
|
|
1400
|
+
visitorKeys: new Set(),
|
|
1331
1401
|
conversions: 0,
|
|
1332
1402
|
revenueCents: 0,
|
|
1333
1403
|
commissionCents: 0,
|
|
1334
1404
|
};
|
|
1335
1405
|
current.visits += 1;
|
|
1336
|
-
current.
|
|
1406
|
+
current.visitorKeys.add(visitorKey);
|
|
1337
1407
|
deltas.affiliates.set(coordinates.key, current);
|
|
1338
1408
|
}
|
|
1339
1409
|
function foldAffiliateConversionRollup(deltas, siteId, event, affiliate, affiliateSlug, revenueCents, commissionCents) {
|
|
@@ -1345,7 +1415,7 @@ function foldAffiliateConversionRollup(deltas, siteId, event, affiliate, affilia
|
|
|
1345
1415
|
shard: coordinates.shard,
|
|
1346
1416
|
affiliateSlug,
|
|
1347
1417
|
visits: 0,
|
|
1348
|
-
|
|
1418
|
+
visitorKeys: new Set(),
|
|
1349
1419
|
conversions: 0,
|
|
1350
1420
|
revenueCents: 0,
|
|
1351
1421
|
commissionCents: 0,
|
|
@@ -1405,7 +1475,7 @@ function foldRouteDelta(deltas, siteId, route, timestamp, visitorId, apply) {
|
|
|
1405
1475
|
continued: 0,
|
|
1406
1476
|
dwellMs: 0,
|
|
1407
1477
|
dwellSamples: 0,
|
|
1408
|
-
|
|
1478
|
+
visitorKeys: new Set(),
|
|
1409
1479
|
outgoing: new Map(),
|
|
1410
1480
|
};
|
|
1411
1481
|
apply(current);
|
|
@@ -1426,7 +1496,7 @@ function foldRouteRollups(deltas, siteId, event, session, existing) {
|
|
|
1426
1496
|
delta.pageviews += 1;
|
|
1427
1497
|
if (firstPageview)
|
|
1428
1498
|
delta.entries += 1;
|
|
1429
|
-
delta.
|
|
1499
|
+
delta.visitorKeys.add(session.visitorKey);
|
|
1430
1500
|
});
|
|
1431
1501
|
// Out-of-order telemetry keeps its view but cannot say where it came from.
|
|
1432
1502
|
if (existing === null || firstPageview)
|
|
@@ -1448,245 +1518,19 @@ function foldRouteRollups(deltas, siteId, event, session, existing) {
|
|
|
1448
1518
|
});
|
|
1449
1519
|
}
|
|
1450
1520
|
}
|
|
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) {
|
|
1521
|
+
function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, recordTelemetry, financialConversion, timezone) {
|
|
1679
1522
|
const revenueCents = financialConversion ? (event.revenueCents ?? 0) : 0;
|
|
1680
1523
|
const dimensionDeltas = dimensions.map((dimension) => ({
|
|
1681
1524
|
...dimension,
|
|
1682
1525
|
count: Number(recordTelemetry),
|
|
1683
1526
|
revenueCents,
|
|
1527
|
+
pageviews: Number(recordTelemetry && event.type === "pageview"),
|
|
1528
|
+
sessions: Number(recordTelemetry && session.isNewSession),
|
|
1684
1529
|
}));
|
|
1685
|
-
for (const granularity of
|
|
1530
|
+
for (const granularity of aggregateGranularities(timezone)) {
|
|
1686
1531
|
const shard = stableHash(event.sessionId) %
|
|
1687
1532
|
(granularity === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
1688
|
-
const
|
|
1689
|
-
const bucketStart = Math.floor(event.timestamp / interval) * interval;
|
|
1533
|
+
const bucketStart = aggregateBucketStart(granularity, event.timestamp, timezone);
|
|
1690
1534
|
const key = `${siteId}:${granularity}:${bucketStart}:${shard}`;
|
|
1691
1535
|
let delta = aggregateDeltas.get(key);
|
|
1692
1536
|
if (!delta) {
|
|
@@ -1706,6 +1550,9 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1706
1550
|
conversions: 0,
|
|
1707
1551
|
revenueCents: 0,
|
|
1708
1552
|
dimensions: [],
|
|
1553
|
+
durationMs: 0,
|
|
1554
|
+
continuedSessions: 0,
|
|
1555
|
+
newVisitors: 0,
|
|
1709
1556
|
};
|
|
1710
1557
|
aggregateDeltas.set(key, delta);
|
|
1711
1558
|
}
|
|
@@ -1727,6 +1574,58 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1727
1574
|
delta.conversions += Number(financialConversion);
|
|
1728
1575
|
delta.revenueCents += revenueCents;
|
|
1729
1576
|
delta.dimensions = mergeDimensions(delta.dimensions, dimensionDeltas);
|
|
1577
|
+
if (recordTelemetry) {
|
|
1578
|
+
delta.durationMs += session.durationDeltaMs;
|
|
1579
|
+
delta.continuedSessions += Number(session.continued);
|
|
1580
|
+
delta.newVisitors += Number(session.isNewSession && session.isNewVisitor);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* The buckets an event lands in: UTC hour and UTC day always, plus the
|
|
1586
|
+
* calendar day in the site's timezone when the site keeps one.
|
|
1587
|
+
*/
|
|
1588
|
+
function aggregateGranularities(timezone) {
|
|
1589
|
+
return timezone ? ["hour", "day", "localDay"] : ["hour", "day"];
|
|
1590
|
+
}
|
|
1591
|
+
function aggregateBucketStart(granularity, timestamp, timezone) {
|
|
1592
|
+
if (granularity === "localDay" && timezone) {
|
|
1593
|
+
return localDayStart(timestamp, timezone);
|
|
1594
|
+
}
|
|
1595
|
+
const interval = granularity === "hour" ? HOUR_MS : DAY_MS;
|
|
1596
|
+
return Math.floor(timestamp / interval) * interval;
|
|
1597
|
+
}
|
|
1598
|
+
/** Folds only session duration growth, for heartbeats. */
|
|
1599
|
+
function foldSessionDuration(aggregateDeltas, siteId, event, durationDeltaMs, timezone) {
|
|
1600
|
+
if (durationDeltaMs <= 0)
|
|
1601
|
+
return;
|
|
1602
|
+
for (const granularity of aggregateGranularities(timezone)) {
|
|
1603
|
+
const shard = stableHash(event.sessionId) %
|
|
1604
|
+
(granularity === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
1605
|
+
const bucketStart = aggregateBucketStart(granularity, event.timestamp, timezone);
|
|
1606
|
+
const key = `${siteId}:${granularity}:${bucketStart}:${shard}`;
|
|
1607
|
+
const delta = aggregateDeltas.get(key) ?? {
|
|
1608
|
+
siteId,
|
|
1609
|
+
granularity,
|
|
1610
|
+
bucketStart,
|
|
1611
|
+
shard,
|
|
1612
|
+
events: 0,
|
|
1613
|
+
pageviews: 0,
|
|
1614
|
+
clicks: 0,
|
|
1615
|
+
customEvents: 0,
|
|
1616
|
+
outboundClicks: 0,
|
|
1617
|
+
sessions: 0,
|
|
1618
|
+
visitors: 0,
|
|
1619
|
+
visitorSketch: createVisitorSketch(),
|
|
1620
|
+
conversions: 0,
|
|
1621
|
+
revenueCents: 0,
|
|
1622
|
+
dimensions: [],
|
|
1623
|
+
durationMs: 0,
|
|
1624
|
+
continuedSessions: 0,
|
|
1625
|
+
newVisitors: 0,
|
|
1626
|
+
};
|
|
1627
|
+
delta.durationMs += durationDeltaMs;
|
|
1628
|
+
aggregateDeltas.set(key, delta);
|
|
1730
1629
|
}
|
|
1731
1630
|
}
|
|
1732
1631
|
async function flushAggregates(ctx, aggregateDeltas) {
|
|
@@ -1739,7 +1638,11 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1739
1638
|
.eq("bucketStart", delta.bucketStart)
|
|
1740
1639
|
.eq("shard", delta.shard))
|
|
1741
1640
|
.unique();
|
|
1742
|
-
const dimensions = mergeDimensions(bucket?.dimensions ?? [], delta.dimensions)
|
|
1641
|
+
const dimensions = mergeDimensions(bucket?.dimensions ?? [], delta.dimensions.map((slot) => ({
|
|
1642
|
+
...slot,
|
|
1643
|
+
pageviews: slot.pageviews ?? 0,
|
|
1644
|
+
sessions: slot.sessions ?? 0,
|
|
1645
|
+
})));
|
|
1743
1646
|
const priorSketch = bucket?.visitorSketch;
|
|
1744
1647
|
const visitorSketch = priorSketch
|
|
1745
1648
|
? unionVisitorSketches(priorSketch, delta.visitorSketch)
|
|
@@ -1761,6 +1664,9 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1761
1664
|
conversions: bucket.conversions + delta.conversions,
|
|
1762
1665
|
revenueCents: bucket.revenueCents + delta.revenueCents,
|
|
1763
1666
|
dimensions,
|
|
1667
|
+
durationMs: (bucket.durationMs ?? 0) + delta.durationMs,
|
|
1668
|
+
continuedSessions: (bucket.continuedSessions ?? 0) + delta.continuedSessions,
|
|
1669
|
+
newVisitors: (bucket.newVisitors ?? 0) + delta.newVisitors,
|
|
1764
1670
|
});
|
|
1765
1671
|
}
|
|
1766
1672
|
else {
|
|
@@ -1781,6 +1687,9 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1781
1687
|
conversions: delta.conversions,
|
|
1782
1688
|
revenueCents: delta.revenueCents,
|
|
1783
1689
|
dimensions,
|
|
1690
|
+
durationMs: delta.durationMs,
|
|
1691
|
+
continuedSessions: delta.continuedSessions,
|
|
1692
|
+
newVisitors: delta.newVisitors,
|
|
1784
1693
|
});
|
|
1785
1694
|
}
|
|
1786
1695
|
}
|
|
@@ -1792,6 +1701,8 @@ function mergeDimensions(current, additions) {
|
|
|
1792
1701
|
if (exact) {
|
|
1793
1702
|
exact.count += addition.count;
|
|
1794
1703
|
exact.revenueCents += addition.revenueCents;
|
|
1704
|
+
exact.pageviews = (exact.pageviews ?? 0) + addition.pageviews;
|
|
1705
|
+
exact.sessions = (exact.sessions ?? 0) + addition.sessions;
|
|
1795
1706
|
continue;
|
|
1796
1707
|
}
|
|
1797
1708
|
const typeSlots = result.filter((candidate) => candidate.type === addition.type);
|
|
@@ -1808,6 +1719,8 @@ function mergeDimensions(current, additions) {
|
|
|
1808
1719
|
value: addition.value,
|
|
1809
1720
|
count: addition.count,
|
|
1810
1721
|
revenueCents: addition.revenueCents,
|
|
1722
|
+
pageviews: addition.pageviews,
|
|
1723
|
+
sessions: addition.sessions,
|
|
1811
1724
|
});
|
|
1812
1725
|
continue;
|
|
1813
1726
|
}
|
|
@@ -1815,6 +1728,8 @@ function mergeDimensions(current, additions) {
|
|
|
1815
1728
|
if (overflow) {
|
|
1816
1729
|
overflow.count += addition.count;
|
|
1817
1730
|
overflow.revenueCents += addition.revenueCents;
|
|
1731
|
+
overflow.pageviews = (overflow.pageviews ?? 0) + addition.pageviews;
|
|
1732
|
+
overflow.sessions = (overflow.sessions ?? 0) + addition.sessions;
|
|
1818
1733
|
continue;
|
|
1819
1734
|
}
|
|
1820
1735
|
if (addition.count === 0 && addition.revenueCents === 0)
|
|
@@ -1830,6 +1745,8 @@ function mergeDimensions(current, additions) {
|
|
|
1830
1745
|
value: "(other)",
|
|
1831
1746
|
count: addition.count,
|
|
1832
1747
|
revenueCents: addition.revenueCents,
|
|
1748
|
+
pageviews: addition.pageviews,
|
|
1749
|
+
sessions: addition.sessions,
|
|
1833
1750
|
});
|
|
1834
1751
|
continue;
|
|
1835
1752
|
}
|
|
@@ -1841,6 +1758,8 @@ function mergeDimensions(current, additions) {
|
|
|
1841
1758
|
replacement.value = "(other)";
|
|
1842
1759
|
replacement.count += addition.count;
|
|
1843
1760
|
replacement.revenueCents += addition.revenueCents;
|
|
1761
|
+
replacement.pageviews = (replacement.pageviews ?? 0) + addition.pageviews;
|
|
1762
|
+
replacement.sessions = (replacement.sessions ?? 0) + addition.sessions;
|
|
1844
1763
|
}
|
|
1845
1764
|
return result;
|
|
1846
1765
|
}
|