@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/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,
|
|
@@ -60,6 +69,7 @@ import {
|
|
|
60
69
|
|
|
61
70
|
type DimensionType =
|
|
62
71
|
| "source"
|
|
72
|
+
| "campaign"
|
|
63
73
|
| "page"
|
|
64
74
|
| "country"
|
|
65
75
|
| "device"
|
|
@@ -81,11 +91,49 @@ type AggregateDelta = {
|
|
|
81
91
|
outboundClicks: number;
|
|
82
92
|
sessions: number;
|
|
83
93
|
visitors: number;
|
|
94
|
+
visitorSketch: VisitorSketch;
|
|
84
95
|
conversions: number;
|
|
85
96
|
revenueCents: number;
|
|
86
97
|
dimensions: Doc<"aggregateBuckets">["dimensions"];
|
|
87
98
|
};
|
|
88
99
|
type AggregateDeltas = Map<string, AggregateDelta>;
|
|
100
|
+
type GoalRollupDelta = {
|
|
101
|
+
siteId: Id<"sites">;
|
|
102
|
+
goalId: Id<"goals">;
|
|
103
|
+
bucketStart: number;
|
|
104
|
+
shard: number;
|
|
105
|
+
goalKey: string;
|
|
106
|
+
completions: number;
|
|
107
|
+
valueCents: number;
|
|
108
|
+
};
|
|
109
|
+
type FunnelRollupDelta = {
|
|
110
|
+
siteId: Id<"sites">;
|
|
111
|
+
funnelId: Id<"funnels">;
|
|
112
|
+
bucketStart: number;
|
|
113
|
+
shard: number;
|
|
114
|
+
steps: Map<number, {
|
|
115
|
+
step: number;
|
|
116
|
+
entrants: number;
|
|
117
|
+
completions: number;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
type AffiliateRollupDelta = {
|
|
121
|
+
siteId: Id<"sites">;
|
|
122
|
+
affiliateId: Id<"affiliates">;
|
|
123
|
+
bucketStart: number;
|
|
124
|
+
shard: number;
|
|
125
|
+
affiliateSlug: string;
|
|
126
|
+
visits: number;
|
|
127
|
+
visitorSketch: VisitorSketch;
|
|
128
|
+
conversions: number;
|
|
129
|
+
revenueCents: number;
|
|
130
|
+
commissionCents: number;
|
|
131
|
+
};
|
|
132
|
+
type ReportRollupDeltas = {
|
|
133
|
+
goals: Map<string, GoalRollupDelta>;
|
|
134
|
+
funnels: Map<string, FunnelRollupDelta>;
|
|
135
|
+
affiliates: Map<string, AffiliateRollupDelta>;
|
|
136
|
+
};
|
|
89
137
|
type AttributionSnapshot = {
|
|
90
138
|
affiliateId: Id<"affiliates">;
|
|
91
139
|
affiliateSlug: string;
|
|
@@ -99,6 +147,8 @@ type SessionState = {
|
|
|
99
147
|
isNewVisitorDay: boolean;
|
|
100
148
|
startedAt: number;
|
|
101
149
|
source: string;
|
|
150
|
+
visitorKey: string;
|
|
151
|
+
utmCampaign?: string;
|
|
102
152
|
country?: string;
|
|
103
153
|
city?: string;
|
|
104
154
|
latitude?: number;
|
|
@@ -161,21 +211,25 @@ export const ingestBatch = mutation({
|
|
|
161
211
|
limit: MAX_BATCH_EVENTS,
|
|
162
212
|
});
|
|
163
213
|
}
|
|
214
|
+
let eventByteCounts: number[];
|
|
164
215
|
try {
|
|
165
|
-
|
|
216
|
+
({ eventByteCounts } = measureBatchBytes(String(args.siteId), args.events));
|
|
166
217
|
} catch {
|
|
167
218
|
fail("BATCH_TOO_LARGE", "batch exceeds 64 KiB", { limitBytes: 65_536 });
|
|
168
219
|
}
|
|
169
220
|
|
|
170
221
|
const now = Date.now();
|
|
171
|
-
|
|
222
|
+
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
223
|
+
await consumeSiteRateLimit(
|
|
172
224
|
ctx,
|
|
173
225
|
args.siteId,
|
|
174
226
|
args.events,
|
|
227
|
+
eventByteCounts,
|
|
175
228
|
now,
|
|
176
229
|
);
|
|
177
230
|
const context = sanitizeContext(args.context);
|
|
178
231
|
const aggregateDeltas: AggregateDeltas = new Map();
|
|
232
|
+
const reportRollupDeltas = createReportRollupDeltas();
|
|
179
233
|
const sessionRateWindows = new Map<string, SessionRateWindowState>();
|
|
180
234
|
const batchState: BatchState = {
|
|
181
235
|
sessions: new Map(),
|
|
@@ -367,6 +421,7 @@ export const ingestBatch = mutation({
|
|
|
367
421
|
goals: canMatchDefinition ? goals ?? [] : [],
|
|
368
422
|
funnels: canMatchDefinition ? funnels ?? [] : [],
|
|
369
423
|
aggregateDeltas,
|
|
424
|
+
reportRollupDeltas,
|
|
370
425
|
recordTelemetry: true,
|
|
371
426
|
financialConversion: false,
|
|
372
427
|
trustedGoalKey: undefined,
|
|
@@ -384,6 +439,7 @@ export const ingestBatch = mutation({
|
|
|
384
439
|
await flushBatchState(ctx, batchState);
|
|
385
440
|
await flushSessionRateLimits(ctx, sessionRateWindows, now);
|
|
386
441
|
await flushAggregates(ctx, aggregateDeltas);
|
|
442
|
+
await flushReportRollups(ctx, reportRollupDeltas);
|
|
387
443
|
return { accepted, duplicates, rejected };
|
|
388
444
|
},
|
|
389
445
|
});
|
|
@@ -475,6 +531,7 @@ export const trackConversion = mutation({
|
|
|
475
531
|
};
|
|
476
532
|
|
|
477
533
|
const aggregateDeltas: AggregateDeltas = new Map();
|
|
534
|
+
const reportRollupDeltas = createReportRollupDeltas();
|
|
478
535
|
const session = await accountEvent(ctx, {
|
|
479
536
|
siteId: args.siteId,
|
|
480
537
|
event,
|
|
@@ -483,6 +540,7 @@ export const trackConversion = mutation({
|
|
|
483
540
|
goals: [],
|
|
484
541
|
funnels: [],
|
|
485
542
|
aggregateDeltas,
|
|
543
|
+
reportRollupDeltas,
|
|
486
544
|
recordTelemetry: existingTelemetry === null && !telemetry.saturated,
|
|
487
545
|
financialConversion: true,
|
|
488
546
|
trustedGoalKey,
|
|
@@ -497,6 +555,8 @@ export const trackConversion = mutation({
|
|
|
497
555
|
);
|
|
498
556
|
}
|
|
499
557
|
await flushAggregates(ctx, aggregateDeltas);
|
|
558
|
+
await flushReportRollups(ctx, reportRollupDeltas);
|
|
559
|
+
await ensureAnalyticsControl(ctx, args.siteId, Date.now());
|
|
500
560
|
return { accepted: 1, duplicates: 0, rejected: 0 };
|
|
501
561
|
},
|
|
502
562
|
});
|
|
@@ -619,6 +679,7 @@ async function accountEvent(
|
|
|
619
679
|
goals: Doc<"goals">[];
|
|
620
680
|
funnels: Doc<"funnels">[];
|
|
621
681
|
aggregateDeltas: AggregateDeltas;
|
|
682
|
+
reportRollupDeltas: ReportRollupDeltas;
|
|
622
683
|
recordTelemetry: boolean;
|
|
623
684
|
financialConversion: boolean;
|
|
624
685
|
trustedGoalKey: string | undefined;
|
|
@@ -635,6 +696,7 @@ async function accountEvent(
|
|
|
635
696
|
input.context,
|
|
636
697
|
input.existingSession,
|
|
637
698
|
input.financialConversion,
|
|
699
|
+
input.reportRollupDeltas,
|
|
638
700
|
input.batchState,
|
|
639
701
|
)
|
|
640
702
|
: await accountTrustedSession(ctx, input.existingSession, event);
|
|
@@ -691,12 +753,25 @@ async function accountEvent(
|
|
|
691
753
|
timestamp: event.timestamp,
|
|
692
754
|
valueCents: goal.valueCents ?? 0,
|
|
693
755
|
});
|
|
756
|
+
foldGoalRollup(
|
|
757
|
+
input.reportRollupDeltas,
|
|
758
|
+
input.siteId,
|
|
759
|
+
event,
|
|
760
|
+
goal,
|
|
761
|
+
);
|
|
694
762
|
matchedGoalKeys.push(goal.key);
|
|
695
763
|
}
|
|
696
764
|
|
|
697
765
|
for (const funnel of input.funnels) {
|
|
698
766
|
if (funnel.active) {
|
|
699
|
-
await advanceFunnel(
|
|
767
|
+
await advanceFunnel(
|
|
768
|
+
ctx,
|
|
769
|
+
input.siteId,
|
|
770
|
+
funnel,
|
|
771
|
+
event,
|
|
772
|
+
input.reportRollupDeltas,
|
|
773
|
+
input.batchState,
|
|
774
|
+
);
|
|
700
775
|
}
|
|
701
776
|
}
|
|
702
777
|
|
|
@@ -707,11 +782,16 @@ async function accountEvent(
|
|
|
707
782
|
session,
|
|
708
783
|
event,
|
|
709
784
|
input.trustedGoalKey,
|
|
785
|
+
input.reportRollupDeltas,
|
|
710
786
|
);
|
|
711
787
|
}
|
|
712
788
|
|
|
789
|
+
const campaignDimensions: Dimension[] = session.utmCampaign
|
|
790
|
+
? [{ type: "campaign", value: session.utmCampaign }]
|
|
791
|
+
: [];
|
|
713
792
|
const dimensions: Dimension[] = [
|
|
714
793
|
{ type: "source", value: session.source },
|
|
794
|
+
...campaignDimensions,
|
|
715
795
|
{ type: "page", value: event.path },
|
|
716
796
|
{ type: "country", value: session.country ?? "unknown" },
|
|
717
797
|
{ type: "device", value: input.context.device ?? "unknown" },
|
|
@@ -769,6 +849,8 @@ async function accountTrustedSession(
|
|
|
769
849
|
isNewVisitorDay: false,
|
|
770
850
|
startedAt: existing.startedAt,
|
|
771
851
|
source: existing.source,
|
|
852
|
+
utmCampaign: existing.utmCampaign,
|
|
853
|
+
visitorKey: existing.visitorKey ?? existing.visitorId,
|
|
772
854
|
country: existing.country,
|
|
773
855
|
city: existing.city,
|
|
774
856
|
latitude: existing.latitude,
|
|
@@ -789,10 +871,19 @@ async function upsertSession(
|
|
|
789
871
|
context: IngestContext,
|
|
790
872
|
existing: CachedDocument<"sessions"> | null,
|
|
791
873
|
financialConversion: boolean,
|
|
874
|
+
reportRollupDeltas: ReportRollupDeltas,
|
|
792
875
|
batchState?: BatchState,
|
|
793
876
|
): Promise<SessionState> {
|
|
794
|
-
|
|
877
|
+
// A host-supplied id is the identity; anonymous sessions count through the
|
|
878
|
+
// daily key derived by the HTTP action, or once per session without one.
|
|
879
|
+
const visitorKey =
|
|
880
|
+
existing?.visitorKey ??
|
|
881
|
+
(event.visitorId !== event.sessionId
|
|
882
|
+
? event.visitorId
|
|
883
|
+
: (context.visitorKey ?? event.sessionId));
|
|
795
884
|
const utmSource = stringProperty(event.properties, "utm_source");
|
|
885
|
+
// Campaign traffic names its own source; the referrer host is the fallback.
|
|
886
|
+
const source = utmSource?.toLowerCase() ?? sourceFromReferrer(event.referrer);
|
|
796
887
|
const utmMedium = stringProperty(event.properties, "utm_medium");
|
|
797
888
|
const utmCampaign = stringProperty(event.properties, "utm_campaign");
|
|
798
889
|
const attribution = await attributionForEvent(
|
|
@@ -803,6 +894,15 @@ async function upsertSession(
|
|
|
803
894
|
batchState,
|
|
804
895
|
);
|
|
805
896
|
if (existing) {
|
|
897
|
+
if (attribution && existing.affiliateId !== attribution.affiliateId) {
|
|
898
|
+
foldAffiliateVisitRollup(
|
|
899
|
+
reportRollupDeltas,
|
|
900
|
+
siteId,
|
|
901
|
+
event,
|
|
902
|
+
attribution,
|
|
903
|
+
visitorKey,
|
|
904
|
+
);
|
|
905
|
+
}
|
|
806
906
|
const startedAt = Math.min(existing.startedAt, event.timestamp);
|
|
807
907
|
const lastSeenAt = Math.max(existing.lastSeenAt, event.timestamp);
|
|
808
908
|
const updates = {
|
|
@@ -850,6 +950,8 @@ async function upsertSession(
|
|
|
850
950
|
Math.floor(event.timestamp / DAY_MS),
|
|
851
951
|
startedAt,
|
|
852
952
|
source: existing.source,
|
|
953
|
+
utmCampaign: existing.utmCampaign,
|
|
954
|
+
visitorKey,
|
|
853
955
|
country: context.country ?? existing.country,
|
|
854
956
|
city: context.city ?? existing.city,
|
|
855
957
|
latitude: context.latitude ?? existing.latitude,
|
|
@@ -885,6 +987,7 @@ async function upsertSession(
|
|
|
885
987
|
siteId,
|
|
886
988
|
sessionId: event.sessionId,
|
|
887
989
|
visitorId: event.visitorId,
|
|
990
|
+
visitorKey,
|
|
888
991
|
startedAt: event.timestamp,
|
|
889
992
|
lastSeenAt: event.timestamp,
|
|
890
993
|
entryPath: event.path,
|
|
@@ -912,6 +1015,15 @@ async function upsertSession(
|
|
|
912
1015
|
...(attribution ?? {}),
|
|
913
1016
|
};
|
|
914
1017
|
const sessionId = await ctx.db.insert("sessions", fields);
|
|
1018
|
+
if (attribution) {
|
|
1019
|
+
foldAffiliateVisitRollup(
|
|
1020
|
+
reportRollupDeltas,
|
|
1021
|
+
siteId,
|
|
1022
|
+
event,
|
|
1023
|
+
attribution,
|
|
1024
|
+
visitorKey,
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
915
1027
|
if (batchState) {
|
|
916
1028
|
const state = batchState.sessions.get(event.sessionId);
|
|
917
1029
|
if (!state) throw new Error("session cache was not initialized");
|
|
@@ -932,6 +1044,8 @@ async function upsertSession(
|
|
|
932
1044
|
Math.floor(event.timestamp / DAY_MS),
|
|
933
1045
|
startedAt: event.timestamp,
|
|
934
1046
|
source,
|
|
1047
|
+
utmCampaign,
|
|
1048
|
+
visitorKey,
|
|
935
1049
|
country: context.country,
|
|
936
1050
|
city: context.city,
|
|
937
1051
|
latitude: context.latitude,
|
|
@@ -1128,6 +1242,7 @@ async function accountHeartbeat(
|
|
|
1128
1242
|
isNewVisitorDay: false,
|
|
1129
1243
|
startedAt,
|
|
1130
1244
|
source: session.source,
|
|
1245
|
+
visitorKey: session.visitorKey ?? session.visitorId,
|
|
1131
1246
|
country: context.country ?? session.country,
|
|
1132
1247
|
city: context.city ?? session.city,
|
|
1133
1248
|
latitude: context.latitude ?? session.latitude,
|
|
@@ -1145,17 +1260,15 @@ async function consumeSiteRateLimit(
|
|
|
1145
1260
|
ctx: MutationCtx,
|
|
1146
1261
|
siteId: Id<"sites">,
|
|
1147
1262
|
events: TrackerEvent[],
|
|
1263
|
+
eventByteCounts: number[],
|
|
1148
1264
|
now: number,
|
|
1149
|
-
): Promise<
|
|
1265
|
+
): Promise<void> {
|
|
1150
1266
|
const windowStart =
|
|
1151
1267
|
Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
1152
1268
|
const increments = new Map<number, { eventCount: number; byteCount: number }>();
|
|
1153
|
-
const eventByteCounts: number[] = [];
|
|
1154
|
-
const encoder = new TextEncoder();
|
|
1155
1269
|
for (let index = 0; index < events.length; index += 1) {
|
|
1156
1270
|
const event = events[index];
|
|
1157
|
-
const byteCount =
|
|
1158
|
-
eventByteCounts.push(byteCount);
|
|
1271
|
+
const byteCount = eventByteCounts[index];
|
|
1159
1272
|
const shard = stableHash(event.sessionId) % SITE_INGEST_SHARDS;
|
|
1160
1273
|
const current = increments.get(shard) ?? { eventCount: 0, byteCount: 0 };
|
|
1161
1274
|
current.eventCount += 1;
|
|
@@ -1225,7 +1338,6 @@ async function consumeSiteRateLimit(
|
|
|
1225
1338
|
});
|
|
1226
1339
|
}
|
|
1227
1340
|
}
|
|
1228
|
-
return eventByteCounts;
|
|
1229
1341
|
}
|
|
1230
1342
|
|
|
1231
1343
|
async function consumeRateLimit(
|
|
@@ -1410,9 +1522,13 @@ async function ensureLiveSweep(
|
|
|
1410
1522
|
.query("liveSweepStates")
|
|
1411
1523
|
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
1412
1524
|
.unique();
|
|
1413
|
-
if (!existing) {
|
|
1525
|
+
if (!existing || existing.scheduledAt <= now) {
|
|
1414
1526
|
const scheduledAt = now + LIVE_SWEEP_INTERVAL_MS;
|
|
1415
|
-
|
|
1527
|
+
if (existing) {
|
|
1528
|
+
await ctx.db.patch("liveSweepStates", existing._id, { scheduledAt });
|
|
1529
|
+
} else {
|
|
1530
|
+
await ctx.db.insert("liveSweepStates", { siteId, scheduledAt });
|
|
1531
|
+
}
|
|
1416
1532
|
await ctx.scheduler.runAt(scheduledAt, internal.live.sweep, {
|
|
1417
1533
|
siteId,
|
|
1418
1534
|
scheduledAt,
|
|
@@ -1433,6 +1549,7 @@ async function advanceFunnel(
|
|
|
1433
1549
|
siteId: Id<"sites">,
|
|
1434
1550
|
funnel: Doc<"funnels">,
|
|
1435
1551
|
event: TrackerEvent,
|
|
1552
|
+
reportRollupDeltas: ReportRollupDeltas,
|
|
1436
1553
|
batchState?: BatchState,
|
|
1437
1554
|
) {
|
|
1438
1555
|
const cacheKey = JSON.stringify([funnel._id, event.visitorId]);
|
|
@@ -1481,6 +1598,7 @@ async function advanceFunnel(
|
|
|
1481
1598
|
};
|
|
1482
1599
|
}
|
|
1483
1600
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, 0, false);
|
|
1601
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1484
1602
|
return;
|
|
1485
1603
|
}
|
|
1486
1604
|
if (progress.completedAt !== undefined || event.timestamp < progress.lastStepAt) return;
|
|
@@ -1507,6 +1625,7 @@ async function advanceFunnel(
|
|
|
1507
1625
|
});
|
|
1508
1626
|
}
|
|
1509
1627
|
await incrementFunnelStep(ctx, siteId, funnel._id, event, 0, false);
|
|
1628
|
+
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1510
1629
|
return;
|
|
1511
1630
|
}
|
|
1512
1631
|
const nextStep = funnel.steps[progress.currentStep];
|
|
@@ -1533,6 +1652,14 @@ async function advanceFunnel(
|
|
|
1533
1652
|
progress.currentStep,
|
|
1534
1653
|
completed,
|
|
1535
1654
|
);
|
|
1655
|
+
foldFunnelRollup(
|
|
1656
|
+
reportRollupDeltas,
|
|
1657
|
+
siteId,
|
|
1658
|
+
funnel._id,
|
|
1659
|
+
event,
|
|
1660
|
+
progress.currentStep,
|
|
1661
|
+
completed,
|
|
1662
|
+
);
|
|
1536
1663
|
}
|
|
1537
1664
|
|
|
1538
1665
|
function stepMatches(
|
|
@@ -1591,6 +1718,7 @@ async function accountConversion(
|
|
|
1591
1718
|
session: SessionState,
|
|
1592
1719
|
event: TrackerEvent,
|
|
1593
1720
|
trustedGoalKey: string | undefined,
|
|
1721
|
+
reportRollupDeltas: ReportRollupDeltas,
|
|
1594
1722
|
) {
|
|
1595
1723
|
const revenueCents = event.revenueCents ?? 0;
|
|
1596
1724
|
const currency = event.currency ?? "USD";
|
|
@@ -1622,6 +1750,15 @@ async function accountConversion(
|
|
|
1622
1750
|
commissionCents,
|
|
1623
1751
|
});
|
|
1624
1752
|
if (commissionAffiliate && attribution) {
|
|
1753
|
+
foldAffiliateConversionRollup(
|
|
1754
|
+
reportRollupDeltas,
|
|
1755
|
+
siteId,
|
|
1756
|
+
event,
|
|
1757
|
+
commissionAffiliate,
|
|
1758
|
+
attribution.affiliateSlug,
|
|
1759
|
+
revenueCents,
|
|
1760
|
+
commissionCents,
|
|
1761
|
+
);
|
|
1625
1762
|
const bucketStart = Math.floor(event.timestamp / DAY_MS) * DAY_MS;
|
|
1626
1763
|
const shard = stableHash(event.sessionId) % SECONDARY_AGGREGATE_SHARDS;
|
|
1627
1764
|
const stats = await ctx.db
|
|
@@ -1657,6 +1794,255 @@ async function accountConversion(
|
|
|
1657
1794
|
}
|
|
1658
1795
|
}
|
|
1659
1796
|
|
|
1797
|
+
function createReportRollupDeltas(): ReportRollupDeltas {
|
|
1798
|
+
return {
|
|
1799
|
+
goals: new Map(),
|
|
1800
|
+
funnels: new Map(),
|
|
1801
|
+
affiliates: new Map(),
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function reportRollupCoordinates(
|
|
1806
|
+
siteId: Id<"sites">,
|
|
1807
|
+
definitionId: string,
|
|
1808
|
+
timestamp: number,
|
|
1809
|
+
visitorId: string,
|
|
1810
|
+
) {
|
|
1811
|
+
const bucketStart = Math.floor(timestamp / DAY_MS) * DAY_MS;
|
|
1812
|
+
const shard = stableHash(visitorId) % REPORT_ROLLUP_SHARDS;
|
|
1813
|
+
return {
|
|
1814
|
+
bucketStart,
|
|
1815
|
+
shard,
|
|
1816
|
+
key: `${siteId}:${definitionId}:${bucketStart}:${shard}`,
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function foldGoalRollup(
|
|
1821
|
+
deltas: ReportRollupDeltas,
|
|
1822
|
+
siteId: Id<"sites">,
|
|
1823
|
+
event: TrackerEvent,
|
|
1824
|
+
goal: Doc<"goals">,
|
|
1825
|
+
) {
|
|
1826
|
+
const coordinates = reportRollupCoordinates(
|
|
1827
|
+
siteId,
|
|
1828
|
+
String(goal._id),
|
|
1829
|
+
event.timestamp,
|
|
1830
|
+
event.visitorId,
|
|
1831
|
+
);
|
|
1832
|
+
const current = deltas.goals.get(coordinates.key) ?? {
|
|
1833
|
+
siteId,
|
|
1834
|
+
goalId: goal._id,
|
|
1835
|
+
bucketStart: coordinates.bucketStart,
|
|
1836
|
+
shard: coordinates.shard,
|
|
1837
|
+
goalKey: goal.key,
|
|
1838
|
+
completions: 0,
|
|
1839
|
+
valueCents: 0,
|
|
1840
|
+
};
|
|
1841
|
+
current.completions += 1;
|
|
1842
|
+
current.valueCents += goal.valueCents ?? 0;
|
|
1843
|
+
deltas.goals.set(coordinates.key, current);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function foldFunnelRollup(
|
|
1847
|
+
deltas: ReportRollupDeltas,
|
|
1848
|
+
siteId: Id<"sites">,
|
|
1849
|
+
funnelId: Id<"funnels">,
|
|
1850
|
+
event: TrackerEvent,
|
|
1851
|
+
step: number,
|
|
1852
|
+
completed: boolean,
|
|
1853
|
+
) {
|
|
1854
|
+
const coordinates = reportRollupCoordinates(
|
|
1855
|
+
siteId,
|
|
1856
|
+
String(funnelId),
|
|
1857
|
+
event.timestamp,
|
|
1858
|
+
event.visitorId,
|
|
1859
|
+
);
|
|
1860
|
+
const rollup = deltas.funnels.get(coordinates.key) ?? {
|
|
1861
|
+
siteId,
|
|
1862
|
+
funnelId,
|
|
1863
|
+
bucketStart: coordinates.bucketStart,
|
|
1864
|
+
shard: coordinates.shard,
|
|
1865
|
+
steps: new Map(),
|
|
1866
|
+
};
|
|
1867
|
+
const current = rollup.steps.get(step) ?? {
|
|
1868
|
+
step,
|
|
1869
|
+
entrants: 0,
|
|
1870
|
+
completions: 0,
|
|
1871
|
+
};
|
|
1872
|
+
current.entrants += 1;
|
|
1873
|
+
current.completions += Number(completed);
|
|
1874
|
+
rollup.steps.set(step, current);
|
|
1875
|
+
deltas.funnels.set(coordinates.key, rollup);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function foldAffiliateVisitRollup(
|
|
1879
|
+
deltas: ReportRollupDeltas,
|
|
1880
|
+
siteId: Id<"sites">,
|
|
1881
|
+
event: TrackerEvent,
|
|
1882
|
+
attribution: AttributionSnapshot,
|
|
1883
|
+
visitorKey: string,
|
|
1884
|
+
) {
|
|
1885
|
+
const coordinates = reportRollupCoordinates(
|
|
1886
|
+
siteId,
|
|
1887
|
+
String(attribution.affiliateId),
|
|
1888
|
+
event.timestamp,
|
|
1889
|
+
event.visitorId,
|
|
1890
|
+
);
|
|
1891
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1892
|
+
siteId,
|
|
1893
|
+
affiliateId: attribution.affiliateId,
|
|
1894
|
+
bucketStart: coordinates.bucketStart,
|
|
1895
|
+
shard: coordinates.shard,
|
|
1896
|
+
affiliateSlug: attribution.affiliateSlug,
|
|
1897
|
+
visits: 0,
|
|
1898
|
+
visitorSketch: createVisitorSketch(),
|
|
1899
|
+
conversions: 0,
|
|
1900
|
+
revenueCents: 0,
|
|
1901
|
+
commissionCents: 0,
|
|
1902
|
+
};
|
|
1903
|
+
current.visits += 1;
|
|
1904
|
+
current.visitorSketch = addVisitor(current.visitorSketch, visitorKey);
|
|
1905
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function foldAffiliateConversionRollup(
|
|
1909
|
+
deltas: ReportRollupDeltas,
|
|
1910
|
+
siteId: Id<"sites">,
|
|
1911
|
+
event: TrackerEvent,
|
|
1912
|
+
affiliate: Doc<"affiliates">,
|
|
1913
|
+
affiliateSlug: string,
|
|
1914
|
+
revenueCents: number,
|
|
1915
|
+
commissionCents: number,
|
|
1916
|
+
) {
|
|
1917
|
+
const coordinates = reportRollupCoordinates(
|
|
1918
|
+
siteId,
|
|
1919
|
+
String(affiliate._id),
|
|
1920
|
+
event.timestamp,
|
|
1921
|
+
event.visitorId,
|
|
1922
|
+
);
|
|
1923
|
+
const current = deltas.affiliates.get(coordinates.key) ?? {
|
|
1924
|
+
siteId,
|
|
1925
|
+
affiliateId: affiliate._id,
|
|
1926
|
+
bucketStart: coordinates.bucketStart,
|
|
1927
|
+
shard: coordinates.shard,
|
|
1928
|
+
affiliateSlug,
|
|
1929
|
+
visits: 0,
|
|
1930
|
+
visitorSketch: createVisitorSketch(),
|
|
1931
|
+
conversions: 0,
|
|
1932
|
+
revenueCents: 0,
|
|
1933
|
+
commissionCents: 0,
|
|
1934
|
+
};
|
|
1935
|
+
current.conversions += 1;
|
|
1936
|
+
current.revenueCents += revenueCents;
|
|
1937
|
+
current.commissionCents += commissionCents;
|
|
1938
|
+
deltas.affiliates.set(coordinates.key, current);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
async function flushReportRollups(
|
|
1942
|
+
ctx: MutationCtx,
|
|
1943
|
+
deltas: ReportRollupDeltas,
|
|
1944
|
+
) {
|
|
1945
|
+
for (const delta of deltas.goals.values()) {
|
|
1946
|
+
const existing = await ctx.db
|
|
1947
|
+
.query("goalDailyRollups")
|
|
1948
|
+
.withIndex("by_siteId_and_goalId_and_bucketStart_and_shard", (range) =>
|
|
1949
|
+
range
|
|
1950
|
+
.eq("siteId", delta.siteId)
|
|
1951
|
+
.eq("goalId", delta.goalId)
|
|
1952
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1953
|
+
.eq("shard", delta.shard),
|
|
1954
|
+
)
|
|
1955
|
+
.unique();
|
|
1956
|
+
const fields = {
|
|
1957
|
+
siteId: delta.siteId,
|
|
1958
|
+
goalId: delta.goalId,
|
|
1959
|
+
bucketStart: delta.bucketStart,
|
|
1960
|
+
shard: delta.shard,
|
|
1961
|
+
generation: 1 as const,
|
|
1962
|
+
goalKey: delta.goalKey,
|
|
1963
|
+
completions: (existing?.completions ?? 0) + delta.completions,
|
|
1964
|
+
valueCents: (existing?.valueCents ?? 0) + delta.valueCents,
|
|
1965
|
+
};
|
|
1966
|
+
if (existing) {
|
|
1967
|
+
await ctx.db.replace("goalDailyRollups", existing._id, fields);
|
|
1968
|
+
} else {
|
|
1969
|
+
await ctx.db.insert("goalDailyRollups", fields);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
for (const delta of deltas.funnels.values()) {
|
|
1973
|
+
const existing = await ctx.db
|
|
1974
|
+
.query("funnelDailyRollups")
|
|
1975
|
+
.withIndex("by_siteId_and_funnelId_and_bucketStart_and_shard", (range) =>
|
|
1976
|
+
range
|
|
1977
|
+
.eq("siteId", delta.siteId)
|
|
1978
|
+
.eq("funnelId", delta.funnelId)
|
|
1979
|
+
.eq("bucketStart", delta.bucketStart)
|
|
1980
|
+
.eq("shard", delta.shard),
|
|
1981
|
+
)
|
|
1982
|
+
.unique();
|
|
1983
|
+
const steps = existing?.steps.map((step) => ({ ...step })) ?? [];
|
|
1984
|
+
for (const addition of delta.steps.values()) {
|
|
1985
|
+
const current = steps.find((step) => step.step === addition.step);
|
|
1986
|
+
if (current) {
|
|
1987
|
+
current.entrants += addition.entrants;
|
|
1988
|
+
current.completions += addition.completions;
|
|
1989
|
+
} else {
|
|
1990
|
+
if (steps.length >= MAX_FUNNEL_STEPS) {
|
|
1991
|
+
fail("LIMIT_EXCEEDED", "daily funnel rollup step limit exceeded");
|
|
1992
|
+
}
|
|
1993
|
+
steps.push(addition);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
const fields = {
|
|
1997
|
+
siteId: delta.siteId,
|
|
1998
|
+
funnelId: delta.funnelId,
|
|
1999
|
+
bucketStart: delta.bucketStart,
|
|
2000
|
+
shard: delta.shard,
|
|
2001
|
+
generation: 1 as const,
|
|
2002
|
+
steps,
|
|
2003
|
+
};
|
|
2004
|
+
if (existing) {
|
|
2005
|
+
await ctx.db.replace("funnelDailyRollups", existing._id, fields);
|
|
2006
|
+
} else {
|
|
2007
|
+
await ctx.db.insert("funnelDailyRollups", fields);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
for (const delta of deltas.affiliates.values()) {
|
|
2011
|
+
const existing = await ctx.db
|
|
2012
|
+
.query("affiliateDailyRollups")
|
|
2013
|
+
.withIndex("by_siteId_and_affiliateId_and_bucketStart_and_shard", (range) =>
|
|
2014
|
+
range
|
|
2015
|
+
.eq("siteId", delta.siteId)
|
|
2016
|
+
.eq("affiliateId", delta.affiliateId)
|
|
2017
|
+
.eq("bucketStart", delta.bucketStart)
|
|
2018
|
+
.eq("shard", delta.shard),
|
|
2019
|
+
)
|
|
2020
|
+
.unique();
|
|
2021
|
+
const fields = {
|
|
2022
|
+
siteId: delta.siteId,
|
|
2023
|
+
affiliateId: delta.affiliateId,
|
|
2024
|
+
bucketStart: delta.bucketStart,
|
|
2025
|
+
shard: delta.shard,
|
|
2026
|
+
generation: 1 as const,
|
|
2027
|
+
affiliateSlug: delta.affiliateSlug,
|
|
2028
|
+
visits: (existing?.visits ?? 0) + delta.visits,
|
|
2029
|
+
visitorSketch: unionVisitorSketches(
|
|
2030
|
+
existing?.visitorSketch ?? createVisitorSketch(),
|
|
2031
|
+
delta.visitorSketch,
|
|
2032
|
+
),
|
|
2033
|
+
conversions: (existing?.conversions ?? 0) + delta.conversions,
|
|
2034
|
+
revenueCents: (existing?.revenueCents ?? 0) + delta.revenueCents,
|
|
2035
|
+
commissionCents:
|
|
2036
|
+
(existing?.commissionCents ?? 0) + delta.commissionCents,
|
|
2037
|
+
};
|
|
2038
|
+
if (existing) {
|
|
2039
|
+
await ctx.db.replace("affiliateDailyRollups", existing._id, fields);
|
|
2040
|
+
} else {
|
|
2041
|
+
await ctx.db.insert("affiliateDailyRollups", fields);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1660
2046
|
function foldAggregates(
|
|
1661
2047
|
aggregateDeltas: AggregateDeltas,
|
|
1662
2048
|
siteId: Id<"sites">,
|
|
@@ -1694,6 +2080,7 @@ function foldAggregates(
|
|
|
1694
2080
|
outboundClicks: 0,
|
|
1695
2081
|
sessions: 0,
|
|
1696
2082
|
visitors: 0,
|
|
2083
|
+
visitorSketch: createVisitorSketch(),
|
|
1697
2084
|
conversions: 0,
|
|
1698
2085
|
revenueCents: 0,
|
|
1699
2086
|
dimensions: [],
|
|
@@ -1714,6 +2101,9 @@ function foldAggregates(
|
|
|
1714
2101
|
? Number(session.isNewVisitorHour)
|
|
1715
2102
|
: Number(session.isNewVisitorDay)
|
|
1716
2103
|
: 0;
|
|
2104
|
+
if (recordTelemetry) {
|
|
2105
|
+
delta.visitorSketch = addVisitor(delta.visitorSketch, session.visitorKey);
|
|
2106
|
+
}
|
|
1717
2107
|
delta.conversions += Number(financialConversion);
|
|
1718
2108
|
delta.revenueCents += revenueCents;
|
|
1719
2109
|
delta.dimensions = mergeDimensions(delta.dimensions, dimensionDeltas);
|
|
@@ -1741,6 +2131,13 @@ async function flushAggregates(
|
|
|
1741
2131
|
bucket?.dimensions ?? [],
|
|
1742
2132
|
delta.dimensions,
|
|
1743
2133
|
);
|
|
2134
|
+
const priorSketch = bucket?.visitorSketch;
|
|
2135
|
+
const visitorSketch = priorSketch
|
|
2136
|
+
? unionVisitorSketches(priorSketch, delta.visitorSketch)
|
|
2137
|
+
: delta.visitorSketch;
|
|
2138
|
+
const visitorSketchComplete = bucket
|
|
2139
|
+
? bucket.visitorSketchComplete === true && priorSketch !== undefined
|
|
2140
|
+
: true;
|
|
1744
2141
|
if (bucket) {
|
|
1745
2142
|
await ctx.db.patch("aggregateBuckets", bucket._id, {
|
|
1746
2143
|
events: bucket.events + delta.events,
|
|
@@ -1750,6 +2147,8 @@ async function flushAggregates(
|
|
|
1750
2147
|
outboundClicks: bucket.outboundClicks + delta.outboundClicks,
|
|
1751
2148
|
sessions: bucket.sessions + delta.sessions,
|
|
1752
2149
|
visitors: bucket.visitors + delta.visitors,
|
|
2150
|
+
visitorSketch,
|
|
2151
|
+
visitorSketchComplete,
|
|
1753
2152
|
conversions: bucket.conversions + delta.conversions,
|
|
1754
2153
|
revenueCents: bucket.revenueCents + delta.revenueCents,
|
|
1755
2154
|
dimensions,
|
|
@@ -1767,6 +2166,8 @@ async function flushAggregates(
|
|
|
1767
2166
|
outboundClicks: delta.outboundClicks,
|
|
1768
2167
|
sessions: delta.sessions,
|
|
1769
2168
|
visitors: delta.visitors,
|
|
2169
|
+
visitorSketch,
|
|
2170
|
+
visitorSketchComplete,
|
|
1770
2171
|
conversions: delta.conversions,
|
|
1771
2172
|
revenueCents: delta.revenueCents,
|
|
1772
2173
|
dimensions,
|