@iann29/rastro 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +374 -62
- package/agent/integration.md +651 -0
- package/agent/manifest.json +183 -0
- package/agent/manifest.schema.json +405 -0
- package/dist/client/federation.d.ts +205 -0
- package/dist/client/federation.d.ts.map +1 -0
- package/dist/client/federation.js +179 -0
- package/dist/client/federation.js.map +1 -0
- package/dist/client/index.d.ts +1658 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +228 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +10 -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 +169 -1
- package/dist/component/_generated/component.d.ts.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 +11 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +11 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +18 -0
- package/dist/component/coverage.d.ts.map +1 -0
- package/dist/component/coverage.js +19 -0
- package/dist/component/coverage.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 +1 -0
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +85 -36
- 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 +1 -0
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +258 -19
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +6 -3
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +120 -2
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +659 -91
- 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 +517 -54
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts +4 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +13 -5
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +224 -7
- package/dist/component/schema.js +113 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +5 -1
- 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 +42 -11
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +10 -1
- 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/tracker/generated.d.ts +4 -4
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +4 -4
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.js +23 -11
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/benchmarks/2026-08-20-realistic.md +4 -4
- package/docs/benchmarks/2026-08-21-formal-certification.md +333 -0
- package/docs/federation-setup.md +395 -0
- package/docs/federation.md +258 -0
- package/docs/upgrading.md +130 -0
- package/llms.txt +65 -0
- package/package.json +41 -11
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +10 -0
- package/src/component/_generated/component.ts +228 -1
- package/src/component/cardinality.ts +116 -0
- package/src/component/constants.ts +11 -0
- package/src/component/coverage.ts +25 -0
- package/src/component/errors.ts +2 -1
- package/src/component/http.ts +129 -38
- package/src/component/identity.ts +74 -0
- package/src/component/ingest.ts +414 -13
- package/src/component/live.ts +8 -3
- package/src/component/reports.ts +910 -98
- package/src/component/retention.ts +624 -88
- package/src/component/sanitize.ts +19 -5
- package/src/component/schema.ts +125 -0
- package/src/component/sites.ts +5 -1
- package/src/component/useragent.ts +171 -0
- package/src/component/validators.ts +11 -0
- package/src/component/visitors.ts +105 -0
package/dist/component/ingest.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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_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_FUNNEL_STEPS, 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";
|
|
5
6
|
import { fail } from "./errors.js";
|
|
7
|
+
import { ensureAnalyticsControl } from "./coverage.js";
|
|
6
8
|
import { bufferEventBatch, eventBatchKey, eventBucketStart, findTelemetry, flushEventBatches, insertEventBatch, inspectTelemetryMinute, } from "./eventStore.js";
|
|
7
|
-
import { cleanString,
|
|
9
|
+
import { cleanString, isValidMoney, measureBatchBytes, originAllowed, sanitizeContext, sanitizeCurrency, sanitizeEvent, sanitizeOpaqueId, sanitizePath, sanitizeProperties, sanitizeSlug, sourceFromReferrer, stableHash, } from "./sanitize.js";
|
|
8
10
|
import { ingestContextValidator, ingestResultValidator, trackerEventValidator, trustedConversionValidator, } from "./validators.js";
|
|
9
11
|
export const ingestBatch = mutation({
|
|
10
12
|
args: {
|
|
@@ -28,16 +30,19 @@ export const ingestBatch = mutation({
|
|
|
28
30
|
limit: MAX_BATCH_EVENTS,
|
|
29
31
|
});
|
|
30
32
|
}
|
|
33
|
+
let eventByteCounts;
|
|
31
34
|
try {
|
|
32
|
-
|
|
35
|
+
({ eventByteCounts } = measureBatchBytes(String(args.siteId), args.events));
|
|
33
36
|
}
|
|
34
37
|
catch {
|
|
35
38
|
fail("BATCH_TOO_LARGE", "batch exceeds 64 KiB", { limitBytes: 65_536 });
|
|
36
39
|
}
|
|
37
40
|
const now = Date.now();
|
|
38
|
-
|
|
41
|
+
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
42
|
+
await consumeSiteRateLimit(ctx, args.siteId, args.events, eventByteCounts, now);
|
|
39
43
|
const context = sanitizeContext(args.context);
|
|
40
44
|
const aggregateDeltas = new Map();
|
|
45
|
+
const reportRollupDeltas = createReportRollupDeltas();
|
|
41
46
|
const sessionRateWindows = new Map();
|
|
42
47
|
const batchState = {
|
|
43
48
|
sessions: new Map(),
|
|
@@ -171,6 +176,7 @@ export const ingestBatch = mutation({
|
|
|
171
176
|
goals: canMatchDefinition ? goals ?? [] : [],
|
|
172
177
|
funnels: canMatchDefinition ? funnels ?? [] : [],
|
|
173
178
|
aggregateDeltas,
|
|
179
|
+
reportRollupDeltas,
|
|
174
180
|
recordTelemetry: true,
|
|
175
181
|
financialConversion: false,
|
|
176
182
|
trustedGoalKey: undefined,
|
|
@@ -183,6 +189,7 @@ export const ingestBatch = mutation({
|
|
|
183
189
|
await flushBatchState(ctx, batchState);
|
|
184
190
|
await flushSessionRateLimits(ctx, sessionRateWindows, now);
|
|
185
191
|
await flushAggregates(ctx, aggregateDeltas);
|
|
192
|
+
await flushReportRollups(ctx, reportRollupDeltas);
|
|
186
193
|
return { accepted, duplicates, rejected };
|
|
187
194
|
},
|
|
188
195
|
});
|
|
@@ -260,6 +267,7 @@ export const trackConversion = mutation({
|
|
|
260
267
|
device: existingTelemetry?.device ?? existingSession?.device,
|
|
261
268
|
};
|
|
262
269
|
const aggregateDeltas = new Map();
|
|
270
|
+
const reportRollupDeltas = createReportRollupDeltas();
|
|
263
271
|
const session = await accountEvent(ctx, {
|
|
264
272
|
siteId: args.siteId,
|
|
265
273
|
event,
|
|
@@ -268,6 +276,7 @@ export const trackConversion = mutation({
|
|
|
268
276
|
goals: [],
|
|
269
277
|
funnels: [],
|
|
270
278
|
aggregateDeltas,
|
|
279
|
+
reportRollupDeltas,
|
|
271
280
|
recordTelemetry: existingTelemetry === null && !telemetry.saturated,
|
|
272
281
|
financialConversion: true,
|
|
273
282
|
trustedGoalKey,
|
|
@@ -277,6 +286,8 @@ export const trackConversion = mutation({
|
|
|
277
286
|
await insertEventBatch(ctx, args.siteId, event, session.country ?? "unknown");
|
|
278
287
|
}
|
|
279
288
|
await flushAggregates(ctx, aggregateDeltas);
|
|
289
|
+
await flushReportRollups(ctx, reportRollupDeltas);
|
|
290
|
+
await ensureAnalyticsControl(ctx, args.siteId, Date.now());
|
|
280
291
|
return { accepted: 1, duplicates: 0, rejected: 0 };
|
|
281
292
|
},
|
|
282
293
|
});
|
|
@@ -346,7 +357,7 @@ function withoutSystemFields(document) {
|
|
|
346
357
|
async function accountEvent(ctx, input) {
|
|
347
358
|
const { event } = input;
|
|
348
359
|
const session = input.recordTelemetry
|
|
349
|
-
? await upsertSession(ctx, input.siteId, event, input.context, input.existingSession, input.financialConversion, input.batchState)
|
|
360
|
+
? await upsertSession(ctx, input.siteId, event, input.context, input.existingSession, input.financialConversion, input.reportRollupDeltas, input.batchState)
|
|
350
361
|
: await accountTrustedSession(ctx, input.existingSession, event);
|
|
351
362
|
const matchedGoalKeys = [];
|
|
352
363
|
const matchingGoals = input.goals.filter((goal) => goal.active && goalMatches(goal, event));
|
|
@@ -393,18 +404,23 @@ async function accountEvent(ctx, input) {
|
|
|
393
404
|
timestamp: event.timestamp,
|
|
394
405
|
valueCents: goal.valueCents ?? 0,
|
|
395
406
|
});
|
|
407
|
+
foldGoalRollup(input.reportRollupDeltas, input.siteId, event, goal);
|
|
396
408
|
matchedGoalKeys.push(goal.key);
|
|
397
409
|
}
|
|
398
410
|
for (const funnel of input.funnels) {
|
|
399
411
|
if (funnel.active) {
|
|
400
|
-
await advanceFunnel(ctx, input.siteId, funnel, event, input.batchState);
|
|
412
|
+
await advanceFunnel(ctx, input.siteId, funnel, event, input.reportRollupDeltas, input.batchState);
|
|
401
413
|
}
|
|
402
414
|
}
|
|
403
415
|
if (input.financialConversion) {
|
|
404
|
-
await accountConversion(ctx, input.siteId, session, event, input.trustedGoalKey);
|
|
416
|
+
await accountConversion(ctx, input.siteId, session, event, input.trustedGoalKey, input.reportRollupDeltas);
|
|
405
417
|
}
|
|
418
|
+
const campaignDimensions = session.utmCampaign
|
|
419
|
+
? [{ type: "campaign", value: session.utmCampaign }]
|
|
420
|
+
: [];
|
|
406
421
|
const dimensions = [
|
|
407
422
|
{ type: "source", value: session.source },
|
|
423
|
+
...campaignDimensions,
|
|
408
424
|
{ type: "page", value: event.path },
|
|
409
425
|
{ type: "country", value: session.country ?? "unknown" },
|
|
410
426
|
{ type: "device", value: input.context.device ?? "unknown" },
|
|
@@ -438,6 +454,8 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
438
454
|
isNewVisitorDay: false,
|
|
439
455
|
startedAt: existing.startedAt,
|
|
440
456
|
source: existing.source,
|
|
457
|
+
utmCampaign: existing.utmCampaign,
|
|
458
|
+
visitorKey: existing.visitorKey ?? existing.visitorId,
|
|
441
459
|
country: existing.country,
|
|
442
460
|
city: existing.city,
|
|
443
461
|
latitude: existing.latitude,
|
|
@@ -450,13 +468,23 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
450
468
|
affiliateExpiresAt: existing.affiliateExpiresAt,
|
|
451
469
|
};
|
|
452
470
|
}
|
|
453
|
-
async function upsertSession(ctx, siteId, event, context, existing, financialConversion, batchState) {
|
|
454
|
-
|
|
471
|
+
async function upsertSession(ctx, siteId, event, context, existing, financialConversion, reportRollupDeltas, batchState) {
|
|
472
|
+
// A host-supplied id is the identity; anonymous sessions count through the
|
|
473
|
+
// daily key derived by the HTTP action, or once per session without one.
|
|
474
|
+
const visitorKey = existing?.visitorKey ??
|
|
475
|
+
(event.visitorId !== event.sessionId
|
|
476
|
+
? event.visitorId
|
|
477
|
+
: (context.visitorKey ?? event.sessionId));
|
|
455
478
|
const utmSource = stringProperty(event.properties, "utm_source");
|
|
479
|
+
// Campaign traffic names its own source; the referrer host is the fallback.
|
|
480
|
+
const source = utmSource?.toLowerCase() ?? sourceFromReferrer(event.referrer);
|
|
456
481
|
const utmMedium = stringProperty(event.properties, "utm_medium");
|
|
457
482
|
const utmCampaign = stringProperty(event.properties, "utm_campaign");
|
|
458
483
|
const attribution = await attributionForEvent(ctx, siteId, event, existing, batchState);
|
|
459
484
|
if (existing) {
|
|
485
|
+
if (attribution && existing.affiliateId !== attribution.affiliateId) {
|
|
486
|
+
foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution, visitorKey);
|
|
487
|
+
}
|
|
460
488
|
const startedAt = Math.min(existing.startedAt, event.timestamp);
|
|
461
489
|
const lastSeenAt = Math.max(existing.lastSeenAt, event.timestamp);
|
|
462
490
|
const updates = {
|
|
@@ -503,6 +531,8 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
503
531
|
Math.floor(event.timestamp / DAY_MS),
|
|
504
532
|
startedAt,
|
|
505
533
|
source: existing.source,
|
|
534
|
+
utmCampaign: existing.utmCampaign,
|
|
535
|
+
visitorKey,
|
|
506
536
|
country: context.country ?? existing.country,
|
|
507
537
|
city: context.city ?? existing.city,
|
|
508
538
|
latitude: context.latitude ?? existing.latitude,
|
|
@@ -532,6 +562,7 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
532
562
|
siteId,
|
|
533
563
|
sessionId: event.sessionId,
|
|
534
564
|
visitorId: event.visitorId,
|
|
565
|
+
visitorKey,
|
|
535
566
|
startedAt: event.timestamp,
|
|
536
567
|
lastSeenAt: event.timestamp,
|
|
537
568
|
entryPath: event.path,
|
|
@@ -559,6 +590,9 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
559
590
|
...(attribution ?? {}),
|
|
560
591
|
};
|
|
561
592
|
const sessionId = await ctx.db.insert("sessions", fields);
|
|
593
|
+
if (attribution) {
|
|
594
|
+
foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution, visitorKey);
|
|
595
|
+
}
|
|
562
596
|
if (batchState) {
|
|
563
597
|
const state = batchState.sessions.get(event.sessionId);
|
|
564
598
|
if (!state)
|
|
@@ -578,6 +612,8 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
578
612
|
Math.floor(event.timestamp / DAY_MS),
|
|
579
613
|
startedAt: event.timestamp,
|
|
580
614
|
source,
|
|
615
|
+
utmCampaign,
|
|
616
|
+
visitorKey,
|
|
581
617
|
country: context.country,
|
|
582
618
|
city: context.city,
|
|
583
619
|
latitude: context.latitude,
|
|
@@ -729,6 +765,7 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
729
765
|
isNewVisitorDay: false,
|
|
730
766
|
startedAt,
|
|
731
767
|
source: session.source,
|
|
768
|
+
visitorKey: session.visitorKey ?? session.visitorId,
|
|
732
769
|
country: context.country ?? session.country,
|
|
733
770
|
city: context.city ?? session.city,
|
|
734
771
|
latitude: context.latitude ?? session.latitude,
|
|
@@ -738,15 +775,12 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
738
775
|
affiliateSlug: session.affiliateSlug,
|
|
739
776
|
}, now, batchState);
|
|
740
777
|
}
|
|
741
|
-
async function consumeSiteRateLimit(ctx, siteId, events, now) {
|
|
778
|
+
async function consumeSiteRateLimit(ctx, siteId, events, eventByteCounts, now) {
|
|
742
779
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
743
780
|
const increments = new Map();
|
|
744
|
-
const eventByteCounts = [];
|
|
745
|
-
const encoder = new TextEncoder();
|
|
746
781
|
for (let index = 0; index < events.length; index += 1) {
|
|
747
782
|
const event = events[index];
|
|
748
|
-
const byteCount =
|
|
749
|
-
eventByteCounts.push(byteCount);
|
|
783
|
+
const byteCount = eventByteCounts[index];
|
|
750
784
|
const shard = stableHash(event.sessionId) % SITE_INGEST_SHARDS;
|
|
751
785
|
const current = increments.get(shard) ?? { eventCount: 0, byteCount: 0 };
|
|
752
786
|
current.eventCount += 1;
|
|
@@ -803,7 +837,6 @@ async function consumeSiteRateLimit(ctx, siteId, events, now) {
|
|
|
803
837
|
});
|
|
804
838
|
}
|
|
805
839
|
}
|
|
806
|
-
return eventByteCounts;
|
|
807
840
|
}
|
|
808
841
|
async function consumeRateLimit(ctx, siteId, sessionId, byteCount, now, states) {
|
|
809
842
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
@@ -957,9 +990,14 @@ async function ensureLiveSweep(ctx, siteId, now, batchState) {
|
|
|
957
990
|
.query("liveSweepStates")
|
|
958
991
|
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
959
992
|
.unique();
|
|
960
|
-
if (!existing) {
|
|
993
|
+
if (!existing || existing.scheduledAt <= now) {
|
|
961
994
|
const scheduledAt = now + LIVE_SWEEP_INTERVAL_MS;
|
|
962
|
-
|
|
995
|
+
if (existing) {
|
|
996
|
+
await ctx.db.patch("liveSweepStates", existing._id, { scheduledAt });
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
await ctx.db.insert("liveSweepStates", { siteId, scheduledAt });
|
|
1000
|
+
}
|
|
963
1001
|
await ctx.scheduler.runAt(scheduledAt, internal.live.sweep, {
|
|
964
1002
|
siteId,
|
|
965
1003
|
scheduledAt,
|
|
@@ -973,7 +1011,7 @@ function goalMatches(goal, event) {
|
|
|
973
1011
|
}
|
|
974
1012
|
return event.type === "custom" && event.name === goal.matchValue;
|
|
975
1013
|
}
|
|
976
|
-
async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
1014
|
+
async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, batchState) {
|
|
977
1015
|
const cacheKey = JSON.stringify([funnel._id, event.visitorId]);
|
|
978
1016
|
let progressState = batchState?.funnelProgress.get(cacheKey);
|
|
979
1017
|
if (batchState && !progressState) {
|
|
@@ -1017,6 +1055,7 @@ async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
|
1017
1055
|
};
|
|
1018
1056
|
}
|
|
1019
1057
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, 0, false);
|
|
1058
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1020
1059
|
return;
|
|
1021
1060
|
}
|
|
1022
1061
|
if (progress.completedAt !== undefined || event.timestamp < progress.lastStepAt)
|
|
@@ -1046,6 +1085,7 @@ async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
|
1046
1085
|
});
|
|
1047
1086
|
}
|
|
1048
1087
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, 0, false);
|
|
1088
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1049
1089
|
return;
|
|
1050
1090
|
}
|
|
1051
1091
|
const nextStep = funnel.steps[progress.currentStep];
|
|
@@ -1067,6 +1107,7 @@ async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
|
1067
1107
|
await ctx.db.patch("funnelProgress", progress._id, updates);
|
|
1068
1108
|
}
|
|
1069
1109
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1110
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1070
1111
|
}
|
|
1071
1112
|
function stepMatches(step, event) {
|
|
1072
1113
|
return step.type === "pageview"
|
|
@@ -1103,7 +1144,7 @@ async function incrementFunnelStep(ctx, siteId, funnelId, event, step, completed
|
|
|
1103
1144
|
});
|
|
1104
1145
|
}
|
|
1105
1146
|
}
|
|
1106
|
-
async function accountConversion(ctx, siteId, session, event, trustedGoalKey) {
|
|
1147
|
+
async function accountConversion(ctx, siteId, session, event, trustedGoalKey, reportRollupDeltas) {
|
|
1107
1148
|
const revenueCents = event.revenueCents ?? 0;
|
|
1108
1149
|
const currency = event.currency ?? "USD";
|
|
1109
1150
|
const candidate = attributionSnapshot(session);
|
|
@@ -1132,6 +1173,7 @@ async function accountConversion(ctx, siteId, session, event, trustedGoalKey) {
|
|
|
1132
1173
|
commissionCents,
|
|
1133
1174
|
});
|
|
1134
1175
|
if (commissionAffiliate && attribution) {
|
|
1176
|
+
foldAffiliateConversionRollup(reportRollupDeltas, siteId, event, commissionAffiliate, attribution.affiliateSlug, revenueCents, commissionCents);
|
|
1135
1177
|
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1136
1178
|
const shard = stableHash(event.sessionId) % SECONDARY_AGGREGATE_SHARDS;
|
|
1137
1179
|
const stats = await ctx.db
|
|
@@ -1163,6 +1205,188 @@ async function accountConversion(ctx, siteId, session, event, trustedGoalKey) {
|
|
|
1163
1205
|
}
|
|
1164
1206
|
}
|
|
1165
1207
|
}
|
|
1208
|
+
function createReportRollupDeltas() {
|
|
1209
|
+
return {
|
|
1210
|
+
goals: new Map(),
|
|
1211
|
+
funnels: new Map(),
|
|
1212
|
+
affiliates: new Map(),
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
function reportRollupCoordinates(siteId, definitionId, timestamp, visitorId) {
|
|
1216
|
+
const bucketStart = Math.floor(timestamp / DAY_MS) * DAY_MS;
|
|
1217
|
+
const shard = stableHash(visitorId) % REPORT_ROLLUP_SHARDS;
|
|
1218
|
+
return {
|
|
1219
|
+
bucketStart,
|
|
1220
|
+
shard,
|
|
1221
|
+
key: `${siteId}:${definitionId}:${bucketStart}:${shard}`,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function foldGoalRollup(deltas, siteId, event, goal) {
|
|
1225
|
+
const coordinates = reportRollupCoordinates(siteId, String(goal._id), event.timestamp, event.visitorId);
|
|
1226
|
+
const current = deltas.goals.get(coordinates.key) ?? {
|
|
1227
|
+
siteId,
|
|
1228
|
+
goalId: goal._id,
|
|
1229
|
+
bucketStart: coordinates.bucketStart,
|
|
1230
|
+
shard: coordinates.shard,
|
|
1231
|
+
goalKey: goal.key,
|
|
1232
|
+
completions: 0,
|
|
1233
|
+
valueCents: 0,
|
|
1234
|
+
};
|
|
1235
|
+
current.completions += 1;
|
|
1236
|
+
current.valueCents += goal.valueCents ?? 0;
|
|
1237
|
+
deltas.goals.set(coordinates.key, current);
|
|
1238
|
+
}
|
|
1239
|
+
function foldFunnelRollup(deltas, siteId, funnelId, event, step, completed) {
|
|
1240
|
+
const coordinates = reportRollupCoordinates(siteId, String(funnelId), event.timestamp, event.visitorId);
|
|
1241
|
+
const rollup = deltas.funnels.get(coordinates.key) ?? {
|
|
1242
|
+
siteId,
|
|
1243
|
+
funnelId,
|
|
1244
|
+
bucketStart: coordinates.bucketStart,
|
|
1245
|
+
shard: coordinates.shard,
|
|
1246
|
+
steps: new Map(),
|
|
1247
|
+
};
|
|
1248
|
+
const current = rollup.steps.get(step) ?? {
|
|
1249
|
+
step,
|
|
1250
|
+
entrants: 0,
|
|
1251
|
+
completions: 0,
|
|
1252
|
+
};
|
|
1253
|
+
current.entrants += 1;
|
|
1254
|
+
current.completions += Number(completed);
|
|
1255
|
+
rollup.steps.set(step, current);
|
|
1256
|
+
deltas.funnels.set(coordinates.key, rollup);
|
|
1257
|
+
}
|
|
1258
|
+
function foldAffiliateVisitRollup(deltas, siteId, event, attribution, visitorKey) {
|
|
1259
|
+
const coordinates = reportRollupCoordinates(siteId, String(attribution.affiliateId), event.timestamp, event.visitorId);
|
|
1260
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1261
|
+
siteId,
|
|
1262
|
+
affiliateId: attribution.affiliateId,
|
|
1263
|
+
bucketStart: coordinates.bucketStart,
|
|
1264
|
+
shard: coordinates.shard,
|
|
1265
|
+
affiliateSlug: attribution.affiliateSlug,
|
|
1266
|
+
visits: 0,
|
|
1267
|
+
visitorSketch: createVisitorSketch(),
|
|
1268
|
+
conversions: 0,
|
|
1269
|
+
revenueCents: 0,
|
|
1270
|
+
commissionCents: 0,
|
|
1271
|
+
};
|
|
1272
|
+
current.visits += 1;
|
|
1273
|
+
current.visitorSketch = addVisitor(current.visitorSketch, visitorKey);
|
|
1274
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1275
|
+
}
|
|
1276
|
+
function foldAffiliateConversionRollup(deltas, siteId, event, affiliate, affiliateSlug, revenueCents, commissionCents) {
|
|
1277
|
+
const coordinates = reportRollupCoordinates(siteId, String(affiliate._id), event.timestamp, event.visitorId);
|
|
1278
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1279
|
+
siteId,
|
|
1280
|
+
affiliateId: affiliate._id,
|
|
1281
|
+
bucketStart: coordinates.bucketStart,
|
|
1282
|
+
shard: coordinates.shard,
|
|
1283
|
+
affiliateSlug,
|
|
1284
|
+
visits: 0,
|
|
1285
|
+
visitorSketch: createVisitorSketch(),
|
|
1286
|
+
conversions: 0,
|
|
1287
|
+
revenueCents: 0,
|
|
1288
|
+
commissionCents: 0,
|
|
1289
|
+
};
|
|
1290
|
+
current.conversions += 1;
|
|
1291
|
+
current.revenueCents += revenueCents;
|
|
1292
|
+
current.commissionCents += commissionCents;
|
|
1293
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1294
|
+
}
|
|
1295
|
+
async function flushReportRollups(ctx, deltas) {
|
|
1296
|
+
for (const delta of deltas.goals.values()) {
|
|
1297
|
+
const existing = await ctx.db
|
|
1298
|
+
.query("goalDailyRollups")
|
|
1299
|
+
.withIndex("by_siteId_and_goalId_and_bucketStart_and_shard", (range) => range
|
|
1300
|
+
.eq("siteId", delta.siteId)
|
|
1301
|
+
.eq("goalId", delta.goalId)
|
|
1302
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1303
|
+
.eq("shard", delta.shard))
|
|
1304
|
+
.unique();
|
|
1305
|
+
const fields = {
|
|
1306
|
+
siteId: delta.siteId,
|
|
1307
|
+
goalId: delta.goalId,
|
|
1308
|
+
bucketStart: delta.bucketStart,
|
|
1309
|
+
shard: delta.shard,
|
|
1310
|
+
generation: 1,
|
|
1311
|
+
goalKey: delta.goalKey,
|
|
1312
|
+
completions: (existing?.completions ?? 0) + delta.completions,
|
|
1313
|
+
valueCents: (existing?.valueCents ?? 0) + delta.valueCents,
|
|
1314
|
+
};
|
|
1315
|
+
if (existing) {
|
|
1316
|
+
await ctx.db.replace("goalDailyRollups", existing._id, fields);
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
await ctx.db.insert("goalDailyRollups", fields);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
for (const delta of deltas.funnels.values()) {
|
|
1323
|
+
const existing = await ctx.db
|
|
1324
|
+
.query("funnelDailyRollups")
|
|
1325
|
+
.withIndex("by_siteId_and_funnelId_and_bucketStart_and_shard", (range) => range
|
|
1326
|
+
.eq("siteId", delta.siteId)
|
|
1327
|
+
.eq("funnelId", delta.funnelId)
|
|
1328
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1329
|
+
.eq("shard", delta.shard))
|
|
1330
|
+
.unique();
|
|
1331
|
+
const steps = existing?.steps.map((step) => ({ ...step })) ?? [];
|
|
1332
|
+
for (const addition of delta.steps.values()) {
|
|
1333
|
+
const current = steps.find((step) => step.step === addition.step);
|
|
1334
|
+
if (current) {
|
|
1335
|
+
current.entrants += addition.entrants;
|
|
1336
|
+
current.completions += addition.completions;
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
if (steps.length >= MAX_FUNNEL_STEPS) {
|
|
1340
|
+
fail("LIMIT_EXCEEDED", "daily funnel rollup step limit exceeded");
|
|
1341
|
+
}
|
|
1342
|
+
steps.push(addition);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
const fields = {
|
|
1346
|
+
siteId: delta.siteId,
|
|
1347
|
+
funnelId: delta.funnelId,
|
|
1348
|
+
bucketStart: delta.bucketStart,
|
|
1349
|
+
shard: delta.shard,
|
|
1350
|
+
generation: 1,
|
|
1351
|
+
steps,
|
|
1352
|
+
};
|
|
1353
|
+
if (existing) {
|
|
1354
|
+
await ctx.db.replace("funnelDailyRollups", existing._id, fields);
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
await ctx.db.insert("funnelDailyRollups", fields);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
for (const delta of deltas.affiliates.values()) {
|
|
1361
|
+
const existing = await ctx.db
|
|
1362
|
+
.query("affiliateDailyRollups")
|
|
1363
|
+
.withIndex("by_siteId_and_affiliateId_and_bucketStart_and_shard", (range) => range
|
|
1364
|
+
.eq("siteId", delta.siteId)
|
|
1365
|
+
.eq("affiliateId", delta.affiliateId)
|
|
1366
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1367
|
+
.eq("shard", delta.shard))
|
|
1368
|
+
.unique();
|
|
1369
|
+
const fields = {
|
|
1370
|
+
siteId: delta.siteId,
|
|
1371
|
+
affiliateId: delta.affiliateId,
|
|
1372
|
+
bucketStart: delta.bucketStart,
|
|
1373
|
+
shard: delta.shard,
|
|
1374
|
+
generation: 1,
|
|
1375
|
+
affiliateSlug: delta.affiliateSlug,
|
|
1376
|
+
visits: (existing?.visits ?? 0) + delta.visits,
|
|
1377
|
+
visitorSketch: unionVisitorSketches(existing?.visitorSketch ?? createVisitorSketch(), delta.visitorSketch),
|
|
1378
|
+
conversions: (existing?.conversions ?? 0) + delta.conversions,
|
|
1379
|
+
revenueCents: (existing?.revenueCents ?? 0) + delta.revenueCents,
|
|
1380
|
+
commissionCents: (existing?.commissionCents ?? 0) + delta.commissionCents,
|
|
1381
|
+
};
|
|
1382
|
+
if (existing) {
|
|
1383
|
+
await ctx.db.replace("affiliateDailyRollups", existing._id, fields);
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
await ctx.db.insert("affiliateDailyRollups", fields);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1166
1390
|
function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, recordTelemetry, financialConversion) {
|
|
1167
1391
|
const revenueCents = financialConversion ? event.revenueCents ?? 0 : 0;
|
|
1168
1392
|
const dimensionDeltas = dimensions.map((dimension) => ({
|
|
@@ -1190,6 +1414,7 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1190
1414
|
outboundClicks: 0,
|
|
1191
1415
|
sessions: 0,
|
|
1192
1416
|
visitors: 0,
|
|
1417
|
+
visitorSketch: createVisitorSketch(),
|
|
1193
1418
|
conversions: 0,
|
|
1194
1419
|
revenueCents: 0,
|
|
1195
1420
|
dimensions: [],
|
|
@@ -1208,6 +1433,9 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1208
1433
|
? Number(session.isNewVisitorHour)
|
|
1209
1434
|
: Number(session.isNewVisitorDay)
|
|
1210
1435
|
: 0;
|
|
1436
|
+
if (recordTelemetry) {
|
|
1437
|
+
delta.visitorSketch = addVisitor(delta.visitorSketch, session.visitorKey);
|
|
1438
|
+
}
|
|
1211
1439
|
delta.conversions += Number(financialConversion);
|
|
1212
1440
|
delta.revenueCents += revenueCents;
|
|
1213
1441
|
delta.dimensions = mergeDimensions(delta.dimensions, dimensionDeltas);
|
|
@@ -1224,6 +1452,13 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1224
1452
|
.eq("shard", delta.shard))
|
|
1225
1453
|
.unique();
|
|
1226
1454
|
const dimensions = mergeDimensions(bucket?.dimensions ?? [], delta.dimensions);
|
|
1455
|
+
const priorSketch = bucket?.visitorSketch;
|
|
1456
|
+
const visitorSketch = priorSketch
|
|
1457
|
+
? unionVisitorSketches(priorSketch, delta.visitorSketch)
|
|
1458
|
+
: delta.visitorSketch;
|
|
1459
|
+
const visitorSketchComplete = bucket
|
|
1460
|
+
? bucket.visitorSketchComplete === true && priorSketch !== undefined
|
|
1461
|
+
: true;
|
|
1227
1462
|
if (bucket) {
|
|
1228
1463
|
await ctx.db.patch("aggregateBuckets", bucket._id, {
|
|
1229
1464
|
events: bucket.events + delta.events,
|
|
@@ -1233,6 +1468,8 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1233
1468
|
outboundClicks: bucket.outboundClicks + delta.outboundClicks,
|
|
1234
1469
|
sessions: bucket.sessions + delta.sessions,
|
|
1235
1470
|
visitors: bucket.visitors + delta.visitors,
|
|
1471
|
+
visitorSketch,
|
|
1472
|
+
visitorSketchComplete,
|
|
1236
1473
|
conversions: bucket.conversions + delta.conversions,
|
|
1237
1474
|
revenueCents: bucket.revenueCents + delta.revenueCents,
|
|
1238
1475
|
dimensions,
|
|
@@ -1251,6 +1488,8 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1251
1488
|
outboundClicks: delta.outboundClicks,
|
|
1252
1489
|
sessions: delta.sessions,
|
|
1253
1490
|
visitors: delta.visitors,
|
|
1491
|
+
visitorSketch,
|
|
1492
|
+
visitorSketchComplete,
|
|
1254
1493
|
conversions: delta.conversions,
|
|
1255
1494
|
revenueCents: delta.revenueCents,
|
|
1256
1495
|
dimensions,
|