@iann29/rastro 0.1.0-alpha.1 → 0.1.0-alpha.3
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 +80 -18
- package/dist/component/_generated/api.d.ts +4 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +10 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +4 -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 +2 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +2 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +6 -1
- package/dist/component/convex.config.js +9 -1
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/diagnostics.d.ts +9 -0
- package/dist/component/diagnostics.d.ts.map +1 -0
- package/dist/component/diagnostics.js +47 -0
- package/dist/component/diagnostics.js.map +1 -0
- package/dist/component/eventStore.d.ts +18 -8
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/eventStore.js +134 -147
- package/dist/component/eventStore.js.map +1 -1
- package/dist/component/geo.d.ts +71 -0
- package/dist/component/geo.d.ts.map +1 -0
- package/dist/component/geo.js +610 -0
- package/dist/component/geo.js.map +1 -0
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +124 -11
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +34 -14
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/reports.d.ts +8 -8
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +10 -30
- package/dist/component/reports.js.map +1 -1
- package/dist/component/schema.d.ts +47 -52
- package/dist/component/schema.js +21 -20
- package/dist/component/schema.js.map +1 -1
- package/dist/component/validators.d.ts +49 -49
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +4 -11
- 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.js +7 -6
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/benchmarks/2026-08-20-realistic.md +1 -1
- package/package.json +1 -1
- package/scripts/benchmark-ingest.mjs +81 -32
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +13 -0
- package/src/component/_generated/server.ts +4 -0
- package/src/component/constants.ts +2 -0
- package/src/component/convex.config.ts +11 -1
- package/src/component/diagnostics.ts +64 -0
- package/src/component/eventStore.ts +195 -178
- package/src/component/geo.ts +779 -0
- package/src/component/http.ts +184 -17
- package/src/component/ingest.ts +49 -15
- package/src/component/reports.ts +12 -38
- package/src/component/schema.ts +23 -21
- package/src/component/validators.ts +7 -12
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
import { defineComponent } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
2
3
|
|
|
3
|
-
export default defineComponent("rastroAnalytics"
|
|
4
|
+
export default defineComponent("rastroAnalytics", {
|
|
5
|
+
env: {
|
|
6
|
+
RASTRO_GEOIP_PROVIDER: v.optional(
|
|
7
|
+
v.union(v.literal("ipinfo"), v.literal("ipwhois")),
|
|
8
|
+
),
|
|
9
|
+
RASTRO_GEOIP_TOKEN: v.optional(v.string()),
|
|
10
|
+
RASTRO_GEOIP_DAILY_LIMIT: v.optional(v.string()),
|
|
11
|
+
RASTRO_TRUST_PROXY: v.optional(v.literal("true")),
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { query } from "./_generated/server.js";
|
|
3
|
+
import { fail } from "./errors.js";
|
|
4
|
+
import { sanitizeOpaqueId } from "./sanitize.js";
|
|
5
|
+
|
|
6
|
+
const MAX_SESSION_TOTAL_IDS = 100;
|
|
7
|
+
|
|
8
|
+
export const sessionEventTotals = query({
|
|
9
|
+
args: {
|
|
10
|
+
siteId: v.id("sites"),
|
|
11
|
+
sessionIds: v.array(v.string()),
|
|
12
|
+
},
|
|
13
|
+
returns: v.object({
|
|
14
|
+
requestedSessions: v.number(),
|
|
15
|
+
foundSessions: v.number(),
|
|
16
|
+
storedEvents: v.number(),
|
|
17
|
+
}),
|
|
18
|
+
handler: async (ctx, args) => {
|
|
19
|
+
if (
|
|
20
|
+
args.sessionIds.length < 1 ||
|
|
21
|
+
args.sessionIds.length > MAX_SESSION_TOTAL_IDS
|
|
22
|
+
) {
|
|
23
|
+
fail(
|
|
24
|
+
"INVALID_ARGUMENT",
|
|
25
|
+
`sessionIds must contain between 1 and ${MAX_SESSION_TOTAL_IDS} IDs`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
let sessionIds: string[];
|
|
29
|
+
try {
|
|
30
|
+
sessionIds = args.sessionIds.map((sessionId) => {
|
|
31
|
+
const sanitized = sanitizeOpaqueId(sessionId, "sessionId");
|
|
32
|
+
if (sanitized !== sessionId) throw new Error("sessionId is not canonical");
|
|
33
|
+
return sanitized;
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
fail(
|
|
37
|
+
"INVALID_ARGUMENT",
|
|
38
|
+
error instanceof Error ? error.message : "invalid sessionId",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (new Set(sessionIds).size !== sessionIds.length) {
|
|
42
|
+
fail("INVALID_ARGUMENT", "sessionIds must be unique");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sessions = await Promise.all(
|
|
46
|
+
sessionIds.map((sessionId) =>
|
|
47
|
+
ctx.db
|
|
48
|
+
.query("sessions")
|
|
49
|
+
.withIndex("by_siteId_and_sessionId", (range) =>
|
|
50
|
+
range.eq("siteId", args.siteId).eq("sessionId", sessionId),
|
|
51
|
+
)
|
|
52
|
+
.unique()
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
return {
|
|
56
|
+
requestedSessions: sessionIds.length,
|
|
57
|
+
foundSessions: sessions.filter((session) => session !== null).length,
|
|
58
|
+
storedEvents: sessions.reduce(
|
|
59
|
+
(total, session) => total + (session?.eventCount ?? 0),
|
|
60
|
+
0,
|
|
61
|
+
),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Doc, Id } from "./_generated/dataModel.js";
|
|
2
2
|
import type { MutationCtx, QueryCtx } from "./_generated/server.js";
|
|
3
3
|
import {
|
|
4
|
+
JOURNEY_READ_BYTE_RESERVE,
|
|
5
|
+
JOURNEY_READ_DOCUMENT_RESERVE,
|
|
4
6
|
MAX_EVENTS_PER_SESSION_WINDOW,
|
|
5
7
|
RATE_LIMIT_WINDOW_MS,
|
|
6
8
|
} from "./constants.js";
|
|
9
|
+
import { fail } from "./errors.js";
|
|
7
10
|
import type { TrackerEvent } from "./validators.js";
|
|
8
11
|
|
|
9
12
|
export type TelemetryMatch = {
|
|
@@ -34,7 +37,16 @@ export type JourneyCursor = {
|
|
|
34
37
|
id: string;
|
|
35
38
|
};
|
|
36
39
|
|
|
37
|
-
export type RawJourneyEvent = Omit<
|
|
40
|
+
export type RawJourneyEvent = Omit<
|
|
41
|
+
Doc<"eventBatches">["events"][number],
|
|
42
|
+
"aggregateCountry"
|
|
43
|
+
> & {
|
|
44
|
+
_id: string;
|
|
45
|
+
_creationTime: number;
|
|
46
|
+
siteId: Id<"sites">;
|
|
47
|
+
sessionId: string;
|
|
48
|
+
visitorId: string;
|
|
49
|
+
};
|
|
38
50
|
|
|
39
51
|
export function eventBucketStart(timestamp: number): number {
|
|
40
52
|
return Math.floor(timestamp / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
@@ -50,32 +62,15 @@ export async function inspectTelemetryMinute(
|
|
|
50
62
|
siteId: Id<"sites">;
|
|
51
63
|
sessionId: string;
|
|
52
64
|
bucketStart: number;
|
|
53
|
-
candidateEventIds: Set<string>;
|
|
54
65
|
},
|
|
55
66
|
) {
|
|
56
|
-
const
|
|
57
|
-
const eventIds = new Set(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
batch.events.map((event) => event.eventId),
|
|
61
|
-
),
|
|
62
|
-
]);
|
|
63
|
-
if (legacyEvents.length > MAX_EVENTS_PER_SESSION_WINDOW) {
|
|
64
|
-
await Promise.all(
|
|
65
|
-
[...input.candidateEventIds]
|
|
66
|
-
.filter((eventId) => !eventIds.has(eventId))
|
|
67
|
-
.map(async (eventId) => {
|
|
68
|
-
const legacy = await findLegacyTelemetry(ctx, {
|
|
69
|
-
...input,
|
|
70
|
-
eventId,
|
|
71
|
-
});
|
|
72
|
-
if (legacy) eventIds.add(eventId);
|
|
73
|
-
}),
|
|
74
|
-
);
|
|
75
|
-
}
|
|
67
|
+
const batches = await loadTelemetryMinute(ctx, input);
|
|
68
|
+
const eventIds = new Set(
|
|
69
|
+
batches.flatMap((batch) => batch.events.map((event) => event.eventId)),
|
|
70
|
+
);
|
|
76
71
|
return {
|
|
77
72
|
eventIds,
|
|
78
|
-
persistedCount: telemetryMinuteCount(
|
|
73
|
+
persistedCount: telemetryMinuteCount(batches),
|
|
79
74
|
};
|
|
80
75
|
}
|
|
81
76
|
|
|
@@ -85,34 +80,31 @@ export async function findTelemetry(
|
|
|
85
80
|
event: TrackerEvent,
|
|
86
81
|
): Promise<{ match: TelemetryMatch | null; saturated: boolean }> {
|
|
87
82
|
const bucketStart = eventBucketStart(event.timestamp);
|
|
88
|
-
const
|
|
83
|
+
const batches = await loadTelemetryMinute(ctx, {
|
|
89
84
|
siteId,
|
|
90
85
|
sessionId: event.sessionId,
|
|
91
86
|
bucketStart,
|
|
92
87
|
});
|
|
93
|
-
const persistedCount = telemetryMinuteCount(
|
|
94
|
-
let legacyEvent = legacyEvents.find((candidate) =>
|
|
95
|
-
candidate.eventId === event.eventId
|
|
96
|
-
);
|
|
97
|
-
if (
|
|
98
|
-
!legacyEvent &&
|
|
99
|
-
legacyEvents.length > MAX_EVENTS_PER_SESSION_WINDOW
|
|
100
|
-
) {
|
|
101
|
-
legacyEvent = await findLegacyTelemetry(ctx, {
|
|
102
|
-
siteId,
|
|
103
|
-
sessionId: event.sessionId,
|
|
104
|
-
bucketStart,
|
|
105
|
-
eventId: event.eventId,
|
|
106
|
-
}) ?? undefined;
|
|
107
|
-
}
|
|
88
|
+
const persistedCount = telemetryMinuteCount(batches);
|
|
108
89
|
const saturated = persistedCount >= MAX_EVENTS_PER_SESSION_WINDOW;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
90
|
+
let batch: Doc<"eventBatches"> | undefined;
|
|
91
|
+
let nestedEvent: Doc<"eventBatches">["events"][number] | undefined;
|
|
92
|
+
for (const candidate of batches) {
|
|
93
|
+
nestedEvent = candidate.events.find((nested) =>
|
|
94
|
+
nested.eventId === event.eventId
|
|
95
|
+
);
|
|
96
|
+
if (nestedEvent) {
|
|
97
|
+
batch = candidate;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
113
101
|
return {
|
|
114
|
-
match: batch
|
|
115
|
-
? {
|
|
102
|
+
match: batch && nestedEvent
|
|
103
|
+
? {
|
|
104
|
+
sessionId: batch.sessionId,
|
|
105
|
+
visitorId: batch.visitorId,
|
|
106
|
+
...eventContext(nestedEvent),
|
|
107
|
+
}
|
|
116
108
|
: null,
|
|
117
109
|
saturated,
|
|
118
110
|
};
|
|
@@ -122,6 +114,7 @@ export function bufferEventBatch(
|
|
|
122
114
|
siteId: Id<"sites">,
|
|
123
115
|
event: TrackerEvent,
|
|
124
116
|
buffer: EventBatchBuffer,
|
|
117
|
+
country: string,
|
|
125
118
|
) {
|
|
126
119
|
const bucketStart = eventBucketStart(event.timestamp);
|
|
127
120
|
const key = eventBatchKey(event.sessionId, bucketStart);
|
|
@@ -130,7 +123,7 @@ export function bufferEventBatch(
|
|
|
130
123
|
if (existing.visitorId !== event.visitorId) {
|
|
131
124
|
throw new Error("event batch visitor does not match its session");
|
|
132
125
|
}
|
|
133
|
-
existing.events.push(eventForBatch(event));
|
|
126
|
+
existing.events.push(eventForBatch(event, country));
|
|
134
127
|
return;
|
|
135
128
|
}
|
|
136
129
|
buffer.set(key, {
|
|
@@ -138,7 +131,7 @@ export function bufferEventBatch(
|
|
|
138
131
|
sessionId: event.sessionId,
|
|
139
132
|
visitorId: event.visitorId,
|
|
140
133
|
bucketStart,
|
|
141
|
-
events: [eventForBatch(event)],
|
|
134
|
+
events: [eventForBatch(event, country)],
|
|
142
135
|
});
|
|
143
136
|
}
|
|
144
137
|
|
|
@@ -146,13 +139,14 @@ export async function insertEventBatch(
|
|
|
146
139
|
ctx: MutationCtx,
|
|
147
140
|
siteId: Id<"sites">,
|
|
148
141
|
event: TrackerEvent,
|
|
142
|
+
country: string,
|
|
149
143
|
) {
|
|
150
144
|
await ctx.db.insert("eventBatches", {
|
|
151
145
|
siteId,
|
|
152
146
|
sessionId: event.sessionId,
|
|
153
147
|
visitorId: event.visitorId,
|
|
154
148
|
bucketStart: eventBucketStart(event.timestamp),
|
|
155
|
-
events: [eventForBatch(event)],
|
|
149
|
+
events: [eventForBatch(event, country)],
|
|
156
150
|
});
|
|
157
151
|
}
|
|
158
152
|
|
|
@@ -183,8 +177,7 @@ export async function loadSessionJourneyEvents(
|
|
|
183
177
|
// be read completely before applying the cursor.
|
|
184
178
|
const loadBatches = async () => {
|
|
185
179
|
const batches: Doc<"eventBatches">[] = [];
|
|
186
|
-
let
|
|
187
|
-
const requiredEvents = input.resultLimit + MAX_EVENTS_PER_SESSION_WINDOW;
|
|
180
|
+
let qualifyingEvents = 0;
|
|
188
181
|
let completedBucket: number | undefined;
|
|
189
182
|
const query = ctx.db
|
|
190
183
|
.query("eventBatches")
|
|
@@ -201,78 +194,81 @@ export async function loadSessionJourneyEvents(
|
|
|
201
194
|
break;
|
|
202
195
|
}
|
|
203
196
|
batches.push(batch);
|
|
204
|
-
|
|
205
|
-
|
|
197
|
+
qualifyingEvents += batch.events.reduce((count, _event, index) => {
|
|
198
|
+
const event = eventFromBatch(batch, index);
|
|
199
|
+
return count + Number(
|
|
200
|
+
event.timestamp >= input.from &&
|
|
201
|
+
event.timestamp <= input.to &&
|
|
202
|
+
(!input.cursor ||
|
|
203
|
+
compareJourneyEventToCursor(event, input.cursor) > 0),
|
|
204
|
+
);
|
|
205
|
+
}, 0);
|
|
206
|
+
if (qualifyingEvents >= input.resultLimit) {
|
|
207
|
+
completedBucket = batch.bucketStart;
|
|
208
|
+
}
|
|
206
209
|
}
|
|
207
210
|
return batches;
|
|
208
211
|
};
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
212
|
+
const batches = await loadBatches();
|
|
213
|
+
const rows: RawJourneyEvent[] = [];
|
|
214
|
+
appendBatchEvents(rows, batches);
|
|
215
|
+
return rows
|
|
216
|
+
.filter((event) =>
|
|
217
|
+
event.timestamp >= input.from &&
|
|
218
|
+
event.timestamp <= input.to &&
|
|
219
|
+
(!input.cursor || compareJourneyEventToCursor(event, input.cursor) > 0)
|
|
220
|
+
)
|
|
221
|
+
.sort(compareJourneyEvents)
|
|
222
|
+
.slice(0, input.resultLimit);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function loadVisitorJourneyEvents(
|
|
226
|
+
ctx: QueryCtx,
|
|
227
|
+
input: {
|
|
228
|
+
siteId: Id<"sites">;
|
|
229
|
+
visitorId: string;
|
|
230
|
+
from: number;
|
|
231
|
+
to: number;
|
|
232
|
+
resultLimit: number;
|
|
233
|
+
},
|
|
234
|
+
): Promise<RawJourneyEvent[]> {
|
|
235
|
+
const firstBucket = eventBucketStart(input.from);
|
|
236
|
+
const lastBucket = eventBucketStart(input.to);
|
|
237
|
+
const loadBatches = async () => {
|
|
238
|
+
const batches: Doc<"eventBatches">[] = [];
|
|
239
|
+
let qualifyingEvents = 0;
|
|
240
|
+
let completedBucket: number | undefined;
|
|
241
|
+
const query = ctx.db
|
|
242
|
+
.query("eventBatches")
|
|
243
|
+
.withIndex("by_siteId_and_visitorId_and_bucketStart", (range) =>
|
|
227
244
|
range
|
|
228
245
|
.eq("siteId", input.siteId)
|
|
229
|
-
.eq("
|
|
230
|
-
.
|
|
231
|
-
.
|
|
246
|
+
.eq("visitorId", input.visitorId)
|
|
247
|
+
.gte("bucketStart", firstBucket)
|
|
248
|
+
.lte("bucketStart", lastBucket),
|
|
232
249
|
)
|
|
233
|
-
.order("asc")
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
.take(take - rows.length));
|
|
250
|
+
.order("asc");
|
|
251
|
+
for await (const batch of query) {
|
|
252
|
+
if (completedBucket !== undefined && batch.bucketStart !== completedBucket) {
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
batches.push(batch);
|
|
256
|
+
await requireJourneyReadHeadroom(ctx);
|
|
257
|
+
qualifyingEvents += batch.events.filter((event) =>
|
|
258
|
+
event.timestamp >= input.from && event.timestamp <= input.to
|
|
259
|
+
).length;
|
|
260
|
+
if (qualifyingEvents >= input.resultLimit) {
|
|
261
|
+
completedBucket = batch.bucketStart;
|
|
262
|
+
}
|
|
247
263
|
}
|
|
248
|
-
return
|
|
264
|
+
return batches;
|
|
249
265
|
};
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
]);
|
|
254
|
-
const rows: RawJourneyEvent[] = legacyEvents.map((event) => ({
|
|
255
|
-
...event,
|
|
256
|
-
_id: String(event._id),
|
|
257
|
-
}));
|
|
258
|
-
for (const batch of batches) {
|
|
259
|
-
for (let index = 0; index < batch.events.length; index += 1) {
|
|
260
|
-
const event = batch.events[index];
|
|
261
|
-
rows.push({
|
|
262
|
-
...event,
|
|
263
|
-
_id: `eventBatch:${batch._id}:${index.toString().padStart(3, "0")}`,
|
|
264
|
-
_creationTime: batch._creationTime,
|
|
265
|
-
siteId: batch.siteId,
|
|
266
|
-
sessionId: batch.sessionId,
|
|
267
|
-
visitorId: batch.visitorId,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
266
|
+
const batches = await loadBatches();
|
|
267
|
+
const rows: RawJourneyEvent[] = [];
|
|
268
|
+
appendBatchEvents(rows, batches);
|
|
271
269
|
return rows
|
|
272
270
|
.filter((event) =>
|
|
273
|
-
event.timestamp >= input.from &&
|
|
274
|
-
event.timestamp <= input.to &&
|
|
275
|
-
(!input.cursor || compareJourneyEventToCursor(event, input.cursor) > 0)
|
|
271
|
+
event.timestamp >= input.from && event.timestamp <= input.to
|
|
276
272
|
)
|
|
277
273
|
.sort(compareJourneyEvents)
|
|
278
274
|
.slice(0, input.resultLimit);
|
|
@@ -342,32 +338,19 @@ export async function deleteTelemetryBefore(
|
|
|
342
338
|
) {
|
|
343
339
|
const batchCutoff = eventBucketStart(input.before);
|
|
344
340
|
const limit = input.batchSize + 1;
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
ctx.db
|
|
353
|
-
.query("eventBatches")
|
|
354
|
-
.withIndex("by_siteId_and_bucketStart", (range) =>
|
|
355
|
-
range.eq("siteId", input.siteId).lt("bucketStart", batchCutoff),
|
|
356
|
-
)
|
|
357
|
-
.take(limit),
|
|
358
|
-
]);
|
|
359
|
-
const rows = [...legacyEvents, ...batches].slice(0, input.batchSize);
|
|
341
|
+
const batches = await ctx.db
|
|
342
|
+
.query("eventBatches")
|
|
343
|
+
.withIndex("by_siteId_and_bucketStart", (range) =>
|
|
344
|
+
range.eq("siteId", input.siteId).lt("bucketStart", batchCutoff),
|
|
345
|
+
)
|
|
346
|
+
.take(limit);
|
|
347
|
+
const rows = batches.slice(0, input.batchSize);
|
|
360
348
|
for (const row of rows) {
|
|
361
|
-
|
|
362
|
-
await ctx.db.delete("eventBatches", row._id);
|
|
363
|
-
} else {
|
|
364
|
-
await ctx.db.delete("events", row._id);
|
|
365
|
-
}
|
|
349
|
+
await ctx.db.delete("eventBatches", row._id);
|
|
366
350
|
}
|
|
367
351
|
return {
|
|
368
352
|
deleted: rows.length,
|
|
369
|
-
hasMore:
|
|
370
|
-
legacyEvents.length + batches.length > input.batchSize,
|
|
353
|
+
hasMore: batches.length > input.batchSize,
|
|
371
354
|
};
|
|
372
355
|
}
|
|
373
356
|
|
|
@@ -379,61 +362,30 @@ async function loadTelemetryMinute(
|
|
|
379
362
|
bucketStart: number;
|
|
380
363
|
},
|
|
381
364
|
) {
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
.gte("timestamp", input.bucketStart)
|
|
390
|
-
.lt("timestamp", input.bucketStart + RATE_LIMIT_WINDOW_MS),
|
|
391
|
-
)
|
|
392
|
-
.take(MAX_EVENTS_PER_SESSION_WINDOW + 1),
|
|
393
|
-
ctx.db
|
|
394
|
-
.query("eventBatches")
|
|
395
|
-
.withIndex("by_siteId_and_sessionId_and_bucketStart", (range) =>
|
|
396
|
-
range
|
|
397
|
-
.eq("siteId", input.siteId)
|
|
398
|
-
.eq("sessionId", input.sessionId)
|
|
399
|
-
.eq("bucketStart", input.bucketStart),
|
|
400
|
-
)
|
|
401
|
-
.take(MAX_EVENTS_PER_SESSION_WINDOW + 1),
|
|
402
|
-
]);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
async function findLegacyTelemetry(
|
|
406
|
-
ctx: MutationCtx,
|
|
407
|
-
input: {
|
|
408
|
-
siteId: Id<"sites">;
|
|
409
|
-
sessionId: string;
|
|
410
|
-
bucketStart: number;
|
|
411
|
-
eventId: string;
|
|
412
|
-
},
|
|
413
|
-
) {
|
|
414
|
-
const candidates = await ctx.db
|
|
415
|
-
.query("events")
|
|
416
|
-
.withIndex("by_siteId_and_eventId", (range) =>
|
|
417
|
-
range.eq("siteId", input.siteId).eq("eventId", input.eventId),
|
|
365
|
+
return ctx.db
|
|
366
|
+
.query("eventBatches")
|
|
367
|
+
.withIndex("by_siteId_and_sessionId_and_bucketStart", (range) =>
|
|
368
|
+
range
|
|
369
|
+
.eq("siteId", input.siteId)
|
|
370
|
+
.eq("sessionId", input.sessionId)
|
|
371
|
+
.eq("bucketStart", input.bucketStart),
|
|
418
372
|
)
|
|
419
|
-
.take(
|
|
420
|
-
return candidates.find((candidate) =>
|
|
421
|
-
candidate.sessionId === input.sessionId &&
|
|
422
|
-
eventBucketStart(candidate.timestamp) === input.bucketStart
|
|
423
|
-
) ?? null;
|
|
373
|
+
.take(MAX_EVENTS_PER_SESSION_WINDOW + 1);
|
|
424
374
|
}
|
|
425
375
|
|
|
426
376
|
function telemetryMinuteCount(
|
|
427
|
-
legacyEvents: Array<{ eventId: string }>,
|
|
428
377
|
batches: Array<{ events: TrackerEvent[] }>,
|
|
429
378
|
) {
|
|
430
|
-
return
|
|
379
|
+
return batches.reduce(
|
|
431
380
|
(total, batch) => total + batch.events.length,
|
|
432
381
|
0,
|
|
433
382
|
);
|
|
434
383
|
}
|
|
435
384
|
|
|
436
|
-
function eventForBatch(
|
|
385
|
+
function eventForBatch(
|
|
386
|
+
event: TrackerEvent,
|
|
387
|
+
country: string,
|
|
388
|
+
): TrackerEvent & { aggregateCountry?: string } {
|
|
437
389
|
return {
|
|
438
390
|
eventId: event.eventId,
|
|
439
391
|
sessionId: event.sessionId,
|
|
@@ -452,9 +404,74 @@ function eventForBatch(event: TrackerEvent): TrackerEvent {
|
|
|
452
404
|
: {}),
|
|
453
405
|
...(event.currency ? { currency: event.currency } : {}),
|
|
454
406
|
...(event.affiliateSlug ? { affiliateSlug: event.affiliateSlug } : {}),
|
|
407
|
+
aggregateCountry: country,
|
|
455
408
|
};
|
|
456
409
|
}
|
|
457
410
|
|
|
411
|
+
function appendBatchEvents(
|
|
412
|
+
rows: RawJourneyEvent[],
|
|
413
|
+
batches: Doc<"eventBatches">[],
|
|
414
|
+
) {
|
|
415
|
+
for (const batch of batches) {
|
|
416
|
+
for (let index = 0; index < batch.events.length; index += 1) {
|
|
417
|
+
rows.push(eventFromBatch(batch, index));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function eventFromBatch(
|
|
423
|
+
batch: Doc<"eventBatches">,
|
|
424
|
+
index: number,
|
|
425
|
+
): RawJourneyEvent {
|
|
426
|
+
const { aggregateCountry: _aggregateCountry, ...event } = batch.events[index]!;
|
|
427
|
+
void _aggregateCountry;
|
|
428
|
+
return {
|
|
429
|
+
...event,
|
|
430
|
+
_id: `eventBatch:${batch._id}:${index.toString().padStart(3, "0")}`,
|
|
431
|
+
_creationTime: batch._creationTime,
|
|
432
|
+
siteId: batch.siteId,
|
|
433
|
+
sessionId: batch.sessionId,
|
|
434
|
+
visitorId: batch.visitorId,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function eventContext(event: {
|
|
439
|
+
country?: string;
|
|
440
|
+
city?: string;
|
|
441
|
+
latitude?: number;
|
|
442
|
+
longitude?: number;
|
|
443
|
+
browser?: string;
|
|
444
|
+
os?: string;
|
|
445
|
+
device?: string;
|
|
446
|
+
}) {
|
|
447
|
+
return {
|
|
448
|
+
...(event.country ? { country: event.country } : {}),
|
|
449
|
+
...(event.city ? { city: event.city } : {}),
|
|
450
|
+
...(event.latitude !== undefined ? { latitude: event.latitude } : {}),
|
|
451
|
+
...(event.longitude !== undefined ? { longitude: event.longitude } : {}),
|
|
452
|
+
...(event.browser ? { browser: event.browser } : {}),
|
|
453
|
+
...(event.os ? { os: event.os } : {}),
|
|
454
|
+
...(event.device ? { device: event.device } : {}),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function requireJourneyReadHeadroom(ctx: QueryCtx) {
|
|
459
|
+
const metrics = await ctx.meta.getTransactionMetrics();
|
|
460
|
+
if (
|
|
461
|
+
metrics.bytesRead.remaining < JOURNEY_READ_BYTE_RESERVE ||
|
|
462
|
+
metrics.documentsRead.remaining < JOURNEY_READ_DOCUMENT_RESERVE
|
|
463
|
+
) {
|
|
464
|
+
fail(
|
|
465
|
+
"LIMIT_EXCEEDED",
|
|
466
|
+
"visitor journey exceeds the safe read budget",
|
|
467
|
+
{
|
|
468
|
+
bytesRead: metrics.bytesRead.used,
|
|
469
|
+
documentsRead: metrics.documentsRead.used,
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
458
475
|
function compareJourneyEventToCursor(
|
|
459
476
|
event: Pick<RawJourneyEvent, "timestamp" | "_creationTime" | "_id">,
|
|
460
477
|
cursor: JourneyCursor,
|