@iann29/rastro 0.1.0-alpha.4 → 0.1.0-alpha.6
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 +203 -56
- package/agent/integration.md +17 -15
- package/agent/manifest.json +6 -4
- package/agent/manifest.schema.json +8 -5
- package/dist/client/federation.d.ts +34 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +17 -3
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +486 -31
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +209 -41
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +8 -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 +82 -8
- package/dist/component/_generated/component.d.ts.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.map +1 -1
- package/dist/component/cardinality.js.map +1 -1
- package/dist/component/constants.d.ts +6 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +12 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/diagnostics.d.ts.map +1 -1
- package/dist/component/diagnostics.js.map +1 -1
- package/dist/component/eventStore.d.ts +3 -1
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/eventStore.js +13 -10
- 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.map +1 -1
- package/dist/component/geo.js +3 -2
- package/dist/component/geo.js.map +1 -1
- 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 +71 -37
- 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 +168 -29
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +1 -3
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +70 -4
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +232 -59
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts +4 -4
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +54 -22
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +42 -11
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +83 -10
- package/dist/component/schema.js +34 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +6 -6
- 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 +29 -18
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +8 -2
- 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 +8 -4
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +8 -4
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +23 -11
- 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 +137 -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 +71 -71
- package/docs/benchmarks/2026-08-21-formal-certification.md +50 -30
- package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
- package/docs/federation.md +1 -1
- package/package.json +27 -9
- package/scripts/benchmark-ingest.mjs +101 -48
- package/src/component/_generated/api.ts +8 -0
- package/src/component/_generated/component.ts +87 -5
- package/src/component/affiliates.ts +20 -5
- package/src/component/cardinality.ts +8 -7
- package/src/component/constants.ts +12 -0
- package/src/component/diagnostics.ts +3 -2
- package/src/component/eventStore.ts +53 -46
- package/src/component/funnels.ts +19 -16
- package/src/component/geo.ts +16 -14
- package/src/component/goals.ts +29 -21
- package/src/component/guards.ts +3 -1
- package/src/component/http.ts +126 -78
- package/src/component/identity.ts +74 -0
- package/src/component/ingest.ts +349 -130
- package/src/component/live.ts +5 -4
- package/src/component/reports.ts +572 -221
- package/src/component/retention.ts +221 -98
- package/src/component/sanitize.ts +77 -27
- package/src/component/schema.ts +46 -5
- package/src/component/sites.ts +22 -11
- package/src/component/useragent.ts +171 -0
- package/src/component/validators.ts +19 -7
- package/src/component/visitors.ts +116 -0
- package/src/component/vitals.ts +146 -0
package/src/component/ingest.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
REPORT_ROLLUP_SHARDS,
|
|
25
25
|
SECONDARY_AGGREGATE_SHARDS,
|
|
26
26
|
SITE_INGEST_SHARDS,
|
|
27
|
+
VITAL_PAGE_SLOTS,
|
|
27
28
|
} from "./constants.js";
|
|
28
29
|
import {
|
|
29
30
|
addVisitor,
|
|
@@ -31,6 +32,15 @@ import {
|
|
|
31
32
|
unionVisitorSketches,
|
|
32
33
|
type VisitorSketch,
|
|
33
34
|
} from "./cardinality.js";
|
|
35
|
+
import {
|
|
36
|
+
addToVitalHistogram,
|
|
37
|
+
emptyVitalHistogram,
|
|
38
|
+
mergeVitalHistograms,
|
|
39
|
+
vitalDevice,
|
|
40
|
+
VITAL_ALL,
|
|
41
|
+
VITAL_OTHER_PAGES,
|
|
42
|
+
type VitalMetric,
|
|
43
|
+
} from "./vitals.js";
|
|
34
44
|
import { fail } from "./errors.js";
|
|
35
45
|
import { ensureAnalyticsControl } from "./coverage.js";
|
|
36
46
|
import {
|
|
@@ -69,6 +79,7 @@ import {
|
|
|
69
79
|
|
|
70
80
|
type DimensionType =
|
|
71
81
|
| "source"
|
|
82
|
+
| "campaign"
|
|
72
83
|
| "page"
|
|
73
84
|
| "country"
|
|
74
85
|
| "device"
|
|
@@ -110,11 +121,14 @@ type FunnelRollupDelta = {
|
|
|
110
121
|
funnelId: Id<"funnels">;
|
|
111
122
|
bucketStart: number;
|
|
112
123
|
shard: number;
|
|
113
|
-
steps: Map<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
steps: Map<
|
|
125
|
+
number,
|
|
126
|
+
{
|
|
127
|
+
step: number;
|
|
128
|
+
entrants: number;
|
|
129
|
+
completions: number;
|
|
130
|
+
}
|
|
131
|
+
>;
|
|
118
132
|
};
|
|
119
133
|
type AffiliateRollupDelta = {
|
|
120
134
|
siteId: Id<"sites">;
|
|
@@ -128,10 +142,22 @@ type AffiliateRollupDelta = {
|
|
|
128
142
|
revenueCents: number;
|
|
129
143
|
commissionCents: number;
|
|
130
144
|
};
|
|
145
|
+
type VitalRollupDelta = {
|
|
146
|
+
siteId: Id<"sites">;
|
|
147
|
+
bucketStart: number;
|
|
148
|
+
device: string;
|
|
149
|
+
page: string;
|
|
150
|
+
shard: number;
|
|
151
|
+
metrics: Map<
|
|
152
|
+
VitalMetric,
|
|
153
|
+
{ count: number; sum: number; histogram: number[] }
|
|
154
|
+
>;
|
|
155
|
+
};
|
|
131
156
|
type ReportRollupDeltas = {
|
|
132
157
|
goals: Map<string, GoalRollupDelta>;
|
|
133
158
|
funnels: Map<string, FunnelRollupDelta>;
|
|
134
159
|
affiliates: Map<string, AffiliateRollupDelta>;
|
|
160
|
+
vitals: Map<string, VitalRollupDelta>;
|
|
135
161
|
};
|
|
136
162
|
type AttributionSnapshot = {
|
|
137
163
|
affiliateId: Id<"affiliates">;
|
|
@@ -146,6 +172,8 @@ type SessionState = {
|
|
|
146
172
|
isNewVisitorDay: boolean;
|
|
147
173
|
startedAt: number;
|
|
148
174
|
source: string;
|
|
175
|
+
visitorKey: string;
|
|
176
|
+
utmCampaign?: string;
|
|
149
177
|
country?: string;
|
|
150
178
|
city?: string;
|
|
151
179
|
latitude?: number;
|
|
@@ -172,8 +200,9 @@ type CachedRow<T> = {
|
|
|
172
200
|
dirty: boolean;
|
|
173
201
|
};
|
|
174
202
|
|
|
175
|
-
type CachedDocument<
|
|
176
|
-
|
|
203
|
+
type CachedDocument<
|
|
204
|
+
TableName extends keyof import("./_generated/dataModel.js").DataModel,
|
|
205
|
+
> = Omit<Doc<TableName>, "_creationTime"> & { _creationTime?: number };
|
|
177
206
|
|
|
178
207
|
type BatchState = {
|
|
179
208
|
sessions: Map<string, CachedRow<CachedDocument<"sessions">>>;
|
|
@@ -204,13 +233,20 @@ export const ingestBatch = mutation({
|
|
|
204
233
|
});
|
|
205
234
|
}
|
|
206
235
|
if (args.events.length < 1 || args.events.length > MAX_BATCH_EVENTS) {
|
|
207
|
-
fail(
|
|
208
|
-
|
|
209
|
-
|
|
236
|
+
fail(
|
|
237
|
+
"BATCH_TOO_LARGE",
|
|
238
|
+
`events must contain 1-${MAX_BATCH_EVENTS} items`,
|
|
239
|
+
{
|
|
240
|
+
limit: MAX_BATCH_EVENTS,
|
|
241
|
+
},
|
|
242
|
+
);
|
|
210
243
|
}
|
|
211
244
|
let eventByteCounts: number[];
|
|
212
245
|
try {
|
|
213
|
-
({ eventByteCounts } = measureBatchBytes(
|
|
246
|
+
({ eventByteCounts } = measureBatchBytes(
|
|
247
|
+
String(args.siteId),
|
|
248
|
+
args.events,
|
|
249
|
+
));
|
|
214
250
|
} catch {
|
|
215
251
|
fail("BATCH_TOO_LARGE", "batch exceeds 64 KiB", { limitBytes: 65_536 });
|
|
216
252
|
}
|
|
@@ -261,7 +297,7 @@ export const ingestBatch = mutation({
|
|
|
261
297
|
const bucketStart = eventBucketStart(event.timestamp);
|
|
262
298
|
const groupKey = eventBatchKey(event.sessionId, bucketStart);
|
|
263
299
|
requestGroups.add(groupKey);
|
|
264
|
-
if (event.type === "heartbeat") continue;
|
|
300
|
+
if (event.type === "heartbeat" || event.type === "vital") continue;
|
|
265
301
|
const eventIds = eventIdsByGroup.get(groupKey) ?? new Set<string>();
|
|
266
302
|
if (eventIds.has(event.eventId)) {
|
|
267
303
|
duplicateInBatch.add(eventIndex);
|
|
@@ -292,8 +328,8 @@ export const ingestBatch = mutation({
|
|
|
292
328
|
sessionId: group.sessionId,
|
|
293
329
|
bucketStart: group.bucketStart,
|
|
294
330
|
});
|
|
295
|
-
const newCount = [...group.eventIds].filter(
|
|
296
|
-
!eventIds.has(eventId)
|
|
331
|
+
const newCount = [...group.eventIds].filter(
|
|
332
|
+
(eventId) => !eventIds.has(eventId),
|
|
297
333
|
).length;
|
|
298
334
|
if (
|
|
299
335
|
newCount > 0 &&
|
|
@@ -332,12 +368,7 @@ export const ingestBatch = mutation({
|
|
|
332
368
|
continue;
|
|
333
369
|
}
|
|
334
370
|
const liveSession = (
|
|
335
|
-
await loadLiveSession(
|
|
336
|
-
ctx,
|
|
337
|
-
args.siteId,
|
|
338
|
-
event.sessionId,
|
|
339
|
-
batchState,
|
|
340
|
-
)
|
|
371
|
+
await loadLiveSession(ctx, args.siteId, event.sessionId, batchState)
|
|
341
372
|
).current;
|
|
342
373
|
if (
|
|
343
374
|
liveSession &&
|
|
@@ -373,10 +404,42 @@ export const ingestBatch = mutation({
|
|
|
373
404
|
continue;
|
|
374
405
|
}
|
|
375
406
|
|
|
407
|
+
if (event.type === "vital") {
|
|
408
|
+
// A vital annotates telemetry its session already produced; it never
|
|
409
|
+
// creates sessions, live presence, event batches, or aggregate counts.
|
|
410
|
+
if (!existingSession) {
|
|
411
|
+
rejected += 1;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
!(await consumeRateLimit(
|
|
416
|
+
ctx,
|
|
417
|
+
args.siteId,
|
|
418
|
+
event.sessionId,
|
|
419
|
+
eventByteCounts[eventIndex],
|
|
420
|
+
now,
|
|
421
|
+
sessionRateWindows,
|
|
422
|
+
))
|
|
423
|
+
) {
|
|
424
|
+
rejected += 1;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
foldVitalRollups(
|
|
428
|
+
reportRollupDeltas,
|
|
429
|
+
args.siteId,
|
|
430
|
+
event,
|
|
431
|
+
vitalDevice(context.device),
|
|
432
|
+
);
|
|
433
|
+
accepted += 1;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
376
437
|
if (
|
|
377
438
|
duplicateInBatch.has(eventIndex) ||
|
|
378
439
|
persistedEventIds
|
|
379
|
-
.get(
|
|
440
|
+
.get(
|
|
441
|
+
eventBatchKey(event.sessionId, eventBucketStart(event.timestamp)),
|
|
442
|
+
)
|
|
380
443
|
?.has(event.eventId)
|
|
381
444
|
) {
|
|
382
445
|
duplicates += 1;
|
|
@@ -415,8 +478,8 @@ export const ingestBatch = mutation({
|
|
|
415
478
|
event,
|
|
416
479
|
context,
|
|
417
480
|
existingSession,
|
|
418
|
-
goals: canMatchDefinition ? goals ?? [] : [],
|
|
419
|
-
funnels: canMatchDefinition ? funnels ?? [] : [],
|
|
481
|
+
goals: canMatchDefinition ? (goals ?? []) : [],
|
|
482
|
+
funnels: canMatchDefinition ? (funnels ?? []) : [],
|
|
420
483
|
aggregateDeltas,
|
|
421
484
|
reportRollupDeltas,
|
|
422
485
|
recordTelemetry: true,
|
|
@@ -448,7 +511,10 @@ export const trackConversion = mutation({
|
|
|
448
511
|
const site = await ctx.db.get("sites", args.siteId);
|
|
449
512
|
if (!site) fail("NOT_FOUND", "site not found");
|
|
450
513
|
if (!isValidMoney(args.revenueCents)) {
|
|
451
|
-
fail(
|
|
514
|
+
fail(
|
|
515
|
+
"INVALID_ARGUMENT",
|
|
516
|
+
"revenueCents must be an integer between 0 and 1000000000",
|
|
517
|
+
);
|
|
452
518
|
}
|
|
453
519
|
let event: TrackerEvent;
|
|
454
520
|
let trustedGoalKey: string | undefined;
|
|
@@ -473,7 +539,10 @@ export const trackConversion = mutation({
|
|
|
473
539
|
Date.now(),
|
|
474
540
|
);
|
|
475
541
|
} catch (error) {
|
|
476
|
-
fail(
|
|
542
|
+
fail(
|
|
543
|
+
"INVALID_ARGUMENT",
|
|
544
|
+
error instanceof Error ? error.message : "invalid conversion",
|
|
545
|
+
);
|
|
477
546
|
}
|
|
478
547
|
|
|
479
548
|
const siteCurrency = site.currency ?? "USD";
|
|
@@ -493,10 +562,14 @@ export const trackConversion = mutation({
|
|
|
493
562
|
return { accepted: 0, duplicates: 1, rejected: 0 };
|
|
494
563
|
}
|
|
495
564
|
if (event.currency !== siteCurrency) {
|
|
496
|
-
fail(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
565
|
+
fail(
|
|
566
|
+
"CURRENCY_MISMATCH",
|
|
567
|
+
"conversion currency does not match site currency",
|
|
568
|
+
{
|
|
569
|
+
siteCurrency,
|
|
570
|
+
conversionCurrency: event.currency ?? "USD",
|
|
571
|
+
},
|
|
572
|
+
);
|
|
500
573
|
}
|
|
501
574
|
|
|
502
575
|
const telemetry = await findTelemetry(ctx, args.siteId, event);
|
|
@@ -656,11 +729,7 @@ async function flushBatchState(ctx: MutationCtx, batchState: BatchState) {
|
|
|
656
729
|
function withoutSystemFields<
|
|
657
730
|
T extends { _id: unknown; _creationTime?: number },
|
|
658
731
|
>(document: T): Omit<T, "_id" | "_creationTime"> {
|
|
659
|
-
const {
|
|
660
|
-
_id,
|
|
661
|
-
_creationTime,
|
|
662
|
-
...fields
|
|
663
|
-
} = document;
|
|
732
|
+
const { _id, _creationTime, ...fields } = document;
|
|
664
733
|
void _id;
|
|
665
734
|
void _creationTime;
|
|
666
735
|
return fields;
|
|
@@ -699,8 +768,8 @@ async function accountEvent(
|
|
|
699
768
|
: await accountTrustedSession(ctx, input.existingSession, event);
|
|
700
769
|
|
|
701
770
|
const matchedGoalKeys: string[] = [];
|
|
702
|
-
const matchingGoals = input.goals.filter(
|
|
703
|
-
goal.active && goalMatches(goal, event)
|
|
771
|
+
const matchingGoals = input.goals.filter(
|
|
772
|
+
(goal) => goal.active && goalMatches(goal, event),
|
|
704
773
|
);
|
|
705
774
|
if (matchingGoals.length > 1) {
|
|
706
775
|
fail("LIMIT_EXCEEDED", "active goals must use unique matchers");
|
|
@@ -711,33 +780,36 @@ async function accountEvent(
|
|
|
711
780
|
eventBucketStart(event.timestamp),
|
|
712
781
|
event.eventId,
|
|
713
782
|
]);
|
|
714
|
-
const scopedPrior = (
|
|
715
|
-
.
|
|
716
|
-
|
|
717
|
-
range
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
783
|
+
const scopedPrior = (
|
|
784
|
+
await ctx.db
|
|
785
|
+
.query("goalCompletions")
|
|
786
|
+
.withIndex("by_siteId_and_goalId_and_eventId", (range) =>
|
|
787
|
+
range
|
|
788
|
+
.eq("siteId", input.siteId)
|
|
789
|
+
.eq("goalId", goal._id)
|
|
790
|
+
.eq("eventId", completionEventId),
|
|
791
|
+
)
|
|
792
|
+
.take(2)
|
|
793
|
+
).find((completion) => completion.idempotencyVersion === 2);
|
|
724
794
|
const legacyPrior = scopedPrior
|
|
725
795
|
? undefined
|
|
726
|
-
: (
|
|
727
|
-
.
|
|
728
|
-
|
|
729
|
-
range
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
796
|
+
: (
|
|
797
|
+
await ctx.db
|
|
798
|
+
.query("goalCompletions")
|
|
799
|
+
.withIndex("by_siteId_and_goalId_and_eventId", (range) =>
|
|
800
|
+
range
|
|
801
|
+
.eq("siteId", input.siteId)
|
|
802
|
+
.eq("goalId", goal._id)
|
|
803
|
+
.eq("eventId", event.eventId),
|
|
804
|
+
)
|
|
805
|
+
.take(2)
|
|
806
|
+
).find(
|
|
807
|
+
(completion) =>
|
|
736
808
|
completion.idempotencyVersion === undefined &&
|
|
737
809
|
completion.sessionId === event.sessionId &&
|
|
738
810
|
eventBucketStart(completion.timestamp) ===
|
|
739
|
-
eventBucketStart(event.timestamp)
|
|
740
|
-
|
|
811
|
+
eventBucketStart(event.timestamp),
|
|
812
|
+
);
|
|
741
813
|
if (scopedPrior || legacyPrior) continue;
|
|
742
814
|
await ctx.db.insert("goalCompletions", {
|
|
743
815
|
siteId: input.siteId,
|
|
@@ -750,12 +822,7 @@ async function accountEvent(
|
|
|
750
822
|
timestamp: event.timestamp,
|
|
751
823
|
valueCents: goal.valueCents ?? 0,
|
|
752
824
|
});
|
|
753
|
-
foldGoalRollup(
|
|
754
|
-
input.reportRollupDeltas,
|
|
755
|
-
input.siteId,
|
|
756
|
-
event,
|
|
757
|
-
goal,
|
|
758
|
-
);
|
|
825
|
+
foldGoalRollup(input.reportRollupDeltas, input.siteId, event, goal);
|
|
759
826
|
matchedGoalKeys.push(goal.key);
|
|
760
827
|
}
|
|
761
828
|
|
|
@@ -783,15 +850,19 @@ async function accountEvent(
|
|
|
783
850
|
);
|
|
784
851
|
}
|
|
785
852
|
|
|
853
|
+
const campaignDimensions: Dimension[] = session.utmCampaign
|
|
854
|
+
? [{ type: "campaign", value: session.utmCampaign }]
|
|
855
|
+
: [];
|
|
786
856
|
const dimensions: Dimension[] = [
|
|
787
857
|
{ type: "source", value: session.source },
|
|
858
|
+
...campaignDimensions,
|
|
788
859
|
{ type: "page", value: event.path },
|
|
789
860
|
{ type: "country", value: session.country ?? "unknown" },
|
|
790
861
|
{ type: "device", value: input.context.device ?? "unknown" },
|
|
791
862
|
{ type: "browser", value: input.context.browser ?? "Other" },
|
|
792
863
|
{
|
|
793
864
|
type: "event",
|
|
794
|
-
value: event.type === "custom" ? event.name ?? "custom" : event.type,
|
|
865
|
+
value: event.type === "custom" ? (event.name ?? "custom") : event.type,
|
|
795
866
|
},
|
|
796
867
|
...matchedGoalKeys.map((value): Dimension => ({ type: "goal", value })),
|
|
797
868
|
];
|
|
@@ -842,6 +913,8 @@ async function accountTrustedSession(
|
|
|
842
913
|
isNewVisitorDay: false,
|
|
843
914
|
startedAt: existing.startedAt,
|
|
844
915
|
source: existing.source,
|
|
916
|
+
utmCampaign: existing.utmCampaign,
|
|
917
|
+
visitorKey: existing.visitorKey ?? existing.visitorId,
|
|
845
918
|
country: existing.country,
|
|
846
919
|
city: existing.city,
|
|
847
920
|
latitude: existing.latitude,
|
|
@@ -865,8 +938,16 @@ async function upsertSession(
|
|
|
865
938
|
reportRollupDeltas: ReportRollupDeltas,
|
|
866
939
|
batchState?: BatchState,
|
|
867
940
|
): Promise<SessionState> {
|
|
868
|
-
|
|
941
|
+
// A host-supplied id is the identity; anonymous sessions count through the
|
|
942
|
+
// daily key derived by the HTTP action, or once per session without one.
|
|
943
|
+
const visitorKey =
|
|
944
|
+
existing?.visitorKey ??
|
|
945
|
+
(event.visitorId !== event.sessionId
|
|
946
|
+
? event.visitorId
|
|
947
|
+
: (context.visitorKey ?? event.sessionId));
|
|
869
948
|
const utmSource = stringProperty(event.properties, "utm_source");
|
|
949
|
+
// Campaign traffic names its own source; the referrer host is the fallback.
|
|
950
|
+
const source = utmSource?.toLowerCase() ?? sourceFromReferrer(event.referrer);
|
|
870
951
|
const utmMedium = stringProperty(event.properties, "utm_medium");
|
|
871
952
|
const utmCampaign = stringProperty(event.properties, "utm_campaign");
|
|
872
953
|
const attribution = await attributionForEvent(
|
|
@@ -883,6 +964,7 @@ async function upsertSession(
|
|
|
883
964
|
siteId,
|
|
884
965
|
event,
|
|
885
966
|
attribution,
|
|
967
|
+
visitorKey,
|
|
886
968
|
);
|
|
887
969
|
}
|
|
888
970
|
const startedAt = Math.min(existing.startedAt, event.timestamp);
|
|
@@ -890,15 +972,18 @@ async function upsertSession(
|
|
|
890
972
|
const updates = {
|
|
891
973
|
startedAt,
|
|
892
974
|
lastSeenAt,
|
|
893
|
-
entryPath:
|
|
894
|
-
|
|
975
|
+
entryPath:
|
|
976
|
+
event.timestamp < existing.startedAt ? event.path : existing.entryPath,
|
|
977
|
+
exitPath:
|
|
978
|
+
event.timestamp >= existing.lastSeenAt ? event.path : existing.exitPath,
|
|
895
979
|
eventCount: existing.eventCount + 1,
|
|
896
|
-
pageviewCount:
|
|
980
|
+
pageviewCount:
|
|
981
|
+
existing.pageviewCount + (event.type === "pageview" ? 1 : 0),
|
|
897
982
|
durationMs: Math.max(0, lastSeenAt - startedAt),
|
|
898
983
|
conversionCount: existing.conversionCount + Number(financialConversion),
|
|
899
984
|
revenueCents:
|
|
900
985
|
existing.revenueCents +
|
|
901
|
-
(financialConversion ? event.revenueCents ?? 0 : 0),
|
|
986
|
+
(financialConversion ? (event.revenueCents ?? 0) : 0),
|
|
902
987
|
...(financialConversion && event.currency
|
|
903
988
|
? { currency: event.currency }
|
|
904
989
|
: {}),
|
|
@@ -909,7 +994,9 @@ async function upsertSession(
|
|
|
909
994
|
...(context.country ? { country: context.country } : {}),
|
|
910
995
|
...(context.city ? { city: context.city } : {}),
|
|
911
996
|
...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
|
|
912
|
-
...(context.longitude !== undefined
|
|
997
|
+
...(context.longitude !== undefined
|
|
998
|
+
? { longitude: context.longitude }
|
|
999
|
+
: {}),
|
|
913
1000
|
browser: context.browser ?? existing.browser,
|
|
914
1001
|
os: context.os ?? existing.os,
|
|
915
1002
|
device: context.device ?? existing.device,
|
|
@@ -932,6 +1019,8 @@ async function upsertSession(
|
|
|
932
1019
|
Math.floor(event.timestamp / DAY_MS),
|
|
933
1020
|
startedAt,
|
|
934
1021
|
source: existing.source,
|
|
1022
|
+
utmCampaign: existing.utmCampaign,
|
|
1023
|
+
visitorKey,
|
|
935
1024
|
country: context.country ?? existing.country,
|
|
936
1025
|
city: context.city ?? existing.city,
|
|
937
1026
|
latitude: context.latitude ?? existing.latitude,
|
|
@@ -967,6 +1056,7 @@ async function upsertSession(
|
|
|
967
1056
|
siteId,
|
|
968
1057
|
sessionId: event.sessionId,
|
|
969
1058
|
visitorId: event.visitorId,
|
|
1059
|
+
visitorKey,
|
|
970
1060
|
startedAt: event.timestamp,
|
|
971
1061
|
lastSeenAt: event.timestamp,
|
|
972
1062
|
entryPath: event.path,
|
|
@@ -979,7 +1069,9 @@ async function upsertSession(
|
|
|
979
1069
|
...(context.country ? { country: context.country } : {}),
|
|
980
1070
|
...(context.city ? { city: context.city } : {}),
|
|
981
1071
|
...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
|
|
982
|
-
...(context.longitude !== undefined
|
|
1072
|
+
...(context.longitude !== undefined
|
|
1073
|
+
? { longitude: context.longitude }
|
|
1074
|
+
: {}),
|
|
983
1075
|
browser: context.browser ?? "Other",
|
|
984
1076
|
os: context.os ?? "Other",
|
|
985
1077
|
device: context.device ?? "unknown",
|
|
@@ -987,7 +1079,7 @@ async function upsertSession(
|
|
|
987
1079
|
pageviewCount: event.type === "pageview" ? 1 : 0,
|
|
988
1080
|
durationMs: 0,
|
|
989
1081
|
conversionCount: Number(financialConversion),
|
|
990
|
-
revenueCents: financialConversion ? event.revenueCents ?? 0 : 0,
|
|
1082
|
+
revenueCents: financialConversion ? (event.revenueCents ?? 0) : 0,
|
|
991
1083
|
...(financialConversion && event.currency
|
|
992
1084
|
? { currency: event.currency }
|
|
993
1085
|
: {}),
|
|
@@ -1000,6 +1092,7 @@ async function upsertSession(
|
|
|
1000
1092
|
siteId,
|
|
1001
1093
|
event,
|
|
1002
1094
|
attribution,
|
|
1095
|
+
visitorKey,
|
|
1003
1096
|
);
|
|
1004
1097
|
}
|
|
1005
1098
|
if (batchState) {
|
|
@@ -1022,6 +1115,8 @@ async function upsertSession(
|
|
|
1022
1115
|
Math.floor(event.timestamp / DAY_MS),
|
|
1023
1116
|
startedAt: event.timestamp,
|
|
1024
1117
|
source,
|
|
1118
|
+
utmCampaign,
|
|
1119
|
+
visitorKey,
|
|
1025
1120
|
country: context.country,
|
|
1026
1121
|
city: context.city,
|
|
1027
1122
|
latitude: context.latitude,
|
|
@@ -1032,14 +1127,12 @@ async function upsertSession(
|
|
|
1032
1127
|
};
|
|
1033
1128
|
}
|
|
1034
1129
|
|
|
1035
|
-
function attributionSnapshot(
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
},
|
|
1042
|
-
): AttributionSnapshot | undefined {
|
|
1130
|
+
function attributionSnapshot(value: {
|
|
1131
|
+
affiliateId?: Id<"affiliates">;
|
|
1132
|
+
affiliateSlug?: string;
|
|
1133
|
+
affiliateAttributedAt?: number;
|
|
1134
|
+
affiliateExpiresAt?: number;
|
|
1135
|
+
}): AttributionSnapshot | undefined {
|
|
1043
1136
|
if (
|
|
1044
1137
|
!value.affiliateId ||
|
|
1045
1138
|
!value.affiliateSlug ||
|
|
@@ -1056,7 +1149,6 @@ function attributionSnapshot(
|
|
|
1056
1149
|
};
|
|
1057
1150
|
}
|
|
1058
1151
|
|
|
1059
|
-
|
|
1060
1152
|
function attributionApplies(
|
|
1061
1153
|
attribution: AttributionSnapshot,
|
|
1062
1154
|
timestamp: number,
|
|
@@ -1187,14 +1279,15 @@ async function accountHeartbeat(
|
|
|
1187
1279
|
const updates = {
|
|
1188
1280
|
startedAt,
|
|
1189
1281
|
lastSeenAt,
|
|
1190
|
-
exitPath:
|
|
1191
|
-
? event.path
|
|
1192
|
-
: session.exitPath,
|
|
1282
|
+
exitPath:
|
|
1283
|
+
event.timestamp >= session.lastSeenAt ? event.path : session.exitPath,
|
|
1193
1284
|
durationMs: Math.max(0, lastSeenAt - startedAt),
|
|
1194
1285
|
...(context.country ? { country: context.country } : {}),
|
|
1195
1286
|
...(context.city ? { city: context.city } : {}),
|
|
1196
1287
|
...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
|
|
1197
|
-
...(context.longitude !== undefined
|
|
1288
|
+
...(context.longitude !== undefined
|
|
1289
|
+
? { longitude: context.longitude }
|
|
1290
|
+
: {}),
|
|
1198
1291
|
browser: context.browser ?? session.browser,
|
|
1199
1292
|
os: context.os ?? session.os,
|
|
1200
1293
|
device: context.device ?? session.device,
|
|
@@ -1218,6 +1311,7 @@ async function accountHeartbeat(
|
|
|
1218
1311
|
isNewVisitorDay: false,
|
|
1219
1312
|
startedAt,
|
|
1220
1313
|
source: session.source,
|
|
1314
|
+
visitorKey: session.visitorKey ?? session.visitorId,
|
|
1221
1315
|
country: context.country ?? session.country,
|
|
1222
1316
|
city: context.city ?? session.city,
|
|
1223
1317
|
latitude: context.latitude ?? session.latitude,
|
|
@@ -1240,7 +1334,10 @@ async function consumeSiteRateLimit(
|
|
|
1240
1334
|
): Promise<void> {
|
|
1241
1335
|
const windowStart =
|
|
1242
1336
|
Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
1243
|
-
const increments = new Map<
|
|
1337
|
+
const increments = new Map<
|
|
1338
|
+
number,
|
|
1339
|
+
{ eventCount: number; byteCount: number }
|
|
1340
|
+
>();
|
|
1244
1341
|
for (let index = 0; index < events.length; index += 1) {
|
|
1245
1342
|
const event = events[index];
|
|
1246
1343
|
const byteCount = eventByteCounts[index];
|
|
@@ -1265,9 +1362,8 @@ async function consumeSiteRateLimit(
|
|
|
1265
1362
|
.order("desc")
|
|
1266
1363
|
.take(1);
|
|
1267
1364
|
const storedWindow = rows[0] ?? null;
|
|
1268
|
-
const window =
|
|
1269
|
-
? storedWindow
|
|
1270
|
-
: null;
|
|
1365
|
+
const window =
|
|
1366
|
+
storedWindow?.windowStart === windowStart ? storedWindow : null;
|
|
1271
1367
|
if (
|
|
1272
1368
|
(window?.eventCount ?? 0) + increment.eventCount >
|
|
1273
1369
|
MAX_SITE_EVENTS_PER_SHARD_WINDOW ||
|
|
@@ -1279,10 +1375,8 @@ async function consumeSiteRateLimit(
|
|
|
1279
1375
|
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
1280
1376
|
eventLimitPerShard: MAX_SITE_EVENTS_PER_SHARD_WINDOW,
|
|
1281
1377
|
byteLimitPerShard: MAX_SITE_BYTES_PER_SHARD_WINDOW,
|
|
1282
|
-
siteEventLimit:
|
|
1283
|
-
|
|
1284
|
-
siteByteLimit:
|
|
1285
|
-
SITE_INGEST_SHARDS * MAX_SITE_BYTES_PER_SHARD_WINDOW,
|
|
1378
|
+
siteEventLimit: SITE_INGEST_SHARDS * MAX_SITE_EVENTS_PER_SHARD_WINDOW,
|
|
1379
|
+
siteByteLimit: SITE_INGEST_SHARDS * MAX_SITE_BYTES_PER_SHARD_WINDOW,
|
|
1286
1380
|
});
|
|
1287
1381
|
}
|
|
1288
1382
|
windows.push({ shard, increment, window: storedWindow });
|
|
@@ -1323,7 +1417,8 @@ async function consumeRateLimit(
|
|
|
1323
1417
|
now: number,
|
|
1324
1418
|
states: Map<string, SessionRateWindowState>,
|
|
1325
1419
|
): Promise<boolean> {
|
|
1326
|
-
const windowStart =
|
|
1420
|
+
const windowStart =
|
|
1421
|
+
Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
1327
1422
|
let state = states.get(sessionId);
|
|
1328
1423
|
if (!state) {
|
|
1329
1424
|
const rows = await ctx.db
|
|
@@ -1361,7 +1456,8 @@ async function flushSessionRateLimits(
|
|
|
1361
1456
|
states: Map<string, SessionRateWindowState>,
|
|
1362
1457
|
now: number,
|
|
1363
1458
|
) {
|
|
1364
|
-
const windowStart =
|
|
1459
|
+
const windowStart =
|
|
1460
|
+
Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
1365
1461
|
for (const state of states.values()) {
|
|
1366
1462
|
if (!state.dirty) continue;
|
|
1367
1463
|
if (state.window) {
|
|
@@ -1411,15 +1507,16 @@ async function updateLiveSession(
|
|
|
1411
1507
|
const latitude = context.latitude ?? session.latitude;
|
|
1412
1508
|
const longitude = context.longitude ?? session.longitude;
|
|
1413
1509
|
if (existing) {
|
|
1414
|
-
const recentHeartbeatEventIds =
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
(existing.
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1510
|
+
const recentHeartbeatEventIds =
|
|
1511
|
+
event.type === "heartbeat"
|
|
1512
|
+
? [
|
|
1513
|
+
...(existing.recentHeartbeatEventIds ??
|
|
1514
|
+
(existing.lastHeartbeatEventId
|
|
1515
|
+
? [existing.lastHeartbeatEventId]
|
|
1516
|
+
: [])),
|
|
1517
|
+
event.eventId,
|
|
1518
|
+
].slice(-8)
|
|
1519
|
+
: undefined;
|
|
1423
1520
|
const updates = {
|
|
1424
1521
|
visitorId: event.visitorId,
|
|
1425
1522
|
path: event.path,
|
|
@@ -1576,7 +1673,11 @@ async function advanceFunnel(
|
|
|
1576
1673
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1577
1674
|
return;
|
|
1578
1675
|
}
|
|
1579
|
-
if (
|
|
1676
|
+
if (
|
|
1677
|
+
progress.completedAt !== undefined ||
|
|
1678
|
+
event.timestamp < progress.lastStepAt
|
|
1679
|
+
)
|
|
1680
|
+
return;
|
|
1580
1681
|
if (event.timestamp - progress.startedAt > funnel.conversionWindowMs) {
|
|
1581
1682
|
if (!matchesFirst) return;
|
|
1582
1683
|
const updated = {
|
|
@@ -1774,6 +1875,7 @@ function createReportRollupDeltas(): ReportRollupDeltas {
|
|
|
1774
1875
|
goals: new Map(),
|
|
1775
1876
|
funnels: new Map(),
|
|
1776
1877
|
affiliates: new Map(),
|
|
1878
|
+
vitals: new Map(),
|
|
1777
1879
|
};
|
|
1778
1880
|
}
|
|
1779
1881
|
|
|
@@ -1855,6 +1957,7 @@ function foldAffiliateVisitRollup(
|
|
|
1855
1957
|
siteId: Id<"sites">,
|
|
1856
1958
|
event: TrackerEvent,
|
|
1857
1959
|
attribution: AttributionSnapshot,
|
|
1960
|
+
visitorKey: string,
|
|
1858
1961
|
) {
|
|
1859
1962
|
const coordinates = reportRollupCoordinates(
|
|
1860
1963
|
siteId,
|
|
@@ -1875,7 +1978,7 @@ function foldAffiliateVisitRollup(
|
|
|
1875
1978
|
commissionCents: 0,
|
|
1876
1979
|
};
|
|
1877
1980
|
current.visits += 1;
|
|
1878
|
-
current.visitorSketch = addVisitor(current.visitorSketch,
|
|
1981
|
+
current.visitorSketch = addVisitor(current.visitorSketch, visitorKey);
|
|
1879
1982
|
deltas.affiliates.set(coordinates.key, current);
|
|
1880
1983
|
}
|
|
1881
1984
|
|
|
@@ -1912,6 +2015,120 @@ function foldAffiliateConversionRollup(
|
|
|
1912
2015
|
deltas.affiliates.set(coordinates.key, current);
|
|
1913
2016
|
}
|
|
1914
2017
|
|
|
2018
|
+
function foldVitalRollups(
|
|
2019
|
+
deltas: ReportRollupDeltas,
|
|
2020
|
+
siteId: Id<"sites">,
|
|
2021
|
+
event: TrackerEvent,
|
|
2022
|
+
device: string,
|
|
2023
|
+
) {
|
|
2024
|
+
const metric = event.name as VitalMetric;
|
|
2025
|
+
const value = event.value ?? 0;
|
|
2026
|
+
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
2027
|
+
const shard = stableHash(event.sessionId) % REPORT_ROLLUP_SHARDS;
|
|
2028
|
+
// Each measurement lands twice: on its page (across devices) and on its
|
|
2029
|
+
// device class (across pages), so both breakdowns stay exact and bounded.
|
|
2030
|
+
for (const target of [
|
|
2031
|
+
{ device: VITAL_ALL, page: event.path },
|
|
2032
|
+
{ device, page: VITAL_ALL },
|
|
2033
|
+
]) {
|
|
2034
|
+
const key = JSON.stringify([
|
|
2035
|
+
siteId,
|
|
2036
|
+
bucketStart,
|
|
2037
|
+
target.device,
|
|
2038
|
+
target.page,
|
|
2039
|
+
shard,
|
|
2040
|
+
]);
|
|
2041
|
+
const row = deltas.vitals.get(key) ?? {
|
|
2042
|
+
siteId,
|
|
2043
|
+
bucketStart,
|
|
2044
|
+
device: target.device,
|
|
2045
|
+
page: target.page,
|
|
2046
|
+
shard,
|
|
2047
|
+
metrics: new Map<
|
|
2048
|
+
VitalMetric,
|
|
2049
|
+
{ count: number; sum: number; histogram: number[] }
|
|
2050
|
+
>(),
|
|
2051
|
+
};
|
|
2052
|
+
const entry = row.metrics.get(metric) ?? {
|
|
2053
|
+
count: 0,
|
|
2054
|
+
sum: 0,
|
|
2055
|
+
histogram: emptyVitalHistogram(),
|
|
2056
|
+
};
|
|
2057
|
+
entry.count += 1;
|
|
2058
|
+
entry.sum += value;
|
|
2059
|
+
entry.histogram = addToVitalHistogram(entry.histogram, value);
|
|
2060
|
+
row.metrics.set(metric, entry);
|
|
2061
|
+
deltas.vitals.set(key, row);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
async function upsertVitalRollup(ctx: MutationCtx, delta: VitalRollupDelta) {
|
|
2066
|
+
const existing = await ctx.db
|
|
2067
|
+
.query("vitalRollups")
|
|
2068
|
+
.withIndex("by_key", (range) =>
|
|
2069
|
+
range
|
|
2070
|
+
.eq("siteId", delta.siteId)
|
|
2071
|
+
.eq("bucketStart", delta.bucketStart)
|
|
2072
|
+
.eq("device", delta.device)
|
|
2073
|
+
.eq("page", delta.page)
|
|
2074
|
+
.eq("shard", delta.shard),
|
|
2075
|
+
)
|
|
2076
|
+
.unique();
|
|
2077
|
+
if (
|
|
2078
|
+
!existing &&
|
|
2079
|
+
delta.device === VITAL_ALL &&
|
|
2080
|
+
delta.page !== VITAL_OTHER_PAGES
|
|
2081
|
+
) {
|
|
2082
|
+
// A new page row spends one bounded read to respect the daily page slots;
|
|
2083
|
+
// past the cap, the day's remaining pages fold into "(other)".
|
|
2084
|
+
const pageRows = await ctx.db
|
|
2085
|
+
.query("vitalRollups")
|
|
2086
|
+
.withIndex("by_key", (range) =>
|
|
2087
|
+
range
|
|
2088
|
+
.eq("siteId", delta.siteId)
|
|
2089
|
+
.eq("bucketStart", delta.bucketStart)
|
|
2090
|
+
.eq("device", VITAL_ALL),
|
|
2091
|
+
)
|
|
2092
|
+
.take(VITAL_PAGE_SLOTS * REPORT_ROLLUP_SHARDS + 1);
|
|
2093
|
+
if (pageRows.length > VITAL_PAGE_SLOTS * REPORT_ROLLUP_SHARDS) {
|
|
2094
|
+
await upsertVitalRollup(ctx, { ...delta, page: VITAL_OTHER_PAGES });
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
const metrics = existing?.metrics.map((entry) => ({ ...entry })) ?? [];
|
|
2099
|
+
for (const [metric, addition] of delta.metrics) {
|
|
2100
|
+
const entry = metrics.find((candidate) => candidate.metric === metric);
|
|
2101
|
+
if (entry) {
|
|
2102
|
+
entry.count += addition.count;
|
|
2103
|
+
entry.sum += addition.sum;
|
|
2104
|
+
entry.histogram = mergeVitalHistograms(
|
|
2105
|
+
entry.histogram,
|
|
2106
|
+
addition.histogram,
|
|
2107
|
+
);
|
|
2108
|
+
} else {
|
|
2109
|
+
metrics.push({
|
|
2110
|
+
metric,
|
|
2111
|
+
count: addition.count,
|
|
2112
|
+
sum: addition.sum,
|
|
2113
|
+
histogram: [...addition.histogram],
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
const fields = {
|
|
2118
|
+
siteId: delta.siteId,
|
|
2119
|
+
bucketStart: delta.bucketStart,
|
|
2120
|
+
device: delta.device,
|
|
2121
|
+
page: delta.page,
|
|
2122
|
+
shard: delta.shard,
|
|
2123
|
+
metrics,
|
|
2124
|
+
};
|
|
2125
|
+
if (existing) {
|
|
2126
|
+
await ctx.db.replace("vitalRollups", existing._id, fields);
|
|
2127
|
+
} else {
|
|
2128
|
+
await ctx.db.insert("vitalRollups", fields);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
1915
2132
|
async function flushReportRollups(
|
|
1916
2133
|
ctx: MutationCtx,
|
|
1917
2134
|
deltas: ReportRollupDeltas,
|
|
@@ -1984,12 +2201,14 @@ async function flushReportRollups(
|
|
|
1984
2201
|
for (const delta of deltas.affiliates.values()) {
|
|
1985
2202
|
const existing = await ctx.db
|
|
1986
2203
|
.query("affiliateDailyRollups")
|
|
1987
|
-
.withIndex(
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2204
|
+
.withIndex(
|
|
2205
|
+
"by_siteId_and_affiliateId_and_bucketStart_and_shard",
|
|
2206
|
+
(range) =>
|
|
2207
|
+
range
|
|
2208
|
+
.eq("siteId", delta.siteId)
|
|
2209
|
+
.eq("affiliateId", delta.affiliateId)
|
|
2210
|
+
.eq("bucketStart", delta.bucketStart)
|
|
2211
|
+
.eq("shard", delta.shard),
|
|
1993
2212
|
)
|
|
1994
2213
|
.unique();
|
|
1995
2214
|
const fields = {
|
|
@@ -2006,8 +2225,7 @@ async function flushReportRollups(
|
|
|
2006
2225
|
),
|
|
2007
2226
|
conversions: (existing?.conversions ?? 0) + delta.conversions,
|
|
2008
2227
|
revenueCents: (existing?.revenueCents ?? 0) + delta.revenueCents,
|
|
2009
|
-
commissionCents:
|
|
2010
|
-
(existing?.commissionCents ?? 0) + delta.commissionCents,
|
|
2228
|
+
commissionCents: (existing?.commissionCents ?? 0) + delta.commissionCents,
|
|
2011
2229
|
};
|
|
2012
2230
|
if (existing) {
|
|
2013
2231
|
await ctx.db.replace("affiliateDailyRollups", existing._id, fields);
|
|
@@ -2015,6 +2233,9 @@ async function flushReportRollups(
|
|
|
2015
2233
|
await ctx.db.insert("affiliateDailyRollups", fields);
|
|
2016
2234
|
}
|
|
2017
2235
|
}
|
|
2236
|
+
for (const delta of deltas.vitals.values()) {
|
|
2237
|
+
await upsertVitalRollup(ctx, delta);
|
|
2238
|
+
}
|
|
2018
2239
|
}
|
|
2019
2240
|
|
|
2020
2241
|
function foldAggregates(
|
|
@@ -2026,16 +2247,15 @@ function foldAggregates(
|
|
|
2026
2247
|
recordTelemetry: boolean,
|
|
2027
2248
|
financialConversion: boolean,
|
|
2028
2249
|
) {
|
|
2029
|
-
const revenueCents = financialConversion ? event.revenueCents ?? 0 : 0;
|
|
2030
|
-
const dimensionDeltas = dimensions.map(
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
}),
|
|
2036
|
-
);
|
|
2250
|
+
const revenueCents = financialConversion ? (event.revenueCents ?? 0) : 0;
|
|
2251
|
+
const dimensionDeltas = dimensions.map((dimension): DimensionDelta => ({
|
|
2252
|
+
...dimension,
|
|
2253
|
+
count: Number(recordTelemetry),
|
|
2254
|
+
revenueCents,
|
|
2255
|
+
}));
|
|
2037
2256
|
for (const granularity of ["hour", "day"] as const) {
|
|
2038
|
-
const shard =
|
|
2257
|
+
const shard =
|
|
2258
|
+
stableHash(event.sessionId) %
|
|
2039
2259
|
(granularity === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
2040
2260
|
const interval = granularity === "hour" ? HOUR_MS : DAY_MS;
|
|
2041
2261
|
const bucketStart = Math.floor(event.timestamp / interval) * interval;
|
|
@@ -2076,7 +2296,7 @@ function foldAggregates(
|
|
|
2076
2296
|
: Number(session.isNewVisitorDay)
|
|
2077
2297
|
: 0;
|
|
2078
2298
|
if (recordTelemetry) {
|
|
2079
|
-
delta.visitorSketch = addVisitor(delta.visitorSketch,
|
|
2299
|
+
delta.visitorSketch = addVisitor(delta.visitorSketch, session.visitorKey);
|
|
2080
2300
|
}
|
|
2081
2301
|
delta.conversions += Number(financialConversion);
|
|
2082
2302
|
delta.revenueCents += revenueCents;
|
|
@@ -2170,10 +2390,7 @@ function mergeDimensions(
|
|
|
2170
2390
|
(candidate) => candidate.type === addition.type,
|
|
2171
2391
|
);
|
|
2172
2392
|
if (typeSlots.length < DIMENSION_SLOTS - 1) {
|
|
2173
|
-
const occupied = Array.from(
|
|
2174
|
-
{ length: DIMENSION_SLOTS },
|
|
2175
|
-
() => false,
|
|
2176
|
-
);
|
|
2393
|
+
const occupied = Array.from({ length: DIMENSION_SLOTS }, () => false);
|
|
2177
2394
|
for (const candidate of typeSlots) occupied[candidate.slot] = true;
|
|
2178
2395
|
let slot = stableHash(addition.value) % DIMENSION_SLOTS;
|
|
2179
2396
|
while (occupied[slot]) slot = (slot + 1) % DIMENSION_SLOTS;
|
|
@@ -2225,5 +2442,7 @@ function stringProperty(
|
|
|
2225
2442
|
key: string,
|
|
2226
2443
|
): string | undefined {
|
|
2227
2444
|
const value = properties?.[key];
|
|
2228
|
-
return typeof value === "string"
|
|
2445
|
+
return typeof value === "string"
|
|
2446
|
+
? cleanString(value, 120) || undefined
|
|
2447
|
+
: undefined;
|
|
2229
2448
|
}
|