@iann29/rastro 0.1.0-alpha.7 → 0.1.0-alpha.9
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 +60 -32
- package/agent/integration.md +14 -6
- package/agent/manifest.json +9 -7
- package/agent/manifest.schema.json +8 -5
- package/dist/client/federation.d.ts +13 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +4 -1
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +419 -17
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +55 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +71 -7
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +6 -1
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +13 -1
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +1 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +13 -2
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/eventStore.d.ts +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +2 -0
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +173 -3
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/reports.d.ts +77 -3
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +275 -3
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts +4 -4
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +12 -2
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts +9 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +25 -0
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +69 -7
- package/dist/component/schema.js +25 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/validators.d.ts +7 -7
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +1 -1
- 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.d.ts +1 -1
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +24 -2
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/federation-setup.md +2 -0
- package/docs/federation.md +4 -0
- package/docs/upgrading.md +91 -10
- package/llms.txt +3 -1
- package/package.json +1 -1
- package/src/component/_generated/component.ts +58 -1
- package/src/component/constants.ts +13 -1
- package/src/component/coverage.ts +12 -1
- package/src/component/http.ts +2 -0
- package/src/component/ingest.ts +278 -1
- package/src/component/reports.ts +364 -2
- package/src/component/retention.ts +16 -1
- package/src/component/sanitize.ts +31 -0
- package/src/component/schema.ts +26 -0
- package/src/component/validators.ts +1 -0
package/src/component/ingest.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
HOURLY_AGGREGATE_SHARDS,
|
|
11
11
|
DIMENSION_SLOTS,
|
|
12
12
|
HOUR_MS,
|
|
13
|
+
LIVE_LEAVE_GRACE_MS,
|
|
13
14
|
LIVE_SESSION_TTL_MS,
|
|
14
15
|
LIVE_SWEEP_INTERVAL_MS,
|
|
15
16
|
MAX_BATCH_EVENTS,
|
|
@@ -20,10 +21,13 @@ import {
|
|
|
20
21
|
MAX_FUNNELS_PER_SITE,
|
|
21
22
|
MAX_FUNNEL_STEPS,
|
|
22
23
|
MAX_GOALS_PER_SITE,
|
|
24
|
+
MAX_SITE_MAP_ROUTES_PER_DAY,
|
|
23
25
|
RATE_LIMIT_WINDOW_MS,
|
|
24
26
|
REPORT_ROLLUP_SHARDS,
|
|
27
|
+
ROUTE_TRANSITION_SLOTS,
|
|
25
28
|
SECONDARY_AGGREGATE_SHARDS,
|
|
26
29
|
SITE_INGEST_SHARDS,
|
|
30
|
+
SITE_MAP_OTHER_ROUTE,
|
|
27
31
|
VITAL_PAGE_SLOTS,
|
|
28
32
|
} from "./constants.js";
|
|
29
33
|
import {
|
|
@@ -57,6 +61,7 @@ import {
|
|
|
57
61
|
cleanString,
|
|
58
62
|
isValidMoney,
|
|
59
63
|
measureBatchBytes,
|
|
64
|
+
normalizeRoute,
|
|
60
65
|
originAllowed,
|
|
61
66
|
sanitizeContext,
|
|
62
67
|
sanitizeCurrency,
|
|
@@ -153,11 +158,25 @@ type VitalRollupDelta = {
|
|
|
153
158
|
{ count: number; sum: number; histogram: number[] }
|
|
154
159
|
>;
|
|
155
160
|
};
|
|
161
|
+
type RouteRollupDelta = {
|
|
162
|
+
siteId: Id<"sites">;
|
|
163
|
+
bucketStart: number;
|
|
164
|
+
shard: number;
|
|
165
|
+
route: string;
|
|
166
|
+
pageviews: number;
|
|
167
|
+
entries: number;
|
|
168
|
+
continued: number;
|
|
169
|
+
dwellMs: number;
|
|
170
|
+
dwellSamples: number;
|
|
171
|
+
visitorSketch: VisitorSketch;
|
|
172
|
+
outgoing: Map<string, number>;
|
|
173
|
+
};
|
|
156
174
|
type ReportRollupDeltas = {
|
|
157
175
|
goals: Map<string, GoalRollupDelta>;
|
|
158
176
|
funnels: Map<string, FunnelRollupDelta>;
|
|
159
177
|
affiliates: Map<string, AffiliateRollupDelta>;
|
|
160
178
|
vitals: Map<string, VitalRollupDelta>;
|
|
179
|
+
routes: Map<string, RouteRollupDelta>;
|
|
161
180
|
};
|
|
162
181
|
type AttributionSnapshot = {
|
|
163
182
|
affiliateId: Id<"affiliates">;
|
|
@@ -297,7 +316,12 @@ export const ingestBatch = mutation({
|
|
|
297
316
|
const bucketStart = eventBucketStart(event.timestamp);
|
|
298
317
|
const groupKey = eventBatchKey(event.sessionId, bucketStart);
|
|
299
318
|
requestGroups.add(groupKey);
|
|
300
|
-
if (
|
|
319
|
+
if (
|
|
320
|
+
event.type === "heartbeat" ||
|
|
321
|
+
event.type === "vital" ||
|
|
322
|
+
event.type === "leave"
|
|
323
|
+
)
|
|
324
|
+
continue;
|
|
301
325
|
const eventIds = eventIdsByGroup.get(groupKey) ?? new Set<string>();
|
|
302
326
|
if (eventIds.has(event.eventId)) {
|
|
303
327
|
duplicateInBatch.add(eventIndex);
|
|
@@ -404,6 +428,50 @@ export const ingestBatch = mutation({
|
|
|
404
428
|
continue;
|
|
405
429
|
}
|
|
406
430
|
|
|
431
|
+
if (event.type === "leave") {
|
|
432
|
+
// A departure only shortens live presence: it stores no telemetry,
|
|
433
|
+
// touches no aggregates, and never extends anything, so a replayed
|
|
434
|
+
// or late leave is inert. The read side filters by expiresAt and the
|
|
435
|
+
// sweep deletes the row, so the dashboard sees the exit in seconds.
|
|
436
|
+
if (!existingSession) {
|
|
437
|
+
rejected += 1;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (
|
|
441
|
+
!(await consumeRateLimit(
|
|
442
|
+
ctx,
|
|
443
|
+
args.siteId,
|
|
444
|
+
event.sessionId,
|
|
445
|
+
eventByteCounts[eventIndex],
|
|
446
|
+
now,
|
|
447
|
+
sessionRateWindows,
|
|
448
|
+
))
|
|
449
|
+
) {
|
|
450
|
+
rejected += 1;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const liveState = await loadLiveSession(
|
|
454
|
+
ctx,
|
|
455
|
+
args.siteId,
|
|
456
|
+
event.sessionId,
|
|
457
|
+
batchState,
|
|
458
|
+
);
|
|
459
|
+
const liveSession = liveState.current;
|
|
460
|
+
if (liveSession) {
|
|
461
|
+
const expiresAt = Math.min(
|
|
462
|
+
liveSession.expiresAt,
|
|
463
|
+
now + LIVE_LEAVE_GRACE_MS,
|
|
464
|
+
);
|
|
465
|
+
if (expiresAt < liveSession.expiresAt) {
|
|
466
|
+
await ctx.db.patch("liveSessions", liveSession._id, { expiresAt });
|
|
467
|
+
liveState.current = { ...liveSession, expiresAt };
|
|
468
|
+
}
|
|
469
|
+
await ensureLiveSweep(ctx, args.siteId, now, batchState);
|
|
470
|
+
}
|
|
471
|
+
accepted += 1;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
407
475
|
if (event.type === "vital") {
|
|
408
476
|
// A vital annotates telemetry its session already produced; it never
|
|
409
477
|
// creates sessions, live presence, event batches, or aggregate counts.
|
|
@@ -766,6 +834,17 @@ async function accountEvent(
|
|
|
766
834
|
input.batchState,
|
|
767
835
|
)
|
|
768
836
|
: await accountTrustedSession(ctx, input.existingSession, event);
|
|
837
|
+
if (input.recordTelemetry && event.type === "pageview") {
|
|
838
|
+
// `existingSession` is the snapshot from before `upsertSession`, so it
|
|
839
|
+
// still carries the route the visitor is leaving.
|
|
840
|
+
foldRouteRollups(
|
|
841
|
+
input.reportRollupDeltas,
|
|
842
|
+
input.siteId,
|
|
843
|
+
event,
|
|
844
|
+
session,
|
|
845
|
+
input.existingSession,
|
|
846
|
+
);
|
|
847
|
+
}
|
|
769
848
|
|
|
770
849
|
const matchedGoalKeys: string[] = [];
|
|
771
850
|
const matchingGoals = input.goals.filter(
|
|
@@ -979,6 +1058,14 @@ async function upsertSession(
|
|
|
979
1058
|
eventCount: existing.eventCount + 1,
|
|
980
1059
|
pageviewCount:
|
|
981
1060
|
existing.pageviewCount + (event.type === "pageview" ? 1 : 0),
|
|
1061
|
+
...(event.type === "pageview"
|
|
1062
|
+
? {
|
|
1063
|
+
lastPageviewAt: Math.max(
|
|
1064
|
+
existing.lastPageviewAt ?? 0,
|
|
1065
|
+
event.timestamp,
|
|
1066
|
+
),
|
|
1067
|
+
}
|
|
1068
|
+
: {}),
|
|
982
1069
|
durationMs: Math.max(0, lastSeenAt - startedAt),
|
|
983
1070
|
conversionCount: existing.conversionCount + Number(financialConversion),
|
|
984
1071
|
revenueCents:
|
|
@@ -1077,6 +1164,7 @@ async function upsertSession(
|
|
|
1077
1164
|
device: context.device ?? "unknown",
|
|
1078
1165
|
eventCount: 1,
|
|
1079
1166
|
pageviewCount: event.type === "pageview" ? 1 : 0,
|
|
1167
|
+
...(event.type === "pageview" ? { lastPageviewAt: event.timestamp } : {}),
|
|
1080
1168
|
durationMs: 0,
|
|
1081
1169
|
conversionCount: Number(financialConversion),
|
|
1082
1170
|
revenueCents: financialConversion ? (event.revenueCents ?? 0) : 0,
|
|
@@ -1532,6 +1620,11 @@ async function updateLiveSession(
|
|
|
1532
1620
|
device: context.device ?? existing.device,
|
|
1533
1621
|
pageviewCount: session.pageviewCount,
|
|
1534
1622
|
eventCount: session.eventCount,
|
|
1623
|
+
// The live map animates a visitor from the route they left to the one
|
|
1624
|
+
// they reached; a heartbeat or click on the same page is not a move.
|
|
1625
|
+
...(event.type === "pageview" && event.path !== existing.path
|
|
1626
|
+
? { previousPath: existing.path, movedAt: event.timestamp }
|
|
1627
|
+
: {}),
|
|
1535
1628
|
...(event.type === "heartbeat"
|
|
1536
1629
|
? {
|
|
1537
1630
|
lastHeartbeatEventId: event.eventId,
|
|
@@ -1876,6 +1969,7 @@ function createReportRollupDeltas(): ReportRollupDeltas {
|
|
|
1876
1969
|
funnels: new Map(),
|
|
1877
1970
|
affiliates: new Map(),
|
|
1878
1971
|
vitals: new Map(),
|
|
1972
|
+
routes: new Map(),
|
|
1879
1973
|
};
|
|
1880
1974
|
}
|
|
1881
1975
|
|
|
@@ -2062,6 +2156,185 @@ function foldVitalRollups(
|
|
|
2062
2156
|
}
|
|
2063
2157
|
}
|
|
2064
2158
|
|
|
2159
|
+
function foldRouteDelta(
|
|
2160
|
+
deltas: ReportRollupDeltas,
|
|
2161
|
+
siteId: Id<"sites">,
|
|
2162
|
+
route: string,
|
|
2163
|
+
timestamp: number,
|
|
2164
|
+
visitorId: string,
|
|
2165
|
+
apply: (delta: RouteRollupDelta) => void,
|
|
2166
|
+
) {
|
|
2167
|
+
const coordinates = reportRollupCoordinates(
|
|
2168
|
+
siteId,
|
|
2169
|
+
`route:${route}`,
|
|
2170
|
+
timestamp,
|
|
2171
|
+
visitorId,
|
|
2172
|
+
);
|
|
2173
|
+
const current = deltas.routes.get(coordinates.key) ?? {
|
|
2174
|
+
siteId,
|
|
2175
|
+
bucketStart: coordinates.bucketStart,
|
|
2176
|
+
shard: coordinates.shard,
|
|
2177
|
+
route,
|
|
2178
|
+
pageviews: 0,
|
|
2179
|
+
entries: 0,
|
|
2180
|
+
continued: 0,
|
|
2181
|
+
dwellMs: 0,
|
|
2182
|
+
dwellSamples: 0,
|
|
2183
|
+
visitorSketch: createVisitorSketch(),
|
|
2184
|
+
outgoing: new Map<string, number>(),
|
|
2185
|
+
};
|
|
2186
|
+
apply(current);
|
|
2187
|
+
deltas.routes.set(coordinates.key, current);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/**
|
|
2191
|
+
* One pageview feeds the site map three ways: the route it lands on gains a
|
|
2192
|
+
* view (and an entry when it opens the session), the route it leaves gains
|
|
2193
|
+
* the transition and the time spent there, and the session's entry route is
|
|
2194
|
+
* marked as continued the first time its visitor moves on — which is what
|
|
2195
|
+
* lets the report derive bounces and exits without a session-end hook.
|
|
2196
|
+
*/
|
|
2197
|
+
function foldRouteRollups(
|
|
2198
|
+
deltas: ReportRollupDeltas,
|
|
2199
|
+
siteId: Id<"sites">,
|
|
2200
|
+
event: TrackerEvent,
|
|
2201
|
+
session: SessionState,
|
|
2202
|
+
existing: CachedDocument<"sessions"> | null,
|
|
2203
|
+
) {
|
|
2204
|
+
const route = normalizeRoute(event.path);
|
|
2205
|
+
const priorPageviews = existing?.pageviewCount ?? 0;
|
|
2206
|
+
const firstPageview = existing === null || priorPageviews === 0;
|
|
2207
|
+
foldRouteDelta(
|
|
2208
|
+
deltas,
|
|
2209
|
+
siteId,
|
|
2210
|
+
route,
|
|
2211
|
+
event.timestamp,
|
|
2212
|
+
event.visitorId,
|
|
2213
|
+
(delta) => {
|
|
2214
|
+
delta.pageviews += 1;
|
|
2215
|
+
if (firstPageview) delta.entries += 1;
|
|
2216
|
+
delta.visitorSketch = addVisitor(delta.visitorSketch, session.visitorKey);
|
|
2217
|
+
},
|
|
2218
|
+
);
|
|
2219
|
+
// Out-of-order telemetry keeps its view but cannot say where it came from.
|
|
2220
|
+
if (existing === null || firstPageview) return;
|
|
2221
|
+
if (event.timestamp < existing.lastSeenAt) return;
|
|
2222
|
+
const previous = normalizeRoute(existing.exitPath);
|
|
2223
|
+
const lastPageviewAt = existing.lastPageviewAt;
|
|
2224
|
+
foldRouteDelta(
|
|
2225
|
+
deltas,
|
|
2226
|
+
siteId,
|
|
2227
|
+
previous,
|
|
2228
|
+
event.timestamp,
|
|
2229
|
+
event.visitorId,
|
|
2230
|
+
(delta) => {
|
|
2231
|
+
delta.outgoing.set(route, (delta.outgoing.get(route) ?? 0) + 1);
|
|
2232
|
+
if (lastPageviewAt !== undefined && event.timestamp >= lastPageviewAt) {
|
|
2233
|
+
delta.dwellMs += event.timestamp - lastPageviewAt;
|
|
2234
|
+
delta.dwellSamples += 1;
|
|
2235
|
+
}
|
|
2236
|
+
},
|
|
2237
|
+
);
|
|
2238
|
+
if (priorPageviews === 1) {
|
|
2239
|
+
foldRouteDelta(
|
|
2240
|
+
deltas,
|
|
2241
|
+
siteId,
|
|
2242
|
+
normalizeRoute(existing.entryPath),
|
|
2243
|
+
existing.startedAt,
|
|
2244
|
+
event.visitorId,
|
|
2245
|
+
(delta) => {
|
|
2246
|
+
delta.continued += 1;
|
|
2247
|
+
},
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
async function upsertRouteRollup(
|
|
2253
|
+
ctx: MutationCtx,
|
|
2254
|
+
delta: RouteRollupDelta,
|
|
2255
|
+
dayRowCounts: Map<string, number>,
|
|
2256
|
+
) {
|
|
2257
|
+
const existing = await ctx.db
|
|
2258
|
+
.query("routeDailyRollups")
|
|
2259
|
+
.withIndex("by_siteId_and_route_and_bucketStart_and_shard", (range) =>
|
|
2260
|
+
range
|
|
2261
|
+
.eq("siteId", delta.siteId)
|
|
2262
|
+
.eq("route", delta.route)
|
|
2263
|
+
.eq("bucketStart", delta.bucketStart)
|
|
2264
|
+
.eq("shard", delta.shard),
|
|
2265
|
+
)
|
|
2266
|
+
.unique();
|
|
2267
|
+
if (!existing && delta.route !== SITE_MAP_OTHER_ROUTE) {
|
|
2268
|
+
// A new route row spends one bounded read per batch to respect the daily
|
|
2269
|
+
// cap; past it, the day's remaining routes fold into "(other)".
|
|
2270
|
+
const capRows = MAX_SITE_MAP_ROUTES_PER_DAY * REPORT_ROLLUP_SHARDS;
|
|
2271
|
+
const dayKey = `${delta.siteId}:${delta.bucketStart}`;
|
|
2272
|
+
let dayRows = dayRowCounts.get(dayKey);
|
|
2273
|
+
if (dayRows === undefined) {
|
|
2274
|
+
dayRows = (
|
|
2275
|
+
await ctx.db
|
|
2276
|
+
.query("routeDailyRollups")
|
|
2277
|
+
.withIndex("by_siteId_and_bucketStart", (range) =>
|
|
2278
|
+
range
|
|
2279
|
+
.eq("siteId", delta.siteId)
|
|
2280
|
+
.eq("bucketStart", delta.bucketStart),
|
|
2281
|
+
)
|
|
2282
|
+
.take(capRows + 1)
|
|
2283
|
+
).length;
|
|
2284
|
+
}
|
|
2285
|
+
if (dayRows >= capRows) {
|
|
2286
|
+
await upsertRouteRollup(
|
|
2287
|
+
ctx,
|
|
2288
|
+
{ ...delta, route: SITE_MAP_OTHER_ROUTE },
|
|
2289
|
+
dayRowCounts,
|
|
2290
|
+
);
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
dayRowCounts.set(dayKey, dayRows + 1);
|
|
2294
|
+
}
|
|
2295
|
+
const outgoing = existing?.outgoing.map((entry) => ({ ...entry })) ?? [];
|
|
2296
|
+
for (const [target, count] of delta.outgoing) {
|
|
2297
|
+
let entry = outgoing.find((candidate) => candidate.route === target);
|
|
2298
|
+
if (!entry) {
|
|
2299
|
+
const named = outgoing.filter(
|
|
2300
|
+
(candidate) => candidate.route !== SITE_MAP_OTHER_ROUTE,
|
|
2301
|
+
).length;
|
|
2302
|
+
const route =
|
|
2303
|
+
target !== SITE_MAP_OTHER_ROUTE && named >= ROUTE_TRANSITION_SLOTS
|
|
2304
|
+
? SITE_MAP_OTHER_ROUTE
|
|
2305
|
+
: target;
|
|
2306
|
+
entry = outgoing.find((candidate) => candidate.route === route);
|
|
2307
|
+
if (!entry) {
|
|
2308
|
+
entry = { route, count: 0 };
|
|
2309
|
+
outgoing.push(entry);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
entry.count += count;
|
|
2313
|
+
}
|
|
2314
|
+
const fields = {
|
|
2315
|
+
siteId: delta.siteId,
|
|
2316
|
+
bucketStart: delta.bucketStart,
|
|
2317
|
+
shard: delta.shard,
|
|
2318
|
+
generation: 1 as const,
|
|
2319
|
+
route: delta.route,
|
|
2320
|
+
pageviews: (existing?.pageviews ?? 0) + delta.pageviews,
|
|
2321
|
+
entries: (existing?.entries ?? 0) + delta.entries,
|
|
2322
|
+
continued: (existing?.continued ?? 0) + delta.continued,
|
|
2323
|
+
dwellMs: (existing?.dwellMs ?? 0) + delta.dwellMs,
|
|
2324
|
+
dwellSamples: (existing?.dwellSamples ?? 0) + delta.dwellSamples,
|
|
2325
|
+
visitorSketch: unionVisitorSketches(
|
|
2326
|
+
existing?.visitorSketch ?? createVisitorSketch(),
|
|
2327
|
+
delta.visitorSketch,
|
|
2328
|
+
),
|
|
2329
|
+
outgoing,
|
|
2330
|
+
};
|
|
2331
|
+
if (existing) {
|
|
2332
|
+
await ctx.db.replace("routeDailyRollups", existing._id, fields);
|
|
2333
|
+
} else {
|
|
2334
|
+
await ctx.db.insert("routeDailyRollups", fields);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2065
2338
|
async function upsertVitalRollup(ctx: MutationCtx, delta: VitalRollupDelta) {
|
|
2066
2339
|
const existing = await ctx.db
|
|
2067
2340
|
.query("vitalRollups")
|
|
@@ -2233,6 +2506,10 @@ async function flushReportRollups(
|
|
|
2233
2506
|
await ctx.db.insert("affiliateDailyRollups", fields);
|
|
2234
2507
|
}
|
|
2235
2508
|
}
|
|
2509
|
+
const routeDayRowCounts = new Map<string, number>();
|
|
2510
|
+
for (const delta of deltas.routes.values()) {
|
|
2511
|
+
await upsertRouteRollup(ctx, delta, routeDayRowCounts);
|
|
2512
|
+
}
|
|
2236
2513
|
for (const delta of deltas.vitals.values()) {
|
|
2237
2514
|
await upsertVitalRollup(ctx, delta);
|
|
2238
2515
|
}
|