@iann29/rastro 0.1.0-alpha.2 → 0.1.0-alpha.4
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 +325 -63
- 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 +1600 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +210 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +9 -5
- 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 +161 -1
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +4 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/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 +10 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +10 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +7 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +9 -4
- package/dist/component/convex.config.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/diagnostics.d.ts +9 -0
- package/dist/component/diagnostics.d.ts.map +1 -0
- package/dist/component/diagnostics.js +47 -0
- package/dist/component/diagnostics.js.map +1 -0
- package/dist/component/errors.d.ts +1 -1
- package/dist/component/errors.d.ts.map +1 -1
- package/dist/component/errors.js.map +1 -1
- package/dist/component/eventStore.d.ts +11 -9
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/eventStore.js +55 -227
- package/dist/component/eventStore.js.map +1 -1
- package/dist/component/geo.d.ts +71 -0
- package/dist/component/geo.d.ts.map +1 -0
- package/dist/component/geo.js +610 -0
- package/dist/component/geo.js.map +1 -0
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +168 -17
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +272 -32
- 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 +117 -8
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +641 -79
- 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 +9 -4
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +202 -53
- package/dist/component/schema.js +113 -16
- 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/validators.d.ts +29 -51
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +11 -12
- package/dist/component/validators.js.map +1 -1
- 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 +7 -6
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/benchmarks/2026-08-20-realistic.md +5 -5
- 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 +29 -7
- package/scripts/benchmark-ingest.mjs +81 -32
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +9 -5
- package/src/component/_generated/component.ts +224 -1
- package/src/component/_generated/server.ts +4 -0
- package/src/component/cardinality.ts +116 -0
- package/src/component/constants.ts +10 -0
- package/src/component/convex.config.ts +11 -5
- package/src/component/coverage.ts +25 -0
- package/src/component/diagnostics.ts +64 -0
- package/src/component/errors.ts +2 -1
- package/src/component/eventStore.ts +78 -280
- package/src/component/geo.ts +779 -0
- package/src/component/http.ts +259 -22
- package/src/component/ingest.ts +436 -27
- package/src/component/live.ts +8 -3
- package/src/component/reports.ts +895 -87
- package/src/component/retention.ts +624 -88
- package/src/component/sanitize.ts +15 -4
- package/src/component/schema.ts +125 -16
- package/src/component/sites.ts +5 -1
- package/src/component/validators.ts +12 -13
- package/src/test.ts +0 -2
- package/dist/component/migrations.d.ts +0 -17
- package/dist/component/migrations.d.ts.map +0 -1
- package/dist/component/migrations.js +0 -44
- package/dist/component/migrations.js.map +0 -1
- package/src/component/migrations.ts +0 -52
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(),
|
|
@@ -97,7 +102,6 @@ export const ingestBatch = mutation({
|
|
|
97
102
|
siteId: args.siteId,
|
|
98
103
|
sessionId: group.sessionId,
|
|
99
104
|
bucketStart: group.bucketStart,
|
|
100
|
-
candidateEventIds: group.eventIds,
|
|
101
105
|
});
|
|
102
106
|
const newCount = [...group.eventIds].filter((eventId) => !eventIds.has(eventId)).length;
|
|
103
107
|
if (newCount > 0 &&
|
|
@@ -164,7 +168,7 @@ export const ingestBatch = mutation({
|
|
|
164
168
|
.withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
|
|
165
169
|
.take(MAX_FUNNELS_PER_SITE);
|
|
166
170
|
}
|
|
167
|
-
await accountEvent(ctx, {
|
|
171
|
+
const session = await accountEvent(ctx, {
|
|
168
172
|
siteId: args.siteId,
|
|
169
173
|
event,
|
|
170
174
|
context,
|
|
@@ -172,18 +176,20 @@ export const ingestBatch = mutation({
|
|
|
172
176
|
goals: canMatchDefinition ? goals ?? [] : [],
|
|
173
177
|
funnels: canMatchDefinition ? funnels ?? [] : [],
|
|
174
178
|
aggregateDeltas,
|
|
179
|
+
reportRollupDeltas,
|
|
175
180
|
recordTelemetry: true,
|
|
176
181
|
financialConversion: false,
|
|
177
182
|
trustedGoalKey: undefined,
|
|
178
183
|
now,
|
|
179
184
|
batchState,
|
|
180
185
|
});
|
|
181
|
-
bufferEventBatch(args.siteId, event, batchState.eventBatches);
|
|
186
|
+
bufferEventBatch(args.siteId, event, batchState.eventBatches, session.country ?? "unknown");
|
|
182
187
|
accepted += 1;
|
|
183
188
|
}
|
|
184
189
|
await flushBatchState(ctx, batchState);
|
|
185
190
|
await flushSessionRateLimits(ctx, sessionRateWindows, now);
|
|
186
191
|
await flushAggregates(ctx, aggregateDeltas);
|
|
192
|
+
await flushReportRollups(ctx, reportRollupDeltas);
|
|
187
193
|
return { accepted, duplicates, rejected };
|
|
188
194
|
},
|
|
189
195
|
});
|
|
@@ -261,7 +267,8 @@ export const trackConversion = mutation({
|
|
|
261
267
|
device: existingTelemetry?.device ?? existingSession?.device,
|
|
262
268
|
};
|
|
263
269
|
const aggregateDeltas = new Map();
|
|
264
|
-
|
|
270
|
+
const reportRollupDeltas = createReportRollupDeltas();
|
|
271
|
+
const session = await accountEvent(ctx, {
|
|
265
272
|
siteId: args.siteId,
|
|
266
273
|
event,
|
|
267
274
|
context: conversionContext,
|
|
@@ -269,15 +276,18 @@ export const trackConversion = mutation({
|
|
|
269
276
|
goals: [],
|
|
270
277
|
funnels: [],
|
|
271
278
|
aggregateDeltas,
|
|
279
|
+
reportRollupDeltas,
|
|
272
280
|
recordTelemetry: existingTelemetry === null && !telemetry.saturated,
|
|
273
281
|
financialConversion: true,
|
|
274
282
|
trustedGoalKey,
|
|
275
283
|
now: Date.now(),
|
|
276
284
|
});
|
|
277
285
|
if (!existingTelemetry && !telemetry.saturated) {
|
|
278
|
-
await insertEventBatch(ctx, args.siteId, event);
|
|
286
|
+
await insertEventBatch(ctx, args.siteId, event, session.country ?? "unknown");
|
|
279
287
|
}
|
|
280
288
|
await flushAggregates(ctx, aggregateDeltas);
|
|
289
|
+
await flushReportRollups(ctx, reportRollupDeltas);
|
|
290
|
+
await ensureAnalyticsControl(ctx, args.siteId, Date.now());
|
|
281
291
|
return { accepted: 1, duplicates: 0, rejected: 0 };
|
|
282
292
|
},
|
|
283
293
|
});
|
|
@@ -347,7 +357,7 @@ function withoutSystemFields(document) {
|
|
|
347
357
|
async function accountEvent(ctx, input) {
|
|
348
358
|
const { event } = input;
|
|
349
359
|
const session = input.recordTelemetry
|
|
350
|
-
? 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)
|
|
351
361
|
: await accountTrustedSession(ctx, input.existingSession, event);
|
|
352
362
|
const matchedGoalKeys = [];
|
|
353
363
|
const matchingGoals = input.goals.filter((goal) => goal.active && goalMatches(goal, event));
|
|
@@ -394,20 +404,21 @@ async function accountEvent(ctx, input) {
|
|
|
394
404
|
timestamp: event.timestamp,
|
|
395
405
|
valueCents: goal.valueCents ?? 0,
|
|
396
406
|
});
|
|
407
|
+
foldGoalRollup(input.reportRollupDeltas, input.siteId, event, goal);
|
|
397
408
|
matchedGoalKeys.push(goal.key);
|
|
398
409
|
}
|
|
399
410
|
for (const funnel of input.funnels) {
|
|
400
411
|
if (funnel.active) {
|
|
401
|
-
await advanceFunnel(ctx, input.siteId, funnel, event, input.batchState);
|
|
412
|
+
await advanceFunnel(ctx, input.siteId, funnel, event, input.reportRollupDeltas, input.batchState);
|
|
402
413
|
}
|
|
403
414
|
}
|
|
404
415
|
if (input.financialConversion) {
|
|
405
|
-
await accountConversion(ctx, input.siteId, session, event, input.trustedGoalKey);
|
|
416
|
+
await accountConversion(ctx, input.siteId, session, event, input.trustedGoalKey, input.reportRollupDeltas);
|
|
406
417
|
}
|
|
407
418
|
const dimensions = [
|
|
408
419
|
{ type: "source", value: session.source },
|
|
409
420
|
{ type: "page", value: event.path },
|
|
410
|
-
{ type: "country", value:
|
|
421
|
+
{ type: "country", value: session.country ?? "unknown" },
|
|
411
422
|
{ type: "device", value: input.context.device ?? "unknown" },
|
|
412
423
|
{ type: "browser", value: input.context.browser ?? "Other" },
|
|
413
424
|
{
|
|
@@ -421,6 +432,7 @@ async function accountEvent(ctx, input) {
|
|
|
421
432
|
event.timestamp >= input.now - LIVE_SESSION_TTL_MS) {
|
|
422
433
|
await updateLiveSession(ctx, input.siteId, event, input.context, session, input.now, input.batchState);
|
|
423
434
|
}
|
|
435
|
+
return session;
|
|
424
436
|
}
|
|
425
437
|
async function accountTrustedSession(ctx, existing, event) {
|
|
426
438
|
if (!existing) {
|
|
@@ -438,6 +450,10 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
438
450
|
isNewVisitorDay: false,
|
|
439
451
|
startedAt: existing.startedAt,
|
|
440
452
|
source: existing.source,
|
|
453
|
+
country: existing.country,
|
|
454
|
+
city: existing.city,
|
|
455
|
+
latitude: existing.latitude,
|
|
456
|
+
longitude: existing.longitude,
|
|
441
457
|
pageviewCount: existing.pageviewCount,
|
|
442
458
|
eventCount: existing.eventCount,
|
|
443
459
|
affiliateId: existing.affiliateId,
|
|
@@ -446,13 +462,16 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
446
462
|
affiliateExpiresAt: existing.affiliateExpiresAt,
|
|
447
463
|
};
|
|
448
464
|
}
|
|
449
|
-
async function upsertSession(ctx, siteId, event, context, existing, financialConversion, batchState) {
|
|
465
|
+
async function upsertSession(ctx, siteId, event, context, existing, financialConversion, reportRollupDeltas, batchState) {
|
|
450
466
|
const source = sourceFromReferrer(event.referrer);
|
|
451
467
|
const utmSource = stringProperty(event.properties, "utm_source");
|
|
452
468
|
const utmMedium = stringProperty(event.properties, "utm_medium");
|
|
453
469
|
const utmCampaign = stringProperty(event.properties, "utm_campaign");
|
|
454
470
|
const attribution = await attributionForEvent(ctx, siteId, event, existing, batchState);
|
|
455
471
|
if (existing) {
|
|
472
|
+
if (attribution && existing.affiliateId !== attribution.affiliateId) {
|
|
473
|
+
foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution);
|
|
474
|
+
}
|
|
456
475
|
const startedAt = Math.min(existing.startedAt, event.timestamp);
|
|
457
476
|
const lastSeenAt = Math.max(existing.lastSeenAt, event.timestamp);
|
|
458
477
|
const updates = {
|
|
@@ -499,6 +518,10 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
499
518
|
Math.floor(event.timestamp / DAY_MS),
|
|
500
519
|
startedAt,
|
|
501
520
|
source: existing.source,
|
|
521
|
+
country: context.country ?? existing.country,
|
|
522
|
+
city: context.city ?? existing.city,
|
|
523
|
+
latitude: context.latitude ?? existing.latitude,
|
|
524
|
+
longitude: context.longitude ?? existing.longitude,
|
|
502
525
|
pageviewCount: existing.pageviewCount + (event.type === "pageview" ? 1 : 0),
|
|
503
526
|
eventCount: existing.eventCount + 1,
|
|
504
527
|
...(attribution ?? {}),
|
|
@@ -551,6 +574,9 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
551
574
|
...(attribution ?? {}),
|
|
552
575
|
};
|
|
553
576
|
const sessionId = await ctx.db.insert("sessions", fields);
|
|
577
|
+
if (attribution) {
|
|
578
|
+
foldAffiliateVisitRollup(reportRollupDeltas, siteId, event, attribution);
|
|
579
|
+
}
|
|
554
580
|
if (batchState) {
|
|
555
581
|
const state = batchState.sessions.get(event.sessionId);
|
|
556
582
|
if (!state)
|
|
@@ -570,6 +596,10 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
570
596
|
Math.floor(event.timestamp / DAY_MS),
|
|
571
597
|
startedAt: event.timestamp,
|
|
572
598
|
source,
|
|
599
|
+
country: context.country,
|
|
600
|
+
city: context.city,
|
|
601
|
+
latitude: context.latitude,
|
|
602
|
+
longitude: context.longitude,
|
|
573
603
|
pageviewCount: event.type === "pageview" ? 1 : 0,
|
|
574
604
|
eventCount: 1,
|
|
575
605
|
...(attribution ?? {}),
|
|
@@ -717,20 +747,21 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
717
747
|
isNewVisitorDay: false,
|
|
718
748
|
startedAt,
|
|
719
749
|
source: session.source,
|
|
750
|
+
country: context.country ?? session.country,
|
|
751
|
+
city: context.city ?? session.city,
|
|
752
|
+
latitude: context.latitude ?? session.latitude,
|
|
753
|
+
longitude: context.longitude ?? session.longitude,
|
|
720
754
|
pageviewCount: session.pageviewCount,
|
|
721
755
|
eventCount: session.eventCount,
|
|
722
756
|
affiliateSlug: session.affiliateSlug,
|
|
723
757
|
}, now, batchState);
|
|
724
758
|
}
|
|
725
|
-
async function consumeSiteRateLimit(ctx, siteId, events, now) {
|
|
759
|
+
async function consumeSiteRateLimit(ctx, siteId, events, eventByteCounts, now) {
|
|
726
760
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
727
761
|
const increments = new Map();
|
|
728
|
-
const eventByteCounts = [];
|
|
729
|
-
const encoder = new TextEncoder();
|
|
730
762
|
for (let index = 0; index < events.length; index += 1) {
|
|
731
763
|
const event = events[index];
|
|
732
|
-
const byteCount =
|
|
733
|
-
eventByteCounts.push(byteCount);
|
|
764
|
+
const byteCount = eventByteCounts[index];
|
|
734
765
|
const shard = stableHash(event.sessionId) % SITE_INGEST_SHARDS;
|
|
735
766
|
const current = increments.get(shard) ?? { eventCount: 0, byteCount: 0 };
|
|
736
767
|
current.eventCount += 1;
|
|
@@ -787,7 +818,6 @@ async function consumeSiteRateLimit(ctx, siteId, events, now) {
|
|
|
787
818
|
});
|
|
788
819
|
}
|
|
789
820
|
}
|
|
790
|
-
return eventByteCounts;
|
|
791
821
|
}
|
|
792
822
|
async function consumeRateLimit(ctx, siteId, sessionId, byteCount, now, states) {
|
|
793
823
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
@@ -855,6 +885,10 @@ async function updateLiveSession(ctx, siteId, event, context, session, now, batc
|
|
|
855
885
|
.withIndex("by_siteId_and_sessionId", (range) => range.eq("siteId", siteId).eq("sessionId", event.sessionId))
|
|
856
886
|
.unique();
|
|
857
887
|
const deadline = now + LIVE_SESSION_TTL_MS;
|
|
888
|
+
const country = context.country ?? session.country;
|
|
889
|
+
const city = context.city ?? session.city;
|
|
890
|
+
const latitude = context.latitude ?? session.latitude;
|
|
891
|
+
const longitude = context.longitude ?? session.longitude;
|
|
858
892
|
if (existing) {
|
|
859
893
|
const recentHeartbeatEventIds = event.type === "heartbeat"
|
|
860
894
|
? [
|
|
@@ -870,10 +904,10 @@ async function updateLiveSession(ctx, siteId, event, context, session, now, batc
|
|
|
870
904
|
path: event.path,
|
|
871
905
|
lastSeenAt: Math.max(existing.lastSeenAt, event.timestamp),
|
|
872
906
|
expiresAt: deadline,
|
|
873
|
-
...(
|
|
874
|
-
...(
|
|
875
|
-
...(
|
|
876
|
-
...(
|
|
907
|
+
...(country ? { country } : {}),
|
|
908
|
+
...(city ? { city } : {}),
|
|
909
|
+
...(latitude !== undefined ? { latitude } : {}),
|
|
910
|
+
...(longitude !== undefined ? { longitude } : {}),
|
|
877
911
|
browser: context.browser ?? existing.browser,
|
|
878
912
|
os: context.os ?? existing.os,
|
|
879
913
|
source: session.source,
|
|
@@ -904,10 +938,10 @@ async function updateLiveSession(ctx, siteId, event, context, session, now, batc
|
|
|
904
938
|
startedAt: event.timestamp,
|
|
905
939
|
lastSeenAt: event.timestamp,
|
|
906
940
|
expiresAt: deadline,
|
|
907
|
-
...(
|
|
908
|
-
...(
|
|
909
|
-
...(
|
|
910
|
-
...(
|
|
941
|
+
...(country ? { country } : {}),
|
|
942
|
+
...(city ? { city } : {}),
|
|
943
|
+
...(latitude !== undefined ? { latitude } : {}),
|
|
944
|
+
...(longitude !== undefined ? { longitude } : {}),
|
|
911
945
|
browser: context.browser ?? "Other",
|
|
912
946
|
os: context.os ?? "Other",
|
|
913
947
|
source: session.source,
|
|
@@ -937,9 +971,14 @@ async function ensureLiveSweep(ctx, siteId, now, batchState) {
|
|
|
937
971
|
.query("liveSweepStates")
|
|
938
972
|
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
939
973
|
.unique();
|
|
940
|
-
if (!existing) {
|
|
974
|
+
if (!existing || existing.scheduledAt <= now) {
|
|
941
975
|
const scheduledAt = now + LIVE_SWEEP_INTERVAL_MS;
|
|
942
|
-
|
|
976
|
+
if (existing) {
|
|
977
|
+
await ctx.db.patch("liveSweepStates", existing._id, { scheduledAt });
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
await ctx.db.insert("liveSweepStates", { siteId, scheduledAt });
|
|
981
|
+
}
|
|
943
982
|
await ctx.scheduler.runAt(scheduledAt, internal.live.sweep, {
|
|
944
983
|
siteId,
|
|
945
984
|
scheduledAt,
|
|
@@ -953,7 +992,7 @@ function goalMatches(goal, event) {
|
|
|
953
992
|
}
|
|
954
993
|
return event.type === "custom" && event.name === goal.matchValue;
|
|
955
994
|
}
|
|
956
|
-
async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
995
|
+
async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, batchState) {
|
|
957
996
|
const cacheKey = JSON.stringify([funnel._id, event.visitorId]);
|
|
958
997
|
let progressState = batchState?.funnelProgress.get(cacheKey);
|
|
959
998
|
if (batchState && !progressState) {
|
|
@@ -997,6 +1036,7 @@ async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
|
997
1036
|
};
|
|
998
1037
|
}
|
|
999
1038
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, 0, false);
|
|
1039
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1000
1040
|
return;
|
|
1001
1041
|
}
|
|
1002
1042
|
if (progress.completedAt !== undefined || event.timestamp < progress.lastStepAt)
|
|
@@ -1026,6 +1066,7 @@ async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
|
1026
1066
|
});
|
|
1027
1067
|
}
|
|
1028
1068
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, 0, false);
|
|
1069
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1029
1070
|
return;
|
|
1030
1071
|
}
|
|
1031
1072
|
const nextStep = funnel.steps[progress.currentStep];
|
|
@@ -1047,6 +1088,7 @@ async function advanceFunnel(ctx, siteId, funnel, event, batchState) {
|
|
|
1047
1088
|
await ctx.db.patch("funnelProgress", progress._id, updates);
|
|
1048
1089
|
}
|
|
1049
1090
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1091
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, progress.currentStep, completed);
|
|
1050
1092
|
}
|
|
1051
1093
|
function stepMatches(step, event) {
|
|
1052
1094
|
return step.type === "pageview"
|
|
@@ -1083,7 +1125,7 @@ async function incrementFunnelStep(ctx, siteId, funnelId, event, step, completed
|
|
|
1083
1125
|
});
|
|
1084
1126
|
}
|
|
1085
1127
|
}
|
|
1086
|
-
async function accountConversion(ctx, siteId, session, event, trustedGoalKey) {
|
|
1128
|
+
async function accountConversion(ctx, siteId, session, event, trustedGoalKey, reportRollupDeltas) {
|
|
1087
1129
|
const revenueCents = event.revenueCents ?? 0;
|
|
1088
1130
|
const currency = event.currency ?? "USD";
|
|
1089
1131
|
const candidate = attributionSnapshot(session);
|
|
@@ -1112,6 +1154,7 @@ async function accountConversion(ctx, siteId, session, event, trustedGoalKey) {
|
|
|
1112
1154
|
commissionCents,
|
|
1113
1155
|
});
|
|
1114
1156
|
if (commissionAffiliate && attribution) {
|
|
1157
|
+
foldAffiliateConversionRollup(reportRollupDeltas, siteId, event, commissionAffiliate, attribution.affiliateSlug, revenueCents, commissionCents);
|
|
1115
1158
|
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1116
1159
|
const shard = stableHash(event.sessionId) % SECONDARY_AGGREGATE_SHARDS;
|
|
1117
1160
|
const stats = await ctx.db
|
|
@@ -1143,6 +1186,188 @@ async function accountConversion(ctx, siteId, session, event, trustedGoalKey) {
|
|
|
1143
1186
|
}
|
|
1144
1187
|
}
|
|
1145
1188
|
}
|
|
1189
|
+
function createReportRollupDeltas() {
|
|
1190
|
+
return {
|
|
1191
|
+
goals: new Map(),
|
|
1192
|
+
funnels: new Map(),
|
|
1193
|
+
affiliates: new Map(),
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function reportRollupCoordinates(siteId, definitionId, timestamp, visitorId) {
|
|
1197
|
+
const bucketStart = Math.floor(timestamp / DAY_MS) * DAY_MS;
|
|
1198
|
+
const shard = stableHash(visitorId) % REPORT_ROLLUP_SHARDS;
|
|
1199
|
+
return {
|
|
1200
|
+
bucketStart,
|
|
1201
|
+
shard,
|
|
1202
|
+
key: `${siteId}:${definitionId}:${bucketStart}:${shard}`,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function foldGoalRollup(deltas, siteId, event, goal) {
|
|
1206
|
+
const coordinates = reportRollupCoordinates(siteId, String(goal._id), event.timestamp, event.visitorId);
|
|
1207
|
+
const current = deltas.goals.get(coordinates.key) ?? {
|
|
1208
|
+
siteId,
|
|
1209
|
+
goalId: goal._id,
|
|
1210
|
+
bucketStart: coordinates.bucketStart,
|
|
1211
|
+
shard: coordinates.shard,
|
|
1212
|
+
goalKey: goal.key,
|
|
1213
|
+
completions: 0,
|
|
1214
|
+
valueCents: 0,
|
|
1215
|
+
};
|
|
1216
|
+
current.completions += 1;
|
|
1217
|
+
current.valueCents += goal.valueCents ?? 0;
|
|
1218
|
+
deltas.goals.set(coordinates.key, current);
|
|
1219
|
+
}
|
|
1220
|
+
function foldFunnelRollup(deltas, siteId, funnelId, event, step, completed) {
|
|
1221
|
+
const coordinates = reportRollupCoordinates(siteId, String(funnelId), event.timestamp, event.visitorId);
|
|
1222
|
+
const rollup = deltas.funnels.get(coordinates.key) ?? {
|
|
1223
|
+
siteId,
|
|
1224
|
+
funnelId,
|
|
1225
|
+
bucketStart: coordinates.bucketStart,
|
|
1226
|
+
shard: coordinates.shard,
|
|
1227
|
+
steps: new Map(),
|
|
1228
|
+
};
|
|
1229
|
+
const current = rollup.steps.get(step) ?? {
|
|
1230
|
+
step,
|
|
1231
|
+
entrants: 0,
|
|
1232
|
+
completions: 0,
|
|
1233
|
+
};
|
|
1234
|
+
current.entrants += 1;
|
|
1235
|
+
current.completions += Number(completed);
|
|
1236
|
+
rollup.steps.set(step, current);
|
|
1237
|
+
deltas.funnels.set(coordinates.key, rollup);
|
|
1238
|
+
}
|
|
1239
|
+
function foldAffiliateVisitRollup(deltas, siteId, event, attribution) {
|
|
1240
|
+
const coordinates = reportRollupCoordinates(siteId, String(attribution.affiliateId), event.timestamp, event.visitorId);
|
|
1241
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1242
|
+
siteId,
|
|
1243
|
+
affiliateId: attribution.affiliateId,
|
|
1244
|
+
bucketStart: coordinates.bucketStart,
|
|
1245
|
+
shard: coordinates.shard,
|
|
1246
|
+
affiliateSlug: attribution.affiliateSlug,
|
|
1247
|
+
visits: 0,
|
|
1248
|
+
visitorSketch: createVisitorSketch(),
|
|
1249
|
+
conversions: 0,
|
|
1250
|
+
revenueCents: 0,
|
|
1251
|
+
commissionCents: 0,
|
|
1252
|
+
};
|
|
1253
|
+
current.visits += 1;
|
|
1254
|
+
current.visitorSketch = addVisitor(current.visitorSketch, event.visitorId);
|
|
1255
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1256
|
+
}
|
|
1257
|
+
function foldAffiliateConversionRollup(deltas, siteId, event, affiliate, affiliateSlug, revenueCents, commissionCents) {
|
|
1258
|
+
const coordinates = reportRollupCoordinates(siteId, String(affiliate._id), event.timestamp, event.visitorId);
|
|
1259
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1260
|
+
siteId,
|
|
1261
|
+
affiliateId: affiliate._id,
|
|
1262
|
+
bucketStart: coordinates.bucketStart,
|
|
1263
|
+
shard: coordinates.shard,
|
|
1264
|
+
affiliateSlug,
|
|
1265
|
+
visits: 0,
|
|
1266
|
+
visitorSketch: createVisitorSketch(),
|
|
1267
|
+
conversions: 0,
|
|
1268
|
+
revenueCents: 0,
|
|
1269
|
+
commissionCents: 0,
|
|
1270
|
+
};
|
|
1271
|
+
current.conversions += 1;
|
|
1272
|
+
current.revenueCents += revenueCents;
|
|
1273
|
+
current.commissionCents += commissionCents;
|
|
1274
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1275
|
+
}
|
|
1276
|
+
async function flushReportRollups(ctx, deltas) {
|
|
1277
|
+
for (const delta of deltas.goals.values()) {
|
|
1278
|
+
const existing = await ctx.db
|
|
1279
|
+
.query("goalDailyRollups")
|
|
1280
|
+
.withIndex("by_siteId_and_goalId_and_bucketStart_and_shard", (range) => range
|
|
1281
|
+
.eq("siteId", delta.siteId)
|
|
1282
|
+
.eq("goalId", delta.goalId)
|
|
1283
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1284
|
+
.eq("shard", delta.shard))
|
|
1285
|
+
.unique();
|
|
1286
|
+
const fields = {
|
|
1287
|
+
siteId: delta.siteId,
|
|
1288
|
+
goalId: delta.goalId,
|
|
1289
|
+
bucketStart: delta.bucketStart,
|
|
1290
|
+
shard: delta.shard,
|
|
1291
|
+
generation: 1,
|
|
1292
|
+
goalKey: delta.goalKey,
|
|
1293
|
+
completions: (existing?.completions ?? 0) + delta.completions,
|
|
1294
|
+
valueCents: (existing?.valueCents ?? 0) + delta.valueCents,
|
|
1295
|
+
};
|
|
1296
|
+
if (existing) {
|
|
1297
|
+
await ctx.db.replace("goalDailyRollups", existing._id, fields);
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
await ctx.db.insert("goalDailyRollups", fields);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
for (const delta of deltas.funnels.values()) {
|
|
1304
|
+
const existing = await ctx.db
|
|
1305
|
+
.query("funnelDailyRollups")
|
|
1306
|
+
.withIndex("by_siteId_and_funnelId_and_bucketStart_and_shard", (range) => range
|
|
1307
|
+
.eq("siteId", delta.siteId)
|
|
1308
|
+
.eq("funnelId", delta.funnelId)
|
|
1309
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1310
|
+
.eq("shard", delta.shard))
|
|
1311
|
+
.unique();
|
|
1312
|
+
const steps = existing?.steps.map((step) => ({ ...step })) ?? [];
|
|
1313
|
+
for (const addition of delta.steps.values()) {
|
|
1314
|
+
const current = steps.find((step) => step.step === addition.step);
|
|
1315
|
+
if (current) {
|
|
1316
|
+
current.entrants += addition.entrants;
|
|
1317
|
+
current.completions += addition.completions;
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
if (steps.length >= MAX_FUNNEL_STEPS) {
|
|
1321
|
+
fail("LIMIT_EXCEEDED", "daily funnel rollup step limit exceeded");
|
|
1322
|
+
}
|
|
1323
|
+
steps.push(addition);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const fields = {
|
|
1327
|
+
siteId: delta.siteId,
|
|
1328
|
+
funnelId: delta.funnelId,
|
|
1329
|
+
bucketStart: delta.bucketStart,
|
|
1330
|
+
shard: delta.shard,
|
|
1331
|
+
generation: 1,
|
|
1332
|
+
steps,
|
|
1333
|
+
};
|
|
1334
|
+
if (existing) {
|
|
1335
|
+
await ctx.db.replace("funnelDailyRollups", existing._id, fields);
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
await ctx.db.insert("funnelDailyRollups", fields);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
for (const delta of deltas.affiliates.values()) {
|
|
1342
|
+
const existing = await ctx.db
|
|
1343
|
+
.query("affiliateDailyRollups")
|
|
1344
|
+
.withIndex("by_siteId_and_affiliateId_and_bucketStart_and_shard", (range) => range
|
|
1345
|
+
.eq("siteId", delta.siteId)
|
|
1346
|
+
.eq("affiliateId", delta.affiliateId)
|
|
1347
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1348
|
+
.eq("shard", delta.shard))
|
|
1349
|
+
.unique();
|
|
1350
|
+
const fields = {
|
|
1351
|
+
siteId: delta.siteId,
|
|
1352
|
+
affiliateId: delta.affiliateId,
|
|
1353
|
+
bucketStart: delta.bucketStart,
|
|
1354
|
+
shard: delta.shard,
|
|
1355
|
+
generation: 1,
|
|
1356
|
+
affiliateSlug: delta.affiliateSlug,
|
|
1357
|
+
visits: (existing?.visits ?? 0) + delta.visits,
|
|
1358
|
+
visitorSketch: unionVisitorSketches(existing?.visitorSketch ?? createVisitorSketch(), delta.visitorSketch),
|
|
1359
|
+
conversions: (existing?.conversions ?? 0) + delta.conversions,
|
|
1360
|
+
revenueCents: (existing?.revenueCents ?? 0) + delta.revenueCents,
|
|
1361
|
+
commissionCents: (existing?.commissionCents ?? 0) + delta.commissionCents,
|
|
1362
|
+
};
|
|
1363
|
+
if (existing) {
|
|
1364
|
+
await ctx.db.replace("affiliateDailyRollups", existing._id, fields);
|
|
1365
|
+
}
|
|
1366
|
+
else {
|
|
1367
|
+
await ctx.db.insert("affiliateDailyRollups", fields);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1146
1371
|
function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, recordTelemetry, financialConversion) {
|
|
1147
1372
|
const revenueCents = financialConversion ? event.revenueCents ?? 0 : 0;
|
|
1148
1373
|
const dimensionDeltas = dimensions.map((dimension) => ({
|
|
@@ -1170,6 +1395,7 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1170
1395
|
outboundClicks: 0,
|
|
1171
1396
|
sessions: 0,
|
|
1172
1397
|
visitors: 0,
|
|
1398
|
+
visitorSketch: createVisitorSketch(),
|
|
1173
1399
|
conversions: 0,
|
|
1174
1400
|
revenueCents: 0,
|
|
1175
1401
|
dimensions: [],
|
|
@@ -1188,6 +1414,9 @@ function foldAggregates(aggregateDeltas, siteId, event, session, dimensions, rec
|
|
|
1188
1414
|
? Number(session.isNewVisitorHour)
|
|
1189
1415
|
: Number(session.isNewVisitorDay)
|
|
1190
1416
|
: 0;
|
|
1417
|
+
if (recordTelemetry) {
|
|
1418
|
+
delta.visitorSketch = addVisitor(delta.visitorSketch, event.visitorId);
|
|
1419
|
+
}
|
|
1191
1420
|
delta.conversions += Number(financialConversion);
|
|
1192
1421
|
delta.revenueCents += revenueCents;
|
|
1193
1422
|
delta.dimensions = mergeDimensions(delta.dimensions, dimensionDeltas);
|
|
@@ -1204,6 +1433,13 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1204
1433
|
.eq("shard", delta.shard))
|
|
1205
1434
|
.unique();
|
|
1206
1435
|
const dimensions = mergeDimensions(bucket?.dimensions ?? [], delta.dimensions);
|
|
1436
|
+
const priorSketch = bucket?.visitorSketch;
|
|
1437
|
+
const visitorSketch = priorSketch
|
|
1438
|
+
? unionVisitorSketches(priorSketch, delta.visitorSketch)
|
|
1439
|
+
: delta.visitorSketch;
|
|
1440
|
+
const visitorSketchComplete = bucket
|
|
1441
|
+
? bucket.visitorSketchComplete === true && priorSketch !== undefined
|
|
1442
|
+
: true;
|
|
1207
1443
|
if (bucket) {
|
|
1208
1444
|
await ctx.db.patch("aggregateBuckets", bucket._id, {
|
|
1209
1445
|
events: bucket.events + delta.events,
|
|
@@ -1213,6 +1449,8 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1213
1449
|
outboundClicks: bucket.outboundClicks + delta.outboundClicks,
|
|
1214
1450
|
sessions: bucket.sessions + delta.sessions,
|
|
1215
1451
|
visitors: bucket.visitors + delta.visitors,
|
|
1452
|
+
visitorSketch,
|
|
1453
|
+
visitorSketchComplete,
|
|
1216
1454
|
conversions: bucket.conversions + delta.conversions,
|
|
1217
1455
|
revenueCents: bucket.revenueCents + delta.revenueCents,
|
|
1218
1456
|
dimensions,
|
|
@@ -1231,6 +1469,8 @@ async function flushAggregates(ctx, aggregateDeltas) {
|
|
|
1231
1469
|
outboundClicks: delta.outboundClicks,
|
|
1232
1470
|
sessions: delta.sessions,
|
|
1233
1471
|
visitors: delta.visitors,
|
|
1472
|
+
visitorSketch,
|
|
1473
|
+
visitorSketchComplete,
|
|
1234
1474
|
conversions: delta.conversions,
|
|
1235
1475
|
revenueCents: delta.revenueCents,
|
|
1236
1476
|
dimensions,
|