@iann29/rastro 0.1.0-alpha.3 → 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 +264 -51
- 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 +4 -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 +151 -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 +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/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/http.d.ts.map +1 -1
- package/dist/component/http.js +46 -8
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +238 -18
- 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 +111 -2
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +639 -78
- 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 +187 -1
- package/dist/component/schema.js +100 -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/validators.d.ts +26 -1
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +7 -0
- package/dist/component/validators.js.map +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 +32 -6
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +211 -1
- package/src/component/cardinality.ts +116 -0
- package/src/component/constants.ts +10 -0
- package/src/component/coverage.ts +25 -0
- package/src/component/errors.ts +2 -1
- package/src/component/http.ts +76 -6
- package/src/component/ingest.ts +387 -12
- package/src/component/live.ts +8 -3
- package/src/component/reports.ts +891 -86
- package/src/component/retention.ts +624 -88
- package/src/component/sanitize.ts +15 -4
- package/src/component/schema.ts +110 -0
- package/src/component/sites.ts +5 -1
- package/src/component/validators.ts +8 -0
package/src/component/ingest.ts
CHANGED
|
@@ -18,12 +18,21 @@ import {
|
|
|
18
18
|
MAX_BYTES_PER_SESSION_WINDOW,
|
|
19
19
|
MAX_EVENTS_PER_SESSION_WINDOW,
|
|
20
20
|
MAX_FUNNELS_PER_SITE,
|
|
21
|
+
MAX_FUNNEL_STEPS,
|
|
21
22
|
MAX_GOALS_PER_SITE,
|
|
22
23
|
RATE_LIMIT_WINDOW_MS,
|
|
24
|
+
REPORT_ROLLUP_SHARDS,
|
|
23
25
|
SECONDARY_AGGREGATE_SHARDS,
|
|
24
26
|
SITE_INGEST_SHARDS,
|
|
25
27
|
} from "./constants.js";
|
|
28
|
+
import {
|
|
29
|
+
addVisitor,
|
|
30
|
+
createVisitorSketch,
|
|
31
|
+
unionVisitorSketches,
|
|
32
|
+
type VisitorSketch,
|
|
33
|
+
} from "./cardinality.js";
|
|
26
34
|
import { fail } from "./errors.js";
|
|
35
|
+
import { ensureAnalyticsControl } from "./coverage.js";
|
|
27
36
|
import {
|
|
28
37
|
bufferEventBatch,
|
|
29
38
|
eventBatchKey,
|
|
@@ -36,8 +45,8 @@ import {
|
|
|
36
45
|
} from "./eventStore.js";
|
|
37
46
|
import {
|
|
38
47
|
cleanString,
|
|
39
|
-
ensureBatchBytes,
|
|
40
48
|
isValidMoney,
|
|
49
|
+
measureBatchBytes,
|
|
41
50
|
originAllowed,
|
|
42
51
|
sanitizeContext,
|
|
43
52
|
sanitizeCurrency,
|
|
@@ -81,11 +90,49 @@ type AggregateDelta = {
|
|
|
81
90
|
outboundClicks: number;
|
|
82
91
|
sessions: number;
|
|
83
92
|
visitors: number;
|
|
93
|
+
visitorSketch: VisitorSketch;
|
|
84
94
|
conversions: number;
|
|
85
95
|
revenueCents: number;
|
|
86
96
|
dimensions: Doc<"aggregateBuckets">["dimensions"];
|
|
87
97
|
};
|
|
88
98
|
type AggregateDeltas = Map<string, AggregateDelta>;
|
|
99
|
+
type GoalRollupDelta = {
|
|
100
|
+
siteId: Id<"sites">;
|
|
101
|
+
goalId: Id<"goals">;
|
|
102
|
+
bucketStart: number;
|
|
103
|
+
shard: number;
|
|
104
|
+
goalKey: string;
|
|
105
|
+
completions: number;
|
|
106
|
+
valueCents: number;
|
|
107
|
+
};
|
|
108
|
+
type FunnelRollupDelta = {
|
|
109
|
+
siteId: Id<"sites">;
|
|
110
|
+
funnelId: Id<"funnels">;
|
|
111
|
+
bucketStart: number;
|
|
112
|
+
shard: number;
|
|
113
|
+
steps: Map<number, {
|
|
114
|
+
step: number;
|
|
115
|
+
entrants: number;
|
|
116
|
+
completions: number;
|
|
117
|
+
}>;
|
|
118
|
+
};
|
|
119
|
+
type AffiliateRollupDelta = {
|
|
120
|
+
siteId: Id<"sites">;
|
|
121
|
+
affiliateId: Id<"affiliates">;
|
|
122
|
+
bucketStart: number;
|
|
123
|
+
shard: number;
|
|
124
|
+
affiliateSlug: string;
|
|
125
|
+
visits: number;
|
|
126
|
+
visitorSketch: VisitorSketch;
|
|
127
|
+
conversions: number;
|
|
128
|
+
revenueCents: number;
|
|
129
|
+
commissionCents: number;
|
|
130
|
+
};
|
|
131
|
+
type ReportRollupDeltas = {
|
|
132
|
+
goals: Map<string, GoalRollupDelta>;
|
|
133
|
+
funnels: Map<string, FunnelRollupDelta>;
|
|
134
|
+
affiliates: Map<string, AffiliateRollupDelta>;
|
|
135
|
+
};
|
|
89
136
|
type AttributionSnapshot = {
|
|
90
137
|
affiliateId: Id<"affiliates">;
|
|
91
138
|
affiliateSlug: string;
|
|
@@ -161,21 +208,25 @@ export const ingestBatch = mutation({
|
|
|
161
208
|
limit: MAX_BATCH_EVENTS,
|
|
162
209
|
});
|
|
163
210
|
}
|
|
211
|
+
let eventByteCounts: number[];
|
|
164
212
|
try {
|
|
165
|
-
|
|
213
|
+
({ eventByteCounts } = measureBatchBytes(String(args.siteId), args.events));
|
|
166
214
|
} catch {
|
|
167
215
|
fail("BATCH_TOO_LARGE", "batch exceeds 64 KiB", { limitBytes: 65_536 });
|
|
168
216
|
}
|
|
169
217
|
|
|
170
218
|
const now = Date.now();
|
|
171
|
-
|
|
219
|
+
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
220
|
+
await consumeSiteRateLimit(
|
|
172
221
|
ctx,
|
|
173
222
|
args.siteId,
|
|
174
223
|
args.events,
|
|
224
|
+
eventByteCounts,
|
|
175
225
|
now,
|
|
176
226
|
);
|
|
177
227
|
const context = sanitizeContext(args.context);
|
|
178
228
|
const aggregateDeltas: AggregateDeltas = new Map();
|
|
229
|
+
const reportRollupDeltas = createReportRollupDeltas();
|
|
179
230
|
const sessionRateWindows = new Map<string, SessionRateWindowState>();
|
|
180
231
|
const batchState: BatchState = {
|
|
181
232
|
sessions: new Map(),
|
|
@@ -367,6 +418,7 @@ export const ingestBatch = mutation({
|
|
|
367
418
|
goals: canMatchDefinition ? goals ?? [] : [],
|
|
368
419
|
funnels: canMatchDefinition ? funnels ?? [] : [],
|
|
369
420
|
aggregateDeltas,
|
|
421
|
+
reportRollupDeltas,
|
|
370
422
|
recordTelemetry: true,
|
|
371
423
|
financialConversion: false,
|
|
372
424
|
trustedGoalKey: undefined,
|
|
@@ -384,6 +436,7 @@ export const ingestBatch = mutation({
|
|
|
384
436
|
await flushBatchState(ctx, batchState);
|
|
385
437
|
await flushSessionRateLimits(ctx, sessionRateWindows, now);
|
|
386
438
|
await flushAggregates(ctx, aggregateDeltas);
|
|
439
|
+
await flushReportRollups(ctx, reportRollupDeltas);
|
|
387
440
|
return { accepted, duplicates, rejected };
|
|
388
441
|
},
|
|
389
442
|
});
|
|
@@ -475,6 +528,7 @@ export const trackConversion = mutation({
|
|
|
475
528
|
};
|
|
476
529
|
|
|
477
530
|
const aggregateDeltas: AggregateDeltas = new Map();
|
|
531
|
+
const reportRollupDeltas = createReportRollupDeltas();
|
|
478
532
|
const session = await accountEvent(ctx, {
|
|
479
533
|
siteId: args.siteId,
|
|
480
534
|
event,
|
|
@@ -483,6 +537,7 @@ export const trackConversion = mutation({
|
|
|
483
537
|
goals: [],
|
|
484
538
|
funnels: [],
|
|
485
539
|
aggregateDeltas,
|
|
540
|
+
reportRollupDeltas,
|
|
486
541
|
recordTelemetry: existingTelemetry === null && !telemetry.saturated,
|
|
487
542
|
financialConversion: true,
|
|
488
543
|
trustedGoalKey,
|
|
@@ -497,6 +552,8 @@ export const trackConversion = mutation({
|
|
|
497
552
|
);
|
|
498
553
|
}
|
|
499
554
|
await flushAggregates(ctx, aggregateDeltas);
|
|
555
|
+
await flushReportRollups(ctx, reportRollupDeltas);
|
|
556
|
+
await ensureAnalyticsControl(ctx, args.siteId, Date.now());
|
|
500
557
|
return { accepted: 1, duplicates: 0, rejected: 0 };
|
|
501
558
|
},
|
|
502
559
|
});
|
|
@@ -619,6 +676,7 @@ async function accountEvent(
|
|
|
619
676
|
goals: Doc<"goals">[];
|
|
620
677
|
funnels: Doc<"funnels">[];
|
|
621
678
|
aggregateDeltas: AggregateDeltas;
|
|
679
|
+
reportRollupDeltas: ReportRollupDeltas;
|
|
622
680
|
recordTelemetry: boolean;
|
|
623
681
|
financialConversion: boolean;
|
|
624
682
|
trustedGoalKey: string | undefined;
|
|
@@ -635,6 +693,7 @@ async function accountEvent(
|
|
|
635
693
|
input.context,
|
|
636
694
|
input.existingSession,
|
|
637
695
|
input.financialConversion,
|
|
696
|
+
input.reportRollupDeltas,
|
|
638
697
|
input.batchState,
|
|
639
698
|
)
|
|
640
699
|
: await accountTrustedSession(ctx, input.existingSession, event);
|
|
@@ -691,12 +750,25 @@ async function accountEvent(
|
|
|
691
750
|
timestamp: event.timestamp,
|
|
692
751
|
valueCents: goal.valueCents ?? 0,
|
|
693
752
|
});
|
|
753
|
+
foldGoalRollup(
|
|
754
|
+
input.reportRollupDeltas,
|
|
755
|
+
input.siteId,
|
|
756
|
+
event,
|
|
757
|
+
goal,
|
|
758
|
+
);
|
|
694
759
|
matchedGoalKeys.push(goal.key);
|
|
695
760
|
}
|
|
696
761
|
|
|
697
762
|
for (const funnel of input.funnels) {
|
|
698
763
|
if (funnel.active) {
|
|
699
|
-
await advanceFunnel(
|
|
764
|
+
await advanceFunnel(
|
|
765
|
+
ctx,
|
|
766
|
+
input.siteId,
|
|
767
|
+
funnel,
|
|
768
|
+
event,
|
|
769
|
+
input.reportRollupDeltas,
|
|
770
|
+
input.batchState,
|
|
771
|
+
);
|
|
700
772
|
}
|
|
701
773
|
}
|
|
702
774
|
|
|
@@ -707,6 +779,7 @@ async function accountEvent(
|
|
|
707
779
|
session,
|
|
708
780
|
event,
|
|
709
781
|
input.trustedGoalKey,
|
|
782
|
+
input.reportRollupDeltas,
|
|
710
783
|
);
|
|
711
784
|
}
|
|
712
785
|
|
|
@@ -789,6 +862,7 @@ async function upsertSession(
|
|
|
789
862
|
context: IngestContext,
|
|
790
863
|
existing: CachedDocument<"sessions"> | null,
|
|
791
864
|
financialConversion: boolean,
|
|
865
|
+
reportRollupDeltas: ReportRollupDeltas,
|
|
792
866
|
batchState?: BatchState,
|
|
793
867
|
): Promise<SessionState> {
|
|
794
868
|
const source = sourceFromReferrer(event.referrer);
|
|
@@ -803,6 +877,14 @@ async function upsertSession(
|
|
|
803
877
|
batchState,
|
|
804
878
|
);
|
|
805
879
|
if (existing) {
|
|
880
|
+
if (attribution && existing.affiliateId !== attribution.affiliateId) {
|
|
881
|
+
foldAffiliateVisitRollup(
|
|
882
|
+
reportRollupDeltas,
|
|
883
|
+
siteId,
|
|
884
|
+
event,
|
|
885
|
+
attribution,
|
|
886
|
+
);
|
|
887
|
+
}
|
|
806
888
|
const startedAt = Math.min(existing.startedAt, event.timestamp);
|
|
807
889
|
const lastSeenAt = Math.max(existing.lastSeenAt, event.timestamp);
|
|
808
890
|
const updates = {
|
|
@@ -912,6 +994,14 @@ async function upsertSession(
|
|
|
912
994
|
...(attribution ?? {}),
|
|
913
995
|
};
|
|
914
996
|
const sessionId = await ctx.db.insert("sessions", fields);
|
|
997
|
+
if (attribution) {
|
|
998
|
+
foldAffiliateVisitRollup(
|
|
999
|
+
reportRollupDeltas,
|
|
1000
|
+
siteId,
|
|
1001
|
+
event,
|
|
1002
|
+
attribution,
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
915
1005
|
if (batchState) {
|
|
916
1006
|
const state = batchState.sessions.get(event.sessionId);
|
|
917
1007
|
if (!state) throw new Error("session cache was not initialized");
|
|
@@ -1145,17 +1235,15 @@ async function consumeSiteRateLimit(
|
|
|
1145
1235
|
ctx: MutationCtx,
|
|
1146
1236
|
siteId: Id<"sites">,
|
|
1147
1237
|
events: TrackerEvent[],
|
|
1238
|
+
eventByteCounts: number[],
|
|
1148
1239
|
now: number,
|
|
1149
|
-
): Promise<
|
|
1240
|
+
): Promise<void> {
|
|
1150
1241
|
const windowStart =
|
|
1151
1242
|
Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
1152
1243
|
const increments = new Map<number, { eventCount: number; byteCount: number }>();
|
|
1153
|
-
const eventByteCounts: number[] = [];
|
|
1154
|
-
const encoder = new TextEncoder();
|
|
1155
1244
|
for (let index = 0; index < events.length; index += 1) {
|
|
1156
1245
|
const event = events[index];
|
|
1157
|
-
const byteCount =
|
|
1158
|
-
eventByteCounts.push(byteCount);
|
|
1246
|
+
const byteCount = eventByteCounts[index];
|
|
1159
1247
|
const shard = stableHash(event.sessionId) % SITE_INGEST_SHARDS;
|
|
1160
1248
|
const current = increments.get(shard) ?? { eventCount: 0, byteCount: 0 };
|
|
1161
1249
|
current.eventCount += 1;
|
|
@@ -1225,7 +1313,6 @@ async function consumeSiteRateLimit(
|
|
|
1225
1313
|
});
|
|
1226
1314
|
}
|
|
1227
1315
|
}
|
|
1228
|
-
return eventByteCounts;
|
|
1229
1316
|
}
|
|
1230
1317
|
|
|
1231
1318
|
async function consumeRateLimit(
|
|
@@ -1410,9 +1497,13 @@ async function ensureLiveSweep(
|
|
|
1410
1497
|
.query("liveSweepStates")
|
|
1411
1498
|
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
1412
1499
|
.unique();
|
|
1413
|
-
if (!existing) {
|
|
1500
|
+
if (!existing || existing.scheduledAt <= now) {
|
|
1414
1501
|
const scheduledAt = now + LIVE_SWEEP_INTERVAL_MS;
|
|
1415
|
-
|
|
1502
|
+
if (existing) {
|
|
1503
|
+
await ctx.db.patch("liveSweepStates", existing._id, { scheduledAt });
|
|
1504
|
+
} else {
|
|
1505
|
+
await ctx.db.insert("liveSweepStates", { siteId, scheduledAt });
|
|
1506
|
+
}
|
|
1416
1507
|
await ctx.scheduler.runAt(scheduledAt, internal.live.sweep, {
|
|
1417
1508
|
siteId,
|
|
1418
1509
|
scheduledAt,
|
|
@@ -1433,6 +1524,7 @@ async function advanceFunnel(
|
|
|
1433
1524
|
siteId: Id<"sites">,
|
|
1434
1525
|
funnel: Doc<"funnels">,
|
|
1435
1526
|
event: TrackerEvent,
|
|
1527
|
+
reportRollupDeltas: ReportRollupDeltas,
|
|
1436
1528
|
batchState?: BatchState,
|
|
1437
1529
|
) {
|
|
1438
1530
|
const cacheKey = JSON.stringify([funnel._id, event.visitorId]);
|
|
@@ -1481,6 +1573,7 @@ async function advanceFunnel(
|
|
|
1481
1573
|
};
|
|
1482
1574
|
}
|
|
1483
1575
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, 0, false);
|
|
1576
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1484
1577
|
return;
|
|
1485
1578
|
}
|
|
1486
1579
|
if (progress.completedAt !== undefined || event.timestamp < progress.lastStepAt) return;
|
|
@@ -1507,6 +1600,7 @@ async function advanceFunnel(
|
|
|
1507
1600
|
});
|
|
1508
1601
|
}
|
|
1509
1602
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, 0, false);
|
|
1603
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1510
1604
|
return;
|
|
1511
1605
|
}
|
|
1512
1606
|
const nextStep = funnel.steps[progress.currentStep];
|
|
@@ -1533,6 +1627,14 @@ async function advanceFunnel(
|
|
|
1533
1627
|
progress.currentStep,
|
|
1534
1628
|
completed,
|
|
1535
1629
|
);
|
|
1630
|
+
foldFunnelRollup(
|
|
1631
|
+
reportRollupDeltas,
|
|
1632
|
+
siteId,
|
|
1633
|
+
funnel._id,
|
|
1634
|
+
event,
|
|
1635
|
+
progress.currentStep,
|
|
1636
|
+
completed,
|
|
1637
|
+
);
|
|
1536
1638
|
}
|
|
1537
1639
|
|
|
1538
1640
|
function stepMatches(
|
|
@@ -1591,6 +1693,7 @@ async function accountConversion(
|
|
|
1591
1693
|
session: SessionState,
|
|
1592
1694
|
event: TrackerEvent,
|
|
1593
1695
|
trustedGoalKey: string | undefined,
|
|
1696
|
+
reportRollupDeltas: ReportRollupDeltas,
|
|
1594
1697
|
) {
|
|
1595
1698
|
const revenueCents = event.revenueCents ?? 0;
|
|
1596
1699
|
const currency = event.currency ?? "USD";
|
|
@@ -1622,6 +1725,15 @@ async function accountConversion(
|
|
|
1622
1725
|
commissionCents,
|
|
1623
1726
|
});
|
|
1624
1727
|
if (commissionAffiliate && attribution) {
|
|
1728
|
+
foldAffiliateConversionRollup(
|
|
1729
|
+
reportRollupDeltas,
|
|
1730
|
+
siteId,
|
|
1731
|
+
event,
|
|
1732
|
+
commissionAffiliate,
|
|
1733
|
+
attribution.affiliateSlug,
|
|
1734
|
+
revenueCents,
|
|
1735
|
+
commissionCents,
|
|
1736
|
+
);
|
|
1625
1737
|
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1626
1738
|
const shard = stableHash(event.sessionId) % SECONDARY_AGGREGATE_SHARDS;
|
|
1627
1739
|
const stats = await ctx.db
|
|
@@ -1657,6 +1769,254 @@ async function accountConversion(
|
|
|
1657
1769
|
}
|
|
1658
1770
|
}
|
|
1659
1771
|
|
|
1772
|
+
function createReportRollupDeltas(): ReportRollupDeltas {
|
|
1773
|
+
return {
|
|
1774
|
+
goals: new Map(),
|
|
1775
|
+
funnels: new Map(),
|
|
1776
|
+
affiliates: new Map(),
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function reportRollupCoordinates(
|
|
1781
|
+
siteId: Id<"sites">,
|
|
1782
|
+
definitionId: string,
|
|
1783
|
+
timestamp: number,
|
|
1784
|
+
visitorId: string,
|
|
1785
|
+
) {
|
|
1786
|
+
const bucketStart = Math.floor(timestamp / DAY_MS) * DAY_MS;
|
|
1787
|
+
const shard = stableHash(visitorId) % REPORT_ROLLUP_SHARDS;
|
|
1788
|
+
return {
|
|
1789
|
+
bucketStart,
|
|
1790
|
+
shard,
|
|
1791
|
+
key: `${siteId}:${definitionId}:${bucketStart}:${shard}`,
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function foldGoalRollup(
|
|
1796
|
+
deltas: ReportRollupDeltas,
|
|
1797
|
+
siteId: Id<"sites">,
|
|
1798
|
+
event: TrackerEvent,
|
|
1799
|
+
goal: Doc<"goals">,
|
|
1800
|
+
) {
|
|
1801
|
+
const coordinates = reportRollupCoordinates(
|
|
1802
|
+
siteId,
|
|
1803
|
+
String(goal._id),
|
|
1804
|
+
event.timestamp,
|
|
1805
|
+
event.visitorId,
|
|
1806
|
+
);
|
|
1807
|
+
const current = deltas.goals.get(coordinates.key) ?? {
|
|
1808
|
+
siteId,
|
|
1809
|
+
goalId: goal._id,
|
|
1810
|
+
bucketStart: coordinates.bucketStart,
|
|
1811
|
+
shard: coordinates.shard,
|
|
1812
|
+
goalKey: goal.key,
|
|
1813
|
+
completions: 0,
|
|
1814
|
+
valueCents: 0,
|
|
1815
|
+
};
|
|
1816
|
+
current.completions += 1;
|
|
1817
|
+
current.valueCents += goal.valueCents ?? 0;
|
|
1818
|
+
deltas.goals.set(coordinates.key, current);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function foldFunnelRollup(
|
|
1822
|
+
deltas: ReportRollupDeltas,
|
|
1823
|
+
siteId: Id<"sites">,
|
|
1824
|
+
funnelId: Id<"funnels">,
|
|
1825
|
+
event: TrackerEvent,
|
|
1826
|
+
step: number,
|
|
1827
|
+
completed: boolean,
|
|
1828
|
+
) {
|
|
1829
|
+
const coordinates = reportRollupCoordinates(
|
|
1830
|
+
siteId,
|
|
1831
|
+
String(funnelId),
|
|
1832
|
+
event.timestamp,
|
|
1833
|
+
event.visitorId,
|
|
1834
|
+
);
|
|
1835
|
+
const rollup = deltas.funnels.get(coordinates.key) ?? {
|
|
1836
|
+
siteId,
|
|
1837
|
+
funnelId,
|
|
1838
|
+
bucketStart: coordinates.bucketStart,
|
|
1839
|
+
shard: coordinates.shard,
|
|
1840
|
+
steps: new Map(),
|
|
1841
|
+
};
|
|
1842
|
+
const current = rollup.steps.get(step) ?? {
|
|
1843
|
+
step,
|
|
1844
|
+
entrants: 0,
|
|
1845
|
+
completions: 0,
|
|
1846
|
+
};
|
|
1847
|
+
current.entrants += 1;
|
|
1848
|
+
current.completions += Number(completed);
|
|
1849
|
+
rollup.steps.set(step, current);
|
|
1850
|
+
deltas.funnels.set(coordinates.key, rollup);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function foldAffiliateVisitRollup(
|
|
1854
|
+
deltas: ReportRollupDeltas,
|
|
1855
|
+
siteId: Id<"sites">,
|
|
1856
|
+
event: TrackerEvent,
|
|
1857
|
+
attribution: AttributionSnapshot,
|
|
1858
|
+
) {
|
|
1859
|
+
const coordinates = reportRollupCoordinates(
|
|
1860
|
+
siteId,
|
|
1861
|
+
String(attribution.affiliateId),
|
|
1862
|
+
event.timestamp,
|
|
1863
|
+
event.visitorId,
|
|
1864
|
+
);
|
|
1865
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1866
|
+
siteId,
|
|
1867
|
+
affiliateId: attribution.affiliateId,
|
|
1868
|
+
bucketStart: coordinates.bucketStart,
|
|
1869
|
+
shard: coordinates.shard,
|
|
1870
|
+
affiliateSlug: attribution.affiliateSlug,
|
|
1871
|
+
visits: 0,
|
|
1872
|
+
visitorSketch: createVisitorSketch(),
|
|
1873
|
+
conversions: 0,
|
|
1874
|
+
revenueCents: 0,
|
|
1875
|
+
commissionCents: 0,
|
|
1876
|
+
};
|
|
1877
|
+
current.visits += 1;
|
|
1878
|
+
current.visitorSketch = addVisitor(current.visitorSketch, event.visitorId);
|
|
1879
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function foldAffiliateConversionRollup(
|
|
1883
|
+
deltas: ReportRollupDeltas,
|
|
1884
|
+
siteId: Id<"sites">,
|
|
1885
|
+
event: TrackerEvent,
|
|
1886
|
+
affiliate: Doc<"affiliates">,
|
|
1887
|
+
affiliateSlug: string,
|
|
1888
|
+
revenueCents: number,
|
|
1889
|
+
commissionCents: number,
|
|
1890
|
+
) {
|
|
1891
|
+
const coordinates = reportRollupCoordinates(
|
|
1892
|
+
siteId,
|
|
1893
|
+
String(affiliate._id),
|
|
1894
|
+
event.timestamp,
|
|
1895
|
+
event.visitorId,
|
|
1896
|
+
);
|
|
1897
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1898
|
+
siteId,
|
|
1899
|
+
affiliateId: affiliate._id,
|
|
1900
|
+
bucketStart: coordinates.bucketStart,
|
|
1901
|
+
shard: coordinates.shard,
|
|
1902
|
+
affiliateSlug,
|
|
1903
|
+
visits: 0,
|
|
1904
|
+
visitorSketch: createVisitorSketch(),
|
|
1905
|
+
conversions: 0,
|
|
1906
|
+
revenueCents: 0,
|
|
1907
|
+
commissionCents: 0,
|
|
1908
|
+
};
|
|
1909
|
+
current.conversions += 1;
|
|
1910
|
+
current.revenueCents += revenueCents;
|
|
1911
|
+
current.commissionCents += commissionCents;
|
|
1912
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
async function flushReportRollups(
|
|
1916
|
+
ctx: MutationCtx,
|
|
1917
|
+
deltas: ReportRollupDeltas,
|
|
1918
|
+
) {
|
|
1919
|
+
for (const delta of deltas.goals.values()) {
|
|
1920
|
+
const existing = await ctx.db
|
|
1921
|
+
.query("goalDailyRollups")
|
|
1922
|
+
.withIndex("by_siteId_and_goalId_and_bucketStart_and_shard", (range) =>
|
|
1923
|
+
range
|
|
1924
|
+
.eq("siteId", delta.siteId)
|
|
1925
|
+
.eq("goalId", delta.goalId)
|
|
1926
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1927
|
+
.eq("shard", delta.shard),
|
|
1928
|
+
)
|
|
1929
|
+
.unique();
|
|
1930
|
+
const fields = {
|
|
1931
|
+
siteId: delta.siteId,
|
|
1932
|
+
goalId: delta.goalId,
|
|
1933
|
+
bucketStart: delta.bucketStart,
|
|
1934
|
+
shard: delta.shard,
|
|
1935
|
+
generation: 1 as const,
|
|
1936
|
+
goalKey: delta.goalKey,
|
|
1937
|
+
completions: (existing?.completions ?? 0) + delta.completions,
|
|
1938
|
+
valueCents: (existing?.valueCents ?? 0) + delta.valueCents,
|
|
1939
|
+
};
|
|
1940
|
+
if (existing) {
|
|
1941
|
+
await ctx.db.replace("goalDailyRollups", existing._id, fields);
|
|
1942
|
+
} else {
|
|
1943
|
+
await ctx.db.insert("goalDailyRollups", fields);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
for (const delta of deltas.funnels.values()) {
|
|
1947
|
+
const existing = await ctx.db
|
|
1948
|
+
.query("funnelDailyRollups")
|
|
1949
|
+
.withIndex("by_siteId_and_funnelId_and_bucketStart_and_shard", (range) =>
|
|
1950
|
+
range
|
|
1951
|
+
.eq("siteId", delta.siteId)
|
|
1952
|
+
.eq("funnelId", delta.funnelId)
|
|
1953
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1954
|
+
.eq("shard", delta.shard),
|
|
1955
|
+
)
|
|
1956
|
+
.unique();
|
|
1957
|
+
const steps = existing?.steps.map((step) => ({ ...step })) ?? [];
|
|
1958
|
+
for (const addition of delta.steps.values()) {
|
|
1959
|
+
const current = steps.find((step) => step.step === addition.step);
|
|
1960
|
+
if (current) {
|
|
1961
|
+
current.entrants += addition.entrants;
|
|
1962
|
+
current.completions += addition.completions;
|
|
1963
|
+
} else {
|
|
1964
|
+
if (steps.length >= MAX_FUNNEL_STEPS) {
|
|
1965
|
+
fail("LIMIT_EXCEEDED", "daily funnel rollup step limit exceeded");
|
|
1966
|
+
}
|
|
1967
|
+
steps.push(addition);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
const fields = {
|
|
1971
|
+
siteId: delta.siteId,
|
|
1972
|
+
funnelId: delta.funnelId,
|
|
1973
|
+
bucketStart: delta.bucketStart,
|
|
1974
|
+
shard: delta.shard,
|
|
1975
|
+
generation: 1 as const,
|
|
1976
|
+
steps,
|
|
1977
|
+
};
|
|
1978
|
+
if (existing) {
|
|
1979
|
+
await ctx.db.replace("funnelDailyRollups", existing._id, fields);
|
|
1980
|
+
} else {
|
|
1981
|
+
await ctx.db.insert("funnelDailyRollups", fields);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
for (const delta of deltas.affiliates.values()) {
|
|
1985
|
+
const existing = await ctx.db
|
|
1986
|
+
.query("affiliateDailyRollups")
|
|
1987
|
+
.withIndex("by_siteId_and_affiliateId_and_bucketStart_and_shard", (range) =>
|
|
1988
|
+
range
|
|
1989
|
+
.eq("siteId", delta.siteId)
|
|
1990
|
+
.eq("affiliateId", delta.affiliateId)
|
|
1991
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1992
|
+
.eq("shard", delta.shard),
|
|
1993
|
+
)
|
|
1994
|
+
.unique();
|
|
1995
|
+
const fields = {
|
|
1996
|
+
siteId: delta.siteId,
|
|
1997
|
+
affiliateId: delta.affiliateId,
|
|
1998
|
+
bucketStart: delta.bucketStart,
|
|
1999
|
+
shard: delta.shard,
|
|
2000
|
+
generation: 1 as const,
|
|
2001
|
+
affiliateSlug: delta.affiliateSlug,
|
|
2002
|
+
visits: (existing?.visits ?? 0) + delta.visits,
|
|
2003
|
+
visitorSketch: unionVisitorSketches(
|
|
2004
|
+
existing?.visitorSketch ?? createVisitorSketch(),
|
|
2005
|
+
delta.visitorSketch,
|
|
2006
|
+
),
|
|
2007
|
+
conversions: (existing?.conversions ?? 0) + delta.conversions,
|
|
2008
|
+
revenueCents: (existing?.revenueCents ?? 0) + delta.revenueCents,
|
|
2009
|
+
commissionCents:
|
|
2010
|
+
(existing?.commissionCents ?? 0) + delta.commissionCents,
|
|
2011
|
+
};
|
|
2012
|
+
if (existing) {
|
|
2013
|
+
await ctx.db.replace("affiliateDailyRollups", existing._id, fields);
|
|
2014
|
+
} else {
|
|
2015
|
+
await ctx.db.insert("affiliateDailyRollups", fields);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1660
2020
|
function foldAggregates(
|
|
1661
2021
|
aggregateDeltas: AggregateDeltas,
|
|
1662
2022
|
siteId: Id<"sites">,
|
|
@@ -1694,6 +2054,7 @@ function foldAggregates(
|
|
|
1694
2054
|
outboundClicks: 0,
|
|
1695
2055
|
sessions: 0,
|
|
1696
2056
|
visitors: 0,
|
|
2057
|
+
visitorSketch: createVisitorSketch(),
|
|
1697
2058
|
conversions: 0,
|
|
1698
2059
|
revenueCents: 0,
|
|
1699
2060
|
dimensions: [],
|
|
@@ -1714,6 +2075,9 @@ function foldAggregates(
|
|
|
1714
2075
|
? Number(session.isNewVisitorHour)
|
|
1715
2076
|
: Number(session.isNewVisitorDay)
|
|
1716
2077
|
: 0;
|
|
2078
|
+
if (recordTelemetry) {
|
|
2079
|
+
delta.visitorSketch = addVisitor(delta.visitorSketch, event.visitorId);
|
|
2080
|
+
}
|
|
1717
2081
|
delta.conversions += Number(financialConversion);
|
|
1718
2082
|
delta.revenueCents += revenueCents;
|
|
1719
2083
|
delta.dimensions = mergeDimensions(delta.dimensions, dimensionDeltas);
|
|
@@ -1741,6 +2105,13 @@ async function flushAggregates(
|
|
|
1741
2105
|
bucket?.dimensions ?? [],
|
|
1742
2106
|
delta.dimensions,
|
|
1743
2107
|
);
|
|
2108
|
+
const priorSketch = bucket?.visitorSketch;
|
|
2109
|
+
const visitorSketch = priorSketch
|
|
2110
|
+
? unionVisitorSketches(priorSketch, delta.visitorSketch)
|
|
2111
|
+
: delta.visitorSketch;
|
|
2112
|
+
const visitorSketchComplete = bucket
|
|
2113
|
+
? bucket.visitorSketchComplete === true && priorSketch !== undefined
|
|
2114
|
+
: true;
|
|
1744
2115
|
if (bucket) {
|
|
1745
2116
|
await ctx.db.patch("aggregateBuckets", bucket._id, {
|
|
1746
2117
|
events: bucket.events + delta.events,
|
|
@@ -1750,6 +2121,8 @@ async function flushAggregates(
|
|
|
1750
2121
|
outboundClicks: bucket.outboundClicks + delta.outboundClicks,
|
|
1751
2122
|
sessions: bucket.sessions + delta.sessions,
|
|
1752
2123
|
visitors: bucket.visitors + delta.visitors,
|
|
2124
|
+
visitorSketch,
|
|
2125
|
+
visitorSketchComplete,
|
|
1753
2126
|
conversions: bucket.conversions + delta.conversions,
|
|
1754
2127
|
revenueCents: bucket.revenueCents + delta.revenueCents,
|
|
1755
2128
|
dimensions,
|
|
@@ -1767,6 +2140,8 @@ async function flushAggregates(
|
|
|
1767
2140
|
outboundClicks: delta.outboundClicks,
|
|
1768
2141
|
sessions: delta.sessions,
|
|
1769
2142
|
visitors: delta.visitors,
|
|
2143
|
+
visitorSketch,
|
|
2144
|
+
visitorSketchComplete,
|
|
1770
2145
|
conversions: delta.conversions,
|
|
1771
2146
|
revenueCents: delta.revenueCents,
|
|
1772
2147
|
dimensions,
|
package/src/component/live.ts
CHANGED
|
@@ -23,16 +23,21 @@ export const expire = internalMutation({
|
|
|
23
23
|
if (liveSession.expiryChainDeadline !== args.scheduledDeadline) {
|
|
24
24
|
return { expired: false, rescheduled: false, stale: true };
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
if (liveSession.expiresAt > now) {
|
|
27
28
|
const state = await ctx.db
|
|
28
29
|
.query("liveSweepStates")
|
|
29
30
|
.withIndex("by_siteId", (range) => range.eq("siteId", liveSession.siteId))
|
|
30
31
|
.unique();
|
|
31
32
|
const scheduledAt = Math.min(
|
|
32
33
|
liveSession.expiresAt,
|
|
33
|
-
|
|
34
|
+
now + LIVE_SWEEP_INTERVAL_MS,
|
|
34
35
|
);
|
|
35
|
-
if (
|
|
36
|
+
if (
|
|
37
|
+
!state ||
|
|
38
|
+
state.scheduledAt <= now ||
|
|
39
|
+
state.scheduledAt > scheduledAt
|
|
40
|
+
) {
|
|
36
41
|
if (state) {
|
|
37
42
|
await ctx.db.patch("liveSweepStates", state._id, {
|
|
38
43
|
scheduledAt,
|