@iann29/rastro 0.2.0 → 0.4.0
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 +82 -36
- package/agent/integration.md +4 -0
- package/agent/manifest.json +9 -5
- package/agent/manifest.schema.json +7 -1
- package/dist/client/federation.d.ts +18 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +7 -1
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +138 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +30 -0
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +46 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +1 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/constants.d.ts +7 -2
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +24 -3
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/geo.d.ts +1 -0
- package/dist/component/geo.d.ts.map +1 -1
- package/dist/component/geo.js +1 -1
- package/dist/component/geo.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +14 -1
- package/dist/component/http.js.map +1 -1
- package/dist/component/identity.d.ts +12 -0
- package/dist/component/identity.d.ts.map +1 -1
- package/dist/component/identity.js +30 -2
- package/dist/component/identity.js.map +1 -1
- package/dist/component/ingest.d.ts +34 -0
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +86 -21
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +34 -4
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +47 -0
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +96 -6
- package/dist/component/reports.js.map +1 -1
- package/dist/component/rollupStore.d.ts +7 -4
- package/dist/component/rollupStore.d.ts.map +1 -1
- package/dist/component/rollupStore.js +37 -17
- package/dist/component/rollupStore.js.map +1 -1
- package/dist/component/rollups.d.ts +27 -8
- package/dist/component/rollups.d.ts.map +1 -1
- package/dist/component/rollups.js +110 -32
- package/dist/component/rollups.js.map +1 -1
- package/dist/component/schema.d.ts +6 -2
- package/dist/component/schema.js +5 -2
- package/dist/component/schema.js.map +1 -1
- package/dist/tracker/generated.d.ts +11 -11
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +11 -11
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.js +88 -43
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker/vitals.d.ts.map +1 -1
- package/dist/tracker/vitals.js +22 -4
- package/dist/tracker/vitals.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/dist/vitals.min.js +1 -1
- package/docs/federation-setup.md +2 -0
- package/docs/federation.md +6 -0
- package/docs/upgrading.md +84 -0
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/component/_generated/component.ts +49 -0
- package/src/component/_generated/server.ts +1 -0
- package/src/component/constants.ts +24 -3
- package/src/component/convex.config.ts +3 -0
- package/src/component/geo.ts +1 -1
- package/src/component/http.ts +15 -1
- package/src/component/identity.ts +41 -2
- package/src/component/ingest.ts +120 -36
- package/src/component/live.ts +51 -4
- package/src/component/reports.ts +120 -8
- package/src/component/rollupStore.ts +51 -20
- package/src/component/rollups.ts +141 -32
- package/src/component/schema.ts +6 -3
- package/src/tracker/generated.ts +11 -11
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { DAY_MS } from "./constants.js";
|
|
3
|
+
import { parseIpv6 } from "./geo.js";
|
|
3
4
|
import { internalMutation, internalQuery } from "./_generated/server.js";
|
|
4
5
|
/**
|
|
5
6
|
* Anonymous visitors (no host-supplied id) are counted through a salted hash of
|
|
@@ -47,8 +48,35 @@ export function randomSecret() {
|
|
|
47
48
|
return hex(crypto.getRandomValues(new Uint8Array(32)));
|
|
48
49
|
}
|
|
49
50
|
export async function deriveVisitorKey(input) {
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
return saltedDailyKey(input.secret, input.now, [
|
|
52
|
+
input.siteId,
|
|
53
|
+
input.ip,
|
|
54
|
+
input.userAgent,
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The per-client ingest budget's key: the same salted daily hash, over the
|
|
59
|
+
* client's network instead of its device — the IPv4 address, or the /64 of an
|
|
60
|
+
* IPv6 address, the block one subscriber gets and could otherwise walk through
|
|
61
|
+
* one address per request.
|
|
62
|
+
*/
|
|
63
|
+
export async function deriveClientKey(input) {
|
|
64
|
+
const groups = parseIpv6(input.ip.toLowerCase());
|
|
65
|
+
const network = groups
|
|
66
|
+
? `${groups
|
|
67
|
+
.slice(0, 4)
|
|
68
|
+
.map((group) => group.toString(16))
|
|
69
|
+
.join(":")}::/64`
|
|
70
|
+
: input.ip;
|
|
71
|
+
return saltedDailyKey(input.secret, input.now, [
|
|
72
|
+
"client",
|
|
73
|
+
input.siteId,
|
|
74
|
+
network,
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
async function saltedDailyKey(secret, now, parts) {
|
|
78
|
+
const dayStart = Math.floor(now / DAY_MS) * DAY_MS;
|
|
79
|
+
const material = new TextEncoder().encode([secret, dayStart, ...parts].join(" "));
|
|
52
80
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", material));
|
|
53
81
|
return hex(digest).slice(0, KEY_HEX_LENGTH);
|
|
54
82
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/component/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEzE;;;;;;;GAOG;AACH,MAAM,cAAc,GAAG,EAAE,CAAC;AAC1B,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAExC,MAAM,CAAC,MAAM,MAAM,GAAG,aAAa,CAAC;IAClC,IAAI,EAAE,EAAE;IACR,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACtC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACrB,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,EAAE;aACrB,KAAK,CAAC,gBAAgB,CAAC;aACvB,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;aAC1D,MAAM,EAAE,CAAC;QACZ,OAAO,GAAG,EAAE,KAAK,IAAI,IAAI,CAAC;IAC5B,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAC;IAC3C,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE;IAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC3B,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,EAAE;aAC1B,KAAK,CAAC,gBAAgB,CAAC;aACvB,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;aAC1D,MAAM,EAAE,CAAC;QACZ,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC,KAAK,CAAC;QACpC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACxE,CAAC;QACD,MAAM,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE;YACpC,GAAG,EAAE,SAAS;YACd,KAAK,EAAE,IAAI,CAAC,SAAS;YACrB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,UAAU,YAAY;IAC1B,OAAO,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAMtC;IACC,MAAM,
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/component/identity.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
+
import type { Doc, Id } from "./_generated/dataModel.js";
|
|
2
|
+
import type { MutationCtx } from "./_generated/server.js";
|
|
3
|
+
import { type VisitorSketch } from "./cardinality.js";
|
|
4
|
+
type AggregateDelta = {
|
|
5
|
+
siteId: Id<"sites">;
|
|
6
|
+
granularity: "hour" | "day" | "localDay";
|
|
7
|
+
bucketStart: number;
|
|
8
|
+
shard: number;
|
|
9
|
+
events: number;
|
|
10
|
+
pageviews: number;
|
|
11
|
+
clicks: number;
|
|
12
|
+
customEvents: number;
|
|
13
|
+
outboundClicks: number;
|
|
14
|
+
sessions: number;
|
|
15
|
+
visitors: number;
|
|
16
|
+
visitorSketch: VisitorSketch;
|
|
17
|
+
conversions: number;
|
|
18
|
+
revenueCents: number;
|
|
19
|
+
dimensions: Doc<"aggregateBuckets">["dimensions"];
|
|
20
|
+
durationMs: number;
|
|
21
|
+
continuedSessions: number;
|
|
22
|
+
newVisitors: number;
|
|
23
|
+
};
|
|
24
|
+
export type AggregateDeltas = Map<string, AggregateDelta>;
|
|
1
25
|
export declare const ingestBatch: import("convex/server").RegisteredMutation<"public", {
|
|
26
|
+
clientKey?: string | undefined;
|
|
2
27
|
context?: {
|
|
3
28
|
browser?: string | undefined;
|
|
4
29
|
city?: string | undefined;
|
|
@@ -50,4 +75,13 @@ export declare const trackConversion: import("convex/server").RegisteredMutation
|
|
|
50
75
|
duplicates: number;
|
|
51
76
|
rejected: number;
|
|
52
77
|
}>>;
|
|
78
|
+
/**
|
|
79
|
+
* Folds session duration growth without telemetry: a departure's final
|
|
80
|
+
* interval, or the heartbeat time of a visitor the live sweep retired. The
|
|
81
|
+
* shard follows `shardKey`: the session for a departure, the site for a
|
|
82
|
+
* sweep.
|
|
83
|
+
*/
|
|
84
|
+
export declare function foldSessionDuration(aggregateDeltas: AggregateDeltas, siteId: Id<"sites">, shardKey: string, timestamp: number, durationDeltaMs: number, timezone?: string): void;
|
|
85
|
+
export declare function flushAggregates(ctx: MutationCtx, aggregateDeltas: AggregateDeltas): Promise<void>;
|
|
86
|
+
export {};
|
|
53
87
|
//# sourceMappingURL=ingest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/component/ingest.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/component/ingest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AA2B1D,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAyE1B,KAAK,cAAc,GAAG;IACpB,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,KAAK,GAAG,UAAU,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,GAAG,CAAC,kBAAkB,CAAC,CAAC,YAAY,CAAC,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AACF,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAqF1D,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwXtB,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;GA+H1B,CAAC;AA2uDH;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,eAAe,EAAE,eAAe,EAChC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,QAAQ,CAAC,EAAE,MAAM,QAgClB;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,WAAW,EAChB,eAAe,EAAE,eAAe,iBAyEjC"}
|
package/dist/component/ingest.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { mutation } from "./_generated/server.js";
|
|
3
3
|
import { internal } from "./_generated/api.js";
|
|
4
|
-
import { AGGREGATE_SHARDS, MAX_BATCH_EVENT_GROUPS, DAY_MS, HOURLY_AGGREGATE_SHARDS, DIMENSION_SLOTS, HOUR_MS, LIVE_LEAVE_GRACE_MS, LIVE_SESSION_TTL_MS, LIVE_SWEEP_INTERVAL_MS, MAX_BATCH_EVENTS, MAX_SITE_BYTES_PER_SHARD_WINDOW, MAX_SITE_EVENTS_PER_SHARD_WINDOW, MAX_BYTES_PER_SESSION_WINDOW, MAX_EVENTS_PER_SESSION_WINDOW, MAX_FUNNELS_PER_SITE, MAX_GOALS_PER_SITE, RATE_LIMIT_WINDOW_MS, REPORT_ROLLUP_SHARDS, SECONDARY_AGGREGATE_SHARDS, SITE_INGEST_SHARDS, } from "./constants.js";
|
|
4
|
+
import { AGGREGATE_SHARDS, MAX_BATCH_EVENT_GROUPS, DAY_MS, HOURLY_AGGREGATE_SHARDS, DIMENSION_SLOTS, HOUR_MS, LIVE_LEAVE_GRACE_MS, LIVE_SESSION_TTL_MS, LIVE_SWEEP_INTERVAL_MS, MAX_BATCH_EVENTS, MAX_BYTES_PER_CLIENT_WINDOW, MAX_EVENTS_PER_CLIENT_WINDOW, MAX_SITE_BYTES_PER_SHARD_WINDOW, MAX_SITE_EVENTS_PER_SHARD_WINDOW, MAX_BYTES_PER_SESSION_WINDOW, MAX_EVENTS_PER_SESSION_WINDOW, MAX_FUNNELS_PER_SITE, MAX_GOALS_PER_SITE, RATE_LIMIT_WINDOW_MS, REPORT_ROLLUP_SHARDS, SECONDARY_AGGREGATE_SHARDS, SITE_INGEST_SHARDS, } from "./constants.js";
|
|
5
5
|
import { addVisitor, createVisitorSketch, unionVisitorSketches, } from "./cardinality.js";
|
|
6
6
|
import { addToVitalHistogram, emptyVitalHistogram, vitalDevice, VITAL_ALL, } from "./vitals.js";
|
|
7
7
|
import { fail } from "./errors.js";
|
|
@@ -26,6 +26,8 @@ export const ingestBatch = mutation({
|
|
|
26
26
|
origin: v.string(),
|
|
27
27
|
events: v.array(trackerEventValidator),
|
|
28
28
|
context: v.optional(ingestContextValidator),
|
|
29
|
+
// The HTTP action's salted daily client-network key (see identity.ts).
|
|
30
|
+
clientKey: v.optional(v.string()),
|
|
29
31
|
},
|
|
30
32
|
returns: ingestResultValidator,
|
|
31
33
|
handler: async (ctx, args) => {
|
|
@@ -51,6 +53,9 @@ export const ingestBatch = mutation({
|
|
|
51
53
|
}
|
|
52
54
|
const now = Date.now();
|
|
53
55
|
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
56
|
+
if (args.clientKey !== undefined) {
|
|
57
|
+
await consumeClientRateLimit(ctx, args.siteId, args.clientKey, eventByteCounts, now);
|
|
58
|
+
}
|
|
54
59
|
await consumeSiteRateLimit(ctx, args.siteId, args.events, eventByteCounts, now);
|
|
55
60
|
const context = sanitizeContext(args.context);
|
|
56
61
|
const timezone = localDayTimezone(site.timezone);
|
|
@@ -155,19 +160,16 @@ export const ingestBatch = mutation({
|
|
|
155
160
|
rejected += 1;
|
|
156
161
|
continue;
|
|
157
162
|
}
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
foldSessionDuration(aggregateDeltas, args.siteId, event, durationDeltaMs, timezone);
|
|
163
|
+
// A heartbeat is presence, not telemetry: it moves the session's
|
|
164
|
+
// lastSeenAt and live row only. The time it proves reaches the
|
|
165
|
+
// duration counters once, when the visitor departs (live.ts).
|
|
166
|
+
await accountHeartbeat(ctx, args.siteId, existingSession, event, context, now, batchState);
|
|
163
167
|
accepted += 1;
|
|
164
168
|
continue;
|
|
165
169
|
}
|
|
166
170
|
if (event.type === "leave") {
|
|
167
|
-
// A departure
|
|
168
|
-
//
|
|
169
|
-
// or late leave is inert. The read side filters by expiresAt and the
|
|
170
|
-
// sweep deletes the row, so the dashboard sees the exit in seconds.
|
|
171
|
+
// A departure closes the final duration interval without telemetry
|
|
172
|
+
// or new presence. Older departures cannot retire a bfcache return.
|
|
171
173
|
if (!existingSession) {
|
|
172
174
|
rejected += 1;
|
|
173
175
|
continue;
|
|
@@ -178,7 +180,22 @@ export const ingestBatch = mutation({
|
|
|
178
180
|
}
|
|
179
181
|
const liveState = await loadLiveSession(ctx, args.siteId, event.sessionId, batchState);
|
|
180
182
|
const liveSession = liveState.current;
|
|
181
|
-
if (liveSession) {
|
|
183
|
+
if (liveSession && event.timestamp >= liveSession.lastSeenAt) {
|
|
184
|
+
// Only the still-covered tail is trusted; a delayed departure
|
|
185
|
+
// cannot lengthen an expired visit or move its start backwards.
|
|
186
|
+
if (event.timestamp > existingSession.lastSeenAt &&
|
|
187
|
+
event.timestamp >= now - LIVE_SESSION_TTL_MS &&
|
|
188
|
+
event.timestamp <= liveSession.expiresAt) {
|
|
189
|
+
const durationMs = Math.max(existingSession.durationMs, event.timestamp - existingSession.startedAt);
|
|
190
|
+
sessionRow.current = {
|
|
191
|
+
...existingSession,
|
|
192
|
+
lastSeenAt: event.timestamp,
|
|
193
|
+
exitPath: event.path,
|
|
194
|
+
durationMs,
|
|
195
|
+
};
|
|
196
|
+
sessionRow.dirty = true;
|
|
197
|
+
foldSessionDuration(aggregateDeltas, args.siteId, event.sessionId, event.timestamp, durationMs - existingSession.durationMs, timezone);
|
|
198
|
+
}
|
|
182
199
|
const expiresAt = Math.min(liveSession.expiresAt, now + LIVE_LEAVE_GRACE_MS);
|
|
183
200
|
if (expiresAt < liveSession.expiresAt) {
|
|
184
201
|
await ctx.db.patch("liveSessions", liveSession._id, { expiresAt });
|
|
@@ -855,6 +872,12 @@ async function attributionForEvent(ctx, siteId, event, existingSession, batchSta
|
|
|
855
872
|
affiliateExpiresAt: expiresAt,
|
|
856
873
|
};
|
|
857
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* Moves the session's lastSeenAt and its live row. `durationMs` stays: it is
|
|
877
|
+
* what the duration counters already hold, and the time a heartbeat proves
|
|
878
|
+
* is counted when the visitor departs (live.ts), so an idle tab never
|
|
879
|
+
* rewrites an aggregate bucket.
|
|
880
|
+
*/
|
|
858
881
|
async function accountHeartbeat(ctx, siteId, session, event, context, now, batchState) {
|
|
859
882
|
const startedAt = Math.min(session.startedAt, event.timestamp);
|
|
860
883
|
const lastSeenAt = Math.max(session.lastSeenAt, event.timestamp);
|
|
@@ -862,7 +885,6 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
862
885
|
startedAt,
|
|
863
886
|
lastSeenAt,
|
|
864
887
|
exitPath: event.timestamp >= session.lastSeenAt ? event.path : session.exitPath,
|
|
865
|
-
durationMs: Math.max(0, lastSeenAt - startedAt),
|
|
866
888
|
...(context.country ? { country: context.country } : {}),
|
|
867
889
|
...(context.city ? { city: context.city } : {}),
|
|
868
890
|
...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
|
|
@@ -883,7 +905,6 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
883
905
|
else {
|
|
884
906
|
await ctx.db.patch("sessions", session._id, updates);
|
|
885
907
|
}
|
|
886
|
-
const durationDeltaMs = Math.max(0, updates.durationMs - session.durationMs);
|
|
887
908
|
await updateLiveSession(ctx, siteId, event, context, {
|
|
888
909
|
isNewSession: false,
|
|
889
910
|
isNewVisitorHour: false,
|
|
@@ -905,7 +926,42 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
905
926
|
conversionCount: session.conversionCount,
|
|
906
927
|
newVisitor: session.newVisitor,
|
|
907
928
|
}, now, batchState);
|
|
908
|
-
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* One client network's budget per site and minute, kept in the session
|
|
932
|
+
* windows' table under `~client:<key>`, a key no sanitized session ID can take.
|
|
933
|
+
* The key is the salted daily hash the HTTP action derived; the address never
|
|
934
|
+
* reaches this mutation. It is one row per client, so only that client's own
|
|
935
|
+
* concurrent requests contend on it.
|
|
936
|
+
*/
|
|
937
|
+
async function consumeClientRateLimit(ctx, siteId, clientKey, eventByteCounts, now) {
|
|
938
|
+
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
939
|
+
const sessionId = `~client:${clientKey}`;
|
|
940
|
+
const rows = await ctx.db
|
|
941
|
+
.query("ingestWindows")
|
|
942
|
+
.withIndex("by_siteId_and_sessionId_and_windowStart", (range) => range.eq("siteId", siteId).eq("sessionId", sessionId))
|
|
943
|
+
.order("desc")
|
|
944
|
+
.take(1);
|
|
945
|
+
const window = rows[0] ?? null;
|
|
946
|
+
const current = window?.windowStart === windowStart ? window : null;
|
|
947
|
+
const eventCount = (current?.eventCount ?? 0) + eventByteCounts.length;
|
|
948
|
+
const byteCount = (current?.byteCount ?? 0) +
|
|
949
|
+
eventByteCounts.reduce((total, bytes) => total + bytes, 0);
|
|
950
|
+
if (eventCount > MAX_EVENTS_PER_CLIENT_WINDOW ||
|
|
951
|
+
byteCount > MAX_BYTES_PER_CLIENT_WINDOW) {
|
|
952
|
+
fail("RATE_LIMITED", "client ingest budget exceeded", {
|
|
953
|
+
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
954
|
+
eventLimitPerClient: MAX_EVENTS_PER_CLIENT_WINDOW,
|
|
955
|
+
byteLimitPerClient: MAX_BYTES_PER_CLIENT_WINDOW,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
const fields = { windowStart, eventCount, byteCount, updatedAt: now };
|
|
959
|
+
if (window) {
|
|
960
|
+
await ctx.db.patch("ingestWindows", window._id, fields);
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
await ctx.db.insert("ingestWindows", { siteId, sessionId, ...fields });
|
|
964
|
+
}
|
|
909
965
|
}
|
|
910
966
|
async function consumeSiteRateLimit(ctx, siteId, events, eventByteCounts, now) {
|
|
911
967
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
@@ -1214,9 +1270,11 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1214
1270
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1215
1271
|
return { funnelId: funnel._id, step: 1, steps: funnel.steps.length };
|
|
1216
1272
|
}
|
|
1217
|
-
if (
|
|
1218
|
-
event.timestamp < progress.lastStepAt)
|
|
1273
|
+
if (event.timestamp < progress.lastStepAt)
|
|
1219
1274
|
return;
|
|
1275
|
+
// An expired window restarts on a first-step event even after a completion,
|
|
1276
|
+
// so a returning buyer can complete again; inside the window a completed
|
|
1277
|
+
// funnel stays done.
|
|
1220
1278
|
if (event.timestamp - progress.startedAt > funnel.conversionWindowMs) {
|
|
1221
1279
|
if (!matchesFirst)
|
|
1222
1280
|
return;
|
|
@@ -1245,6 +1303,8 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1245
1303
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1246
1304
|
return { funnelId: funnel._id, step: 1, steps: funnel.steps.length };
|
|
1247
1305
|
}
|
|
1306
|
+
if (progress.completedAt !== undefined)
|
|
1307
|
+
return;
|
|
1248
1308
|
const nextStep = funnel.steps[progress.currentStep];
|
|
1249
1309
|
if (!nextStep || !stepMatches(nextStep, event))
|
|
1250
1310
|
return;
|
|
@@ -1595,14 +1655,19 @@ function aggregateBucketStart(granularity, timestamp, timezone) {
|
|
|
1595
1655
|
const interval = granularity === "hour" ? HOUR_MS : DAY_MS;
|
|
1596
1656
|
return Math.floor(timestamp / interval) * interval;
|
|
1597
1657
|
}
|
|
1598
|
-
/**
|
|
1599
|
-
|
|
1658
|
+
/**
|
|
1659
|
+
* Folds session duration growth without telemetry: a departure's final
|
|
1660
|
+
* interval, or the heartbeat time of a visitor the live sweep retired. The
|
|
1661
|
+
* shard follows `shardKey`: the session for a departure, the site for a
|
|
1662
|
+
* sweep.
|
|
1663
|
+
*/
|
|
1664
|
+
export function foldSessionDuration(aggregateDeltas, siteId, shardKey, timestamp, durationDeltaMs, timezone) {
|
|
1600
1665
|
if (durationDeltaMs <= 0)
|
|
1601
1666
|
return;
|
|
1602
1667
|
for (const granularity of aggregateGranularities(timezone)) {
|
|
1603
|
-
const shard = stableHash(
|
|
1668
|
+
const shard = stableHash(shardKey) %
|
|
1604
1669
|
(granularity === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
1605
|
-
const bucketStart = aggregateBucketStart(granularity,
|
|
1670
|
+
const bucketStart = aggregateBucketStart(granularity, timestamp, timezone);
|
|
1606
1671
|
const key = `${siteId}:${granularity}:${bucketStart}:${shard}`;
|
|
1607
1672
|
const delta = aggregateDeltas.get(key) ?? {
|
|
1608
1673
|
siteId,
|
|
@@ -1628,7 +1693,7 @@ function foldSessionDuration(aggregateDeltas, siteId, event, durationDeltaMs, ti
|
|
|
1628
1693
|
aggregateDeltas.set(key, delta);
|
|
1629
1694
|
}
|
|
1630
1695
|
}
|
|
1631
|
-
async function flushAggregates(ctx, aggregateDeltas) {
|
|
1696
|
+
export async function flushAggregates(ctx, aggregateDeltas) {
|
|
1632
1697
|
for (const delta of aggregateDeltas.values()) {
|
|
1633
1698
|
const bucket = await ctx.db
|
|
1634
1699
|
.query("aggregateBuckets")
|