@iann29/rastro 0.1.0-alpha.7 → 0.1.0-alpha.8
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 +34 -16
- 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 +412 -10
- 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 +68 -4
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +4 -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 +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/http.d.ts.map +1 -1
- package/dist/component/http.js +1 -0
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +144 -2
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/reports.d.ts +75 -1
- 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 +65 -3
- package/dist/component/schema.js +25 -0
- package/dist/component/schema.js.map +1 -1
- package/docs/federation-setup.md +2 -0
- package/docs/federation.md +4 -0
- package/docs/upgrading.md +53 -10
- package/llms.txt +2 -0
- package/package.json +1 -1
- package/src/component/_generated/component.ts +55 -1
- package/src/component/constants.ts +11 -0
- package/src/component/coverage.ts +12 -1
- package/src/component/http.ts +1 -0
- package/src/component/ingest.ts +227 -0
- 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/ingest.ts
CHANGED
|
@@ -20,10 +20,13 @@ import {
|
|
|
20
20
|
MAX_FUNNELS_PER_SITE,
|
|
21
21
|
MAX_FUNNEL_STEPS,
|
|
22
22
|
MAX_GOALS_PER_SITE,
|
|
23
|
+
MAX_SITE_MAP_ROUTES_PER_DAY,
|
|
23
24
|
RATE_LIMIT_WINDOW_MS,
|
|
24
25
|
REPORT_ROLLUP_SHARDS,
|
|
26
|
+
ROUTE_TRANSITION_SLOTS,
|
|
25
27
|
SECONDARY_AGGREGATE_SHARDS,
|
|
26
28
|
SITE_INGEST_SHARDS,
|
|
29
|
+
SITE_MAP_OTHER_ROUTE,
|
|
27
30
|
VITAL_PAGE_SLOTS,
|
|
28
31
|
} from "./constants.js";
|
|
29
32
|
import {
|
|
@@ -57,6 +60,7 @@ import {
|
|
|
57
60
|
cleanString,
|
|
58
61
|
isValidMoney,
|
|
59
62
|
measureBatchBytes,
|
|
63
|
+
normalizeRoute,
|
|
60
64
|
originAllowed,
|
|
61
65
|
sanitizeContext,
|
|
62
66
|
sanitizeCurrency,
|
|
@@ -153,11 +157,25 @@ type VitalRollupDelta = {
|
|
|
153
157
|
{ count: number; sum: number; histogram: number[] }
|
|
154
158
|
>;
|
|
155
159
|
};
|
|
160
|
+
type RouteRollupDelta = {
|
|
161
|
+
siteId: Id<"sites">;
|
|
162
|
+
bucketStart: number;
|
|
163
|
+
shard: number;
|
|
164
|
+
route: string;
|
|
165
|
+
pageviews: number;
|
|
166
|
+
entries: number;
|
|
167
|
+
continued: number;
|
|
168
|
+
dwellMs: number;
|
|
169
|
+
dwellSamples: number;
|
|
170
|
+
visitorSketch: VisitorSketch;
|
|
171
|
+
outgoing: Map<string, number>;
|
|
172
|
+
};
|
|
156
173
|
type ReportRollupDeltas = {
|
|
157
174
|
goals: Map<string, GoalRollupDelta>;
|
|
158
175
|
funnels: Map<string, FunnelRollupDelta>;
|
|
159
176
|
affiliates: Map<string, AffiliateRollupDelta>;
|
|
160
177
|
vitals: Map<string, VitalRollupDelta>;
|
|
178
|
+
routes: Map<string, RouteRollupDelta>;
|
|
161
179
|
};
|
|
162
180
|
type AttributionSnapshot = {
|
|
163
181
|
affiliateId: Id<"affiliates">;
|
|
@@ -766,6 +784,17 @@ async function accountEvent(
|
|
|
766
784
|
input.batchState,
|
|
767
785
|
)
|
|
768
786
|
: await accountTrustedSession(ctx, input.existingSession, event);
|
|
787
|
+
if (input.recordTelemetry && event.type === "pageview") {
|
|
788
|
+
// `existingSession` is the snapshot from before `upsertSession`, so it
|
|
789
|
+
// still carries the route the visitor is leaving.
|
|
790
|
+
foldRouteRollups(
|
|
791
|
+
input.reportRollupDeltas,
|
|
792
|
+
input.siteId,
|
|
793
|
+
event,
|
|
794
|
+
session,
|
|
795
|
+
input.existingSession,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
769
798
|
|
|
770
799
|
const matchedGoalKeys: string[] = [];
|
|
771
800
|
const matchingGoals = input.goals.filter(
|
|
@@ -979,6 +1008,14 @@ async function upsertSession(
|
|
|
979
1008
|
eventCount: existing.eventCount + 1,
|
|
980
1009
|
pageviewCount:
|
|
981
1010
|
existing.pageviewCount + (event.type === "pageview" ? 1 : 0),
|
|
1011
|
+
...(event.type === "pageview"
|
|
1012
|
+
? {
|
|
1013
|
+
lastPageviewAt: Math.max(
|
|
1014
|
+
existing.lastPageviewAt ?? 0,
|
|
1015
|
+
event.timestamp,
|
|
1016
|
+
),
|
|
1017
|
+
}
|
|
1018
|
+
: {}),
|
|
982
1019
|
durationMs: Math.max(0, lastSeenAt - startedAt),
|
|
983
1020
|
conversionCount: existing.conversionCount + Number(financialConversion),
|
|
984
1021
|
revenueCents:
|
|
@@ -1077,6 +1114,7 @@ async function upsertSession(
|
|
|
1077
1114
|
device: context.device ?? "unknown",
|
|
1078
1115
|
eventCount: 1,
|
|
1079
1116
|
pageviewCount: event.type === "pageview" ? 1 : 0,
|
|
1117
|
+
...(event.type === "pageview" ? { lastPageviewAt: event.timestamp } : {}),
|
|
1080
1118
|
durationMs: 0,
|
|
1081
1119
|
conversionCount: Number(financialConversion),
|
|
1082
1120
|
revenueCents: financialConversion ? (event.revenueCents ?? 0) : 0,
|
|
@@ -1532,6 +1570,11 @@ async function updateLiveSession(
|
|
|
1532
1570
|
device: context.device ?? existing.device,
|
|
1533
1571
|
pageviewCount: session.pageviewCount,
|
|
1534
1572
|
eventCount: session.eventCount,
|
|
1573
|
+
// The live map animates a visitor from the route they left to the one
|
|
1574
|
+
// they reached; a heartbeat or click on the same page is not a move.
|
|
1575
|
+
...(event.type === "pageview" && event.path !== existing.path
|
|
1576
|
+
? { previousPath: existing.path, movedAt: event.timestamp }
|
|
1577
|
+
: {}),
|
|
1535
1578
|
...(event.type === "heartbeat"
|
|
1536
1579
|
? {
|
|
1537
1580
|
lastHeartbeatEventId: event.eventId,
|
|
@@ -1876,6 +1919,7 @@ function createReportRollupDeltas(): ReportRollupDeltas {
|
|
|
1876
1919
|
funnels: new Map(),
|
|
1877
1920
|
affiliates: new Map(),
|
|
1878
1921
|
vitals: new Map(),
|
|
1922
|
+
routes: new Map(),
|
|
1879
1923
|
};
|
|
1880
1924
|
}
|
|
1881
1925
|
|
|
@@ -2062,6 +2106,185 @@ function foldVitalRollups(
|
|
|
2062
2106
|
}
|
|
2063
2107
|
}
|
|
2064
2108
|
|
|
2109
|
+
function foldRouteDelta(
|
|
2110
|
+
deltas: ReportRollupDeltas,
|
|
2111
|
+
siteId: Id<"sites">,
|
|
2112
|
+
route: string,
|
|
2113
|
+
timestamp: number,
|
|
2114
|
+
visitorId: string,
|
|
2115
|
+
apply: (delta: RouteRollupDelta) => void,
|
|
2116
|
+
) {
|
|
2117
|
+
const coordinates = reportRollupCoordinates(
|
|
2118
|
+
siteId,
|
|
2119
|
+
`route:${route}`,
|
|
2120
|
+
timestamp,
|
|
2121
|
+
visitorId,
|
|
2122
|
+
);
|
|
2123
|
+
const current = deltas.routes.get(coordinates.key) ?? {
|
|
2124
|
+
siteId,
|
|
2125
|
+
bucketStart: coordinates.bucketStart,
|
|
2126
|
+
shard: coordinates.shard,
|
|
2127
|
+
route,
|
|
2128
|
+
pageviews: 0,
|
|
2129
|
+
entries: 0,
|
|
2130
|
+
continued: 0,
|
|
2131
|
+
dwellMs: 0,
|
|
2132
|
+
dwellSamples: 0,
|
|
2133
|
+
visitorSketch: createVisitorSketch(),
|
|
2134
|
+
outgoing: new Map<string, number>(),
|
|
2135
|
+
};
|
|
2136
|
+
apply(current);
|
|
2137
|
+
deltas.routes.set(coordinates.key, current);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
/**
|
|
2141
|
+
* One pageview feeds the site map three ways: the route it lands on gains a
|
|
2142
|
+
* view (and an entry when it opens the session), the route it leaves gains
|
|
2143
|
+
* the transition and the time spent there, and the session's entry route is
|
|
2144
|
+
* marked as continued the first time its visitor moves on — which is what
|
|
2145
|
+
* lets the report derive bounces and exits without a session-end hook.
|
|
2146
|
+
*/
|
|
2147
|
+
function foldRouteRollups(
|
|
2148
|
+
deltas: ReportRollupDeltas,
|
|
2149
|
+
siteId: Id<"sites">,
|
|
2150
|
+
event: TrackerEvent,
|
|
2151
|
+
session: SessionState,
|
|
2152
|
+
existing: CachedDocument<"sessions"> | null,
|
|
2153
|
+
) {
|
|
2154
|
+
const route = normalizeRoute(event.path);
|
|
2155
|
+
const priorPageviews = existing?.pageviewCount ?? 0;
|
|
2156
|
+
const firstPageview = existing === null || priorPageviews === 0;
|
|
2157
|
+
foldRouteDelta(
|
|
2158
|
+
deltas,
|
|
2159
|
+
siteId,
|
|
2160
|
+
route,
|
|
2161
|
+
event.timestamp,
|
|
2162
|
+
event.visitorId,
|
|
2163
|
+
(delta) => {
|
|
2164
|
+
delta.pageviews += 1;
|
|
2165
|
+
if (firstPageview) delta.entries += 1;
|
|
2166
|
+
delta.visitorSketch = addVisitor(delta.visitorSketch, session.visitorKey);
|
|
2167
|
+
},
|
|
2168
|
+
);
|
|
2169
|
+
// Out-of-order telemetry keeps its view but cannot say where it came from.
|
|
2170
|
+
if (existing === null || firstPageview) return;
|
|
2171
|
+
if (event.timestamp < existing.lastSeenAt) return;
|
|
2172
|
+
const previous = normalizeRoute(existing.exitPath);
|
|
2173
|
+
const lastPageviewAt = existing.lastPageviewAt;
|
|
2174
|
+
foldRouteDelta(
|
|
2175
|
+
deltas,
|
|
2176
|
+
siteId,
|
|
2177
|
+
previous,
|
|
2178
|
+
event.timestamp,
|
|
2179
|
+
event.visitorId,
|
|
2180
|
+
(delta) => {
|
|
2181
|
+
delta.outgoing.set(route, (delta.outgoing.get(route) ?? 0) + 1);
|
|
2182
|
+
if (lastPageviewAt !== undefined && event.timestamp >= lastPageviewAt) {
|
|
2183
|
+
delta.dwellMs += event.timestamp - lastPageviewAt;
|
|
2184
|
+
delta.dwellSamples += 1;
|
|
2185
|
+
}
|
|
2186
|
+
},
|
|
2187
|
+
);
|
|
2188
|
+
if (priorPageviews === 1) {
|
|
2189
|
+
foldRouteDelta(
|
|
2190
|
+
deltas,
|
|
2191
|
+
siteId,
|
|
2192
|
+
normalizeRoute(existing.entryPath),
|
|
2193
|
+
existing.startedAt,
|
|
2194
|
+
event.visitorId,
|
|
2195
|
+
(delta) => {
|
|
2196
|
+
delta.continued += 1;
|
|
2197
|
+
},
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
async function upsertRouteRollup(
|
|
2203
|
+
ctx: MutationCtx,
|
|
2204
|
+
delta: RouteRollupDelta,
|
|
2205
|
+
dayRowCounts: Map<string, number>,
|
|
2206
|
+
) {
|
|
2207
|
+
const existing = await ctx.db
|
|
2208
|
+
.query("routeDailyRollups")
|
|
2209
|
+
.withIndex("by_siteId_and_route_and_bucketStart_and_shard", (range) =>
|
|
2210
|
+
range
|
|
2211
|
+
.eq("siteId", delta.siteId)
|
|
2212
|
+
.eq("route", delta.route)
|
|
2213
|
+
.eq("bucketStart", delta.bucketStart)
|
|
2214
|
+
.eq("shard", delta.shard),
|
|
2215
|
+
)
|
|
2216
|
+
.unique();
|
|
2217
|
+
if (!existing && delta.route !== SITE_MAP_OTHER_ROUTE) {
|
|
2218
|
+
// A new route row spends one bounded read per batch to respect the daily
|
|
2219
|
+
// cap; past it, the day's remaining routes fold into "(other)".
|
|
2220
|
+
const capRows = MAX_SITE_MAP_ROUTES_PER_DAY * REPORT_ROLLUP_SHARDS;
|
|
2221
|
+
const dayKey = `${delta.siteId}:${delta.bucketStart}`;
|
|
2222
|
+
let dayRows = dayRowCounts.get(dayKey);
|
|
2223
|
+
if (dayRows === undefined) {
|
|
2224
|
+
dayRows = (
|
|
2225
|
+
await ctx.db
|
|
2226
|
+
.query("routeDailyRollups")
|
|
2227
|
+
.withIndex("by_siteId_and_bucketStart", (range) =>
|
|
2228
|
+
range
|
|
2229
|
+
.eq("siteId", delta.siteId)
|
|
2230
|
+
.eq("bucketStart", delta.bucketStart),
|
|
2231
|
+
)
|
|
2232
|
+
.take(capRows + 1)
|
|
2233
|
+
).length;
|
|
2234
|
+
}
|
|
2235
|
+
if (dayRows >= capRows) {
|
|
2236
|
+
await upsertRouteRollup(
|
|
2237
|
+
ctx,
|
|
2238
|
+
{ ...delta, route: SITE_MAP_OTHER_ROUTE },
|
|
2239
|
+
dayRowCounts,
|
|
2240
|
+
);
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
dayRowCounts.set(dayKey, dayRows + 1);
|
|
2244
|
+
}
|
|
2245
|
+
const outgoing = existing?.outgoing.map((entry) => ({ ...entry })) ?? [];
|
|
2246
|
+
for (const [target, count] of delta.outgoing) {
|
|
2247
|
+
let entry = outgoing.find((candidate) => candidate.route === target);
|
|
2248
|
+
if (!entry) {
|
|
2249
|
+
const named = outgoing.filter(
|
|
2250
|
+
(candidate) => candidate.route !== SITE_MAP_OTHER_ROUTE,
|
|
2251
|
+
).length;
|
|
2252
|
+
const route =
|
|
2253
|
+
target !== SITE_MAP_OTHER_ROUTE && named >= ROUTE_TRANSITION_SLOTS
|
|
2254
|
+
? SITE_MAP_OTHER_ROUTE
|
|
2255
|
+
: target;
|
|
2256
|
+
entry = outgoing.find((candidate) => candidate.route === route);
|
|
2257
|
+
if (!entry) {
|
|
2258
|
+
entry = { route, count: 0 };
|
|
2259
|
+
outgoing.push(entry);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
entry.count += count;
|
|
2263
|
+
}
|
|
2264
|
+
const fields = {
|
|
2265
|
+
siteId: delta.siteId,
|
|
2266
|
+
bucketStart: delta.bucketStart,
|
|
2267
|
+
shard: delta.shard,
|
|
2268
|
+
generation: 1 as const,
|
|
2269
|
+
route: delta.route,
|
|
2270
|
+
pageviews: (existing?.pageviews ?? 0) + delta.pageviews,
|
|
2271
|
+
entries: (existing?.entries ?? 0) + delta.entries,
|
|
2272
|
+
continued: (existing?.continued ?? 0) + delta.continued,
|
|
2273
|
+
dwellMs: (existing?.dwellMs ?? 0) + delta.dwellMs,
|
|
2274
|
+
dwellSamples: (existing?.dwellSamples ?? 0) + delta.dwellSamples,
|
|
2275
|
+
visitorSketch: unionVisitorSketches(
|
|
2276
|
+
existing?.visitorSketch ?? createVisitorSketch(),
|
|
2277
|
+
delta.visitorSketch,
|
|
2278
|
+
),
|
|
2279
|
+
outgoing,
|
|
2280
|
+
};
|
|
2281
|
+
if (existing) {
|
|
2282
|
+
await ctx.db.replace("routeDailyRollups", existing._id, fields);
|
|
2283
|
+
} else {
|
|
2284
|
+
await ctx.db.insert("routeDailyRollups", fields);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2065
2288
|
async function upsertVitalRollup(ctx: MutationCtx, delta: VitalRollupDelta) {
|
|
2066
2289
|
const existing = await ctx.db
|
|
2067
2290
|
.query("vitalRollups")
|
|
@@ -2233,6 +2456,10 @@ async function flushReportRollups(
|
|
|
2233
2456
|
await ctx.db.insert("affiliateDailyRollups", fields);
|
|
2234
2457
|
}
|
|
2235
2458
|
}
|
|
2459
|
+
const routeDayRowCounts = new Map<string, number>();
|
|
2460
|
+
for (const delta of deltas.routes.values()) {
|
|
2461
|
+
await upsertRouteRollup(ctx, delta, routeDayRowCounts);
|
|
2462
|
+
}
|
|
2236
2463
|
for (const delta of deltas.vitals.values()) {
|
|
2237
2464
|
await upsertVitalRollup(ctx, delta);
|
|
2238
2465
|
}
|