@iann29/rastro 0.1.0-alpha.2 → 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.
Files changed (70) hide show
  1. package/README.md +79 -30
  2. package/dist/component/_generated/api.d.ts +5 -5
  3. package/dist/component/_generated/api.d.ts.map +1 -1
  4. package/dist/component/_generated/api.js.map +1 -1
  5. package/dist/component/_generated/component.d.ts +10 -0
  6. package/dist/component/_generated/component.d.ts.map +1 -1
  7. package/dist/component/_generated/server.d.ts +4 -0
  8. package/dist/component/_generated/server.d.ts.map +1 -1
  9. package/dist/component/_generated/server.js.map +1 -1
  10. package/dist/component/convex.config.d.ts +7 -2
  11. package/dist/component/convex.config.d.ts.map +1 -1
  12. package/dist/component/convex.config.js +9 -4
  13. package/dist/component/convex.config.js.map +1 -1
  14. package/dist/component/diagnostics.d.ts +9 -0
  15. package/dist/component/diagnostics.d.ts.map +1 -0
  16. package/dist/component/diagnostics.js +47 -0
  17. package/dist/component/diagnostics.js.map +1 -0
  18. package/dist/component/eventStore.d.ts +11 -9
  19. package/dist/component/eventStore.d.ts.map +1 -1
  20. package/dist/component/eventStore.js +55 -227
  21. package/dist/component/eventStore.js.map +1 -1
  22. package/dist/component/geo.d.ts +71 -0
  23. package/dist/component/geo.d.ts.map +1 -0
  24. package/dist/component/geo.js +610 -0
  25. package/dist/component/geo.js.map +1 -0
  26. package/dist/component/http.d.ts.map +1 -1
  27. package/dist/component/http.js +124 -11
  28. package/dist/component/http.js.map +1 -1
  29. package/dist/component/ingest.d.ts.map +1 -1
  30. package/dist/component/ingest.js +34 -14
  31. package/dist/component/ingest.js.map +1 -1
  32. package/dist/component/reports.d.ts +8 -8
  33. package/dist/component/reports.d.ts.map +1 -1
  34. package/dist/component/reports.js +2 -1
  35. package/dist/component/reports.js.map +1 -1
  36. package/dist/component/schema.d.ts +15 -52
  37. package/dist/component/schema.js +14 -16
  38. package/dist/component/schema.js.map +1 -1
  39. package/dist/component/validators.d.ts +3 -50
  40. package/dist/component/validators.d.ts.map +1 -1
  41. package/dist/component/validators.js +4 -12
  42. package/dist/component/validators.js.map +1 -1
  43. package/dist/tracker/generated.d.ts +4 -4
  44. package/dist/tracker/generated.d.ts.map +1 -1
  45. package/dist/tracker/generated.js +4 -4
  46. package/dist/tracker/generated.js.map +1 -1
  47. package/dist/tracker/tracker.js +7 -6
  48. package/dist/tracker/tracker.js.map +1 -1
  49. package/dist/tracker.min.js +1 -1
  50. package/docs/benchmarks/2026-08-20-realistic.md +1 -1
  51. package/package.json +2 -6
  52. package/scripts/benchmark-ingest.mjs +81 -32
  53. package/src/component/_generated/api.ts +5 -5
  54. package/src/component/_generated/component.ts +13 -0
  55. package/src/component/_generated/server.ts +4 -0
  56. package/src/component/convex.config.ts +11 -5
  57. package/src/component/diagnostics.ts +64 -0
  58. package/src/component/eventStore.ts +78 -280
  59. package/src/component/geo.ts +779 -0
  60. package/src/component/http.ts +184 -17
  61. package/src/component/ingest.ts +49 -15
  62. package/src/component/reports.ts +4 -1
  63. package/src/component/schema.ts +15 -16
  64. package/src/component/validators.ts +4 -13
  65. package/src/test.ts +0 -2
  66. package/dist/component/migrations.d.ts +0 -17
  67. package/dist/component/migrations.d.ts.map +0 -1
  68. package/dist/component/migrations.js +0 -44
  69. package/dist/component/migrations.js.map +0 -1
  70. package/src/component/migrations.ts +0 -52
@@ -3,7 +3,6 @@ import type { MutationCtx, QueryCtx } from "./_generated/server.js";
3
3
  import {
4
4
  JOURNEY_READ_BYTE_RESERVE,
5
5
  JOURNEY_READ_DOCUMENT_RESERVE,
6
- MAX_BATCH_EVENTS,
7
6
  MAX_EVENTS_PER_SESSION_WINDOW,
8
7
  RATE_LIMIT_WINDOW_MS,
9
8
  } from "./constants.js";
@@ -32,23 +31,22 @@ type BufferedEventBatch = {
32
31
 
33
32
  export type EventBatchBuffer = Map<string, BufferedEventBatch>;
34
33
 
35
- type MigrationBatch = Pick<
36
- Doc<"eventBatches">,
37
- "_id" | "visitorId" | "events" | "migrationVersion"
38
- >;
39
-
40
- const migrationBatchCache = new WeakMap<
41
- MutationCtx,
42
- Map<string, MigrationBatch[]>
43
- >();
44
-
45
34
  export type JourneyCursor = {
46
35
  timestamp: number;
47
36
  creationTime: number;
48
37
  id: string;
49
38
  };
50
39
 
51
- export type RawJourneyEvent = Omit<Doc<"events">, "_id"> & { _id: string };
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
+ };
52
50
 
53
51
  export function eventBucketStart(timestamp: number): number {
54
52
  return Math.floor(timestamp / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
@@ -64,32 +62,15 @@ export async function inspectTelemetryMinute(
64
62
  siteId: Id<"sites">;
65
63
  sessionId: string;
66
64
  bucketStart: number;
67
- candidateEventIds: Set<string>;
68
65
  },
69
66
  ) {
70
- const [legacyEvents, batches] = await loadTelemetryMinute(ctx, input);
71
- const eventIds = new Set([
72
- ...legacyEvents.map((event) => event.eventId),
73
- ...batches.flatMap((batch) =>
74
- batch.events.map((event) => event.eventId),
75
- ),
76
- ]);
77
- if (legacyEvents.length > MAX_EVENTS_PER_SESSION_WINDOW) {
78
- await Promise.all(
79
- [...input.candidateEventIds]
80
- .filter((eventId) => !eventIds.has(eventId))
81
- .map(async (eventId) => {
82
- const legacy = await findLegacyTelemetry(ctx, {
83
- ...input,
84
- eventId,
85
- });
86
- if (legacy) eventIds.add(eventId);
87
- }),
88
- );
89
- }
67
+ const batches = await loadTelemetryMinute(ctx, input);
68
+ const eventIds = new Set(
69
+ batches.flatMap((batch) => batch.events.map((event) => event.eventId)),
70
+ );
90
71
  return {
91
72
  eventIds,
92
- persistedCount: telemetryMinuteCount(legacyEvents, batches),
73
+ persistedCount: telemetryMinuteCount(batches),
93
74
  };
94
75
  }
95
76
 
@@ -99,28 +80,13 @@ export async function findTelemetry(
99
80
  event: TrackerEvent,
100
81
  ): Promise<{ match: TelemetryMatch | null; saturated: boolean }> {
101
82
  const bucketStart = eventBucketStart(event.timestamp);
102
- const [legacyEvents, batches] = await loadTelemetryMinute(ctx, {
83
+ const batches = await loadTelemetryMinute(ctx, {
103
84
  siteId,
104
85
  sessionId: event.sessionId,
105
86
  bucketStart,
106
87
  });
107
- const persistedCount = telemetryMinuteCount(legacyEvents, batches);
108
- let legacyEvent = legacyEvents.find((candidate) =>
109
- candidate.eventId === event.eventId
110
- );
111
- if (
112
- !legacyEvent &&
113
- legacyEvents.length > MAX_EVENTS_PER_SESSION_WINDOW
114
- ) {
115
- legacyEvent = await findLegacyTelemetry(ctx, {
116
- siteId,
117
- sessionId: event.sessionId,
118
- bucketStart,
119
- eventId: event.eventId,
120
- }) ?? undefined;
121
- }
88
+ const persistedCount = telemetryMinuteCount(batches);
122
89
  const saturated = persistedCount >= MAX_EVENTS_PER_SESSION_WINDOW;
123
- if (legacyEvent) return { match: legacyEvent, saturated };
124
90
  let batch: Doc<"eventBatches"> | undefined;
125
91
  let nestedEvent: Doc<"eventBatches">["events"][number] | undefined;
126
92
  for (const candidate of batches) {
@@ -148,6 +114,7 @@ export function bufferEventBatch(
148
114
  siteId: Id<"sites">,
149
115
  event: TrackerEvent,
150
116
  buffer: EventBatchBuffer,
117
+ country: string,
151
118
  ) {
152
119
  const bucketStart = eventBucketStart(event.timestamp);
153
120
  const key = eventBatchKey(event.sessionId, bucketStart);
@@ -156,7 +123,7 @@ export function bufferEventBatch(
156
123
  if (existing.visitorId !== event.visitorId) {
157
124
  throw new Error("event batch visitor does not match its session");
158
125
  }
159
- existing.events.push(eventForBatch(event));
126
+ existing.events.push(eventForBatch(event, country));
160
127
  return;
161
128
  }
162
129
  buffer.set(key, {
@@ -164,7 +131,7 @@ export function bufferEventBatch(
164
131
  sessionId: event.sessionId,
165
132
  visitorId: event.visitorId,
166
133
  bucketStart,
167
- events: [eventForBatch(event)],
134
+ events: [eventForBatch(event, country)],
168
135
  });
169
136
  }
170
137
 
@@ -172,13 +139,14 @@ export async function insertEventBatch(
172
139
  ctx: MutationCtx,
173
140
  siteId: Id<"sites">,
174
141
  event: TrackerEvent,
142
+ country: string,
175
143
  ) {
176
144
  await ctx.db.insert("eventBatches", {
177
145
  siteId,
178
146
  sessionId: event.sessionId,
179
147
  visitorId: event.visitorId,
180
148
  bucketStart: eventBucketStart(event.timestamp),
181
- events: [eventForBatch(event)],
149
+ events: [eventForBatch(event, country)],
182
150
  });
183
151
  }
184
152
 
@@ -191,84 +159,6 @@ export async function flushEventBatches(
191
159
  }
192
160
  }
193
161
 
194
- export async function migrateLegacyEventToBatch(
195
- ctx: MutationCtx,
196
- event: Doc<"events">,
197
- ) {
198
- const bucketStart = eventBucketStart(event.timestamp);
199
- let transactionCache = migrationBatchCache.get(ctx);
200
- if (!transactionCache) {
201
- transactionCache = new Map();
202
- migrationBatchCache.set(ctx, transactionCache);
203
- }
204
- const cacheKey = JSON.stringify([
205
- event.siteId,
206
- event.sessionId,
207
- bucketStart,
208
- ]);
209
- let batches = transactionCache.get(cacheKey);
210
- if (!batches) {
211
- batches = await ctx.db
212
- .query("eventBatches")
213
- .withIndex("by_siteId_and_sessionId_and_bucketStart", (range) =>
214
- range
215
- .eq("siteId", event.siteId)
216
- .eq("sessionId", event.sessionId)
217
- .eq("bucketStart", bucketStart),
218
- )
219
- .order("desc")
220
- .take(MAX_EVENTS_PER_SESSION_WINDOW + 1);
221
- transactionCache.set(cacheKey, batches);
222
- }
223
- const duplicateBatch = batches.find((batch) =>
224
- batch.events.some((candidate) => candidate.eventId === event.eventId)
225
- );
226
- const context = eventContext(event);
227
- if (duplicateBatch) {
228
- if (Object.keys(context).length > 0) {
229
- const events = duplicateBatch.events.map((candidate) =>
230
- candidate.eventId === event.eventId
231
- ? { ...candidate, ...context }
232
- : candidate
233
- );
234
- await ctx.db.patch("eventBatches", duplicateBatch._id, {
235
- events,
236
- });
237
- duplicateBatch.events = events;
238
- }
239
- await ctx.db.delete("events", event._id);
240
- return;
241
- }
242
- const migratedEvent = {
243
- ...eventForBatch(event),
244
- ...context,
245
- };
246
- const latestBatch = batches[0];
247
- if (
248
- latestBatch?.migrationVersion === 1 &&
249
- latestBatch.visitorId === event.visitorId &&
250
- latestBatch.events.length < MAX_BATCH_EVENTS
251
- ) {
252
- const events = [...latestBatch.events, migratedEvent];
253
- await ctx.db.patch("eventBatches", latestBatch._id, {
254
- events,
255
- });
256
- latestBatch.events = events;
257
- } else {
258
- const batch = {
259
- siteId: event.siteId,
260
- sessionId: event.sessionId,
261
- visitorId: event.visitorId,
262
- bucketStart,
263
- events: [migratedEvent],
264
- migrationVersion: 1 as const,
265
- };
266
- const batchId = await ctx.db.insert("eventBatches", batch);
267
- batches.unshift({ _id: batchId, ...batch });
268
- }
269
- await ctx.db.delete("events", event._id);
270
- }
271
-
272
162
  export async function loadSessionJourneyEvents(
273
163
  ctx: QueryCtx,
274
164
  input: {
@@ -287,8 +177,7 @@ export async function loadSessionJourneyEvents(
287
177
  // be read completely before applying the cursor.
288
178
  const loadBatches = async () => {
289
179
  const batches: Doc<"eventBatches">[] = [];
290
- let eventCount = 0;
291
- const requiredEvents = input.resultLimit + MAX_EVENTS_PER_SESSION_WINDOW;
180
+ let qualifyingEvents = 0;
292
181
  let completedBucket: number | undefined;
293
182
  const query = ctx.db
294
183
  .query("eventBatches")
@@ -305,60 +194,23 @@ export async function loadSessionJourneyEvents(
305
194
  break;
306
195
  }
307
196
  batches.push(batch);
308
- eventCount += batch.events.length;
309
- if (eventCount >= requiredEvents) completedBucket = batch.bucketStart;
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
+ }
310
209
  }
311
210
  return batches;
312
211
  };
313
- const loadLegacyEvents = async () => {
314
- const take = input.resultLimit + 1;
315
- if (!input.cursor) {
316
- return ctx.db
317
- .query("events")
318
- .withIndex("by_siteId_and_sessionId_and_timestamp", (range) =>
319
- range
320
- .eq("siteId", input.siteId)
321
- .eq("sessionId", input.sessionId)
322
- .gte("timestamp", input.from)
323
- .lte("timestamp", input.to),
324
- )
325
- .order("asc")
326
- .take(take);
327
- }
328
- const rows = await ctx.db
329
- .query("events")
330
- .withIndex("by_siteId_and_sessionId_and_timestamp", (range) =>
331
- range
332
- .eq("siteId", input.siteId)
333
- .eq("sessionId", input.sessionId)
334
- .eq("timestamp", input.cursor!.timestamp)
335
- .gt("_creationTime", input.cursor!.creationTime),
336
- )
337
- .order("asc")
338
- .take(take);
339
- if (rows.length < take) {
340
- rows.push(...await ctx.db
341
- .query("events")
342
- .withIndex("by_siteId_and_sessionId_and_timestamp", (range) =>
343
- range
344
- .eq("siteId", input.siteId)
345
- .eq("sessionId", input.sessionId)
346
- .gt("timestamp", input.cursor!.timestamp)
347
- .lte("timestamp", input.to),
348
- )
349
- .order("asc")
350
- .take(take - rows.length));
351
- }
352
- return rows;
353
- };
354
- const [legacyEvents, batches] = await Promise.all([
355
- loadLegacyEvents(),
356
- loadBatches(),
357
- ]);
358
- const rows: RawJourneyEvent[] = legacyEvents.map((event) => ({
359
- ...event,
360
- _id: String(event._id),
361
- }));
212
+ const batches = await loadBatches();
213
+ const rows: RawJourneyEvent[] = [];
362
214
  appendBatchEvents(rows, batches);
363
215
  return rows
364
216
  .filter((event) =>
@@ -411,27 +263,8 @@ export async function loadVisitorJourneyEvents(
411
263
  }
412
264
  return batches;
413
265
  };
414
- const legacyEvents: Doc<"events">[] = [];
415
- const legacyQuery = ctx.db
416
- .query("events")
417
- .withIndex("by_siteId_and_visitorId_and_timestamp", (range) =>
418
- range
419
- .eq("siteId", input.siteId)
420
- .eq("visitorId", input.visitorId)
421
- .gte("timestamp", input.from)
422
- .lte("timestamp", input.to),
423
- )
424
- .order("asc");
425
- for await (const event of legacyQuery) {
426
- legacyEvents.push(event);
427
- await requireJourneyReadHeadroom(ctx);
428
- if (legacyEvents.length >= input.resultLimit) break;
429
- }
430
266
  const batches = await loadBatches();
431
- const rows: RawJourneyEvent[] = legacyEvents.map((event) => ({
432
- ...event,
433
- _id: String(event._id),
434
- }));
267
+ const rows: RawJourneyEvent[] = [];
435
268
  appendBatchEvents(rows, batches);
436
269
  return rows
437
270
  .filter((event) =>
@@ -505,32 +338,19 @@ export async function deleteTelemetryBefore(
505
338
  ) {
506
339
  const batchCutoff = eventBucketStart(input.before);
507
340
  const limit = input.batchSize + 1;
508
- const [legacyEvents, batches] = await Promise.all([
509
- ctx.db
510
- .query("events")
511
- .withIndex("by_siteId_and_timestamp", (range) =>
512
- range.eq("siteId", input.siteId).lt("timestamp", input.before),
513
- )
514
- .take(limit),
515
- ctx.db
516
- .query("eventBatches")
517
- .withIndex("by_siteId_and_bucketStart", (range) =>
518
- range.eq("siteId", input.siteId).lt("bucketStart", batchCutoff),
519
- )
520
- .take(limit),
521
- ]);
522
- 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);
523
348
  for (const row of rows) {
524
- if ("bucketStart" in row) {
525
- await ctx.db.delete("eventBatches", row._id);
526
- } else {
527
- await ctx.db.delete("events", row._id);
528
- }
349
+ await ctx.db.delete("eventBatches", row._id);
529
350
  }
530
351
  return {
531
352
  deleted: rows.length,
532
- hasMore:
533
- legacyEvents.length + batches.length > input.batchSize,
353
+ hasMore: batches.length > input.batchSize,
534
354
  };
535
355
  }
536
356
 
@@ -542,61 +362,30 @@ async function loadTelemetryMinute(
542
362
  bucketStart: number;
543
363
  },
544
364
  ) {
545
- return Promise.all([
546
- ctx.db
547
- .query("events")
548
- .withIndex("by_siteId_and_sessionId_and_timestamp", (range) =>
549
- range
550
- .eq("siteId", input.siteId)
551
- .eq("sessionId", input.sessionId)
552
- .gte("timestamp", input.bucketStart)
553
- .lt("timestamp", input.bucketStart + RATE_LIMIT_WINDOW_MS),
554
- )
555
- .take(MAX_EVENTS_PER_SESSION_WINDOW + 1),
556
- ctx.db
557
- .query("eventBatches")
558
- .withIndex("by_siteId_and_sessionId_and_bucketStart", (range) =>
559
- range
560
- .eq("siteId", input.siteId)
561
- .eq("sessionId", input.sessionId)
562
- .eq("bucketStart", input.bucketStart),
563
- )
564
- .take(MAX_EVENTS_PER_SESSION_WINDOW + 1),
565
- ]);
566
- }
567
-
568
- async function findLegacyTelemetry(
569
- ctx: MutationCtx,
570
- input: {
571
- siteId: Id<"sites">;
572
- sessionId: string;
573
- bucketStart: number;
574
- eventId: string;
575
- },
576
- ) {
577
- const candidates = await ctx.db
578
- .query("events")
579
- .withIndex("by_siteId_and_eventId", (range) =>
580
- 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),
581
372
  )
582
- .take(2);
583
- return candidates.find((candidate) =>
584
- candidate.sessionId === input.sessionId &&
585
- eventBucketStart(candidate.timestamp) === input.bucketStart
586
- ) ?? null;
373
+ .take(MAX_EVENTS_PER_SESSION_WINDOW + 1);
587
374
  }
588
375
 
589
376
  function telemetryMinuteCount(
590
- legacyEvents: Array<{ eventId: string }>,
591
377
  batches: Array<{ events: TrackerEvent[] }>,
592
378
  ) {
593
- return legacyEvents.length + batches.reduce(
379
+ return batches.reduce(
594
380
  (total, batch) => total + batch.events.length,
595
381
  0,
596
382
  );
597
383
  }
598
384
 
599
- function eventForBatch(event: TrackerEvent): TrackerEvent {
385
+ function eventForBatch(
386
+ event: TrackerEvent,
387
+ country: string,
388
+ ): TrackerEvent & { aggregateCountry?: string } {
600
389
  return {
601
390
  eventId: event.eventId,
602
391
  sessionId: event.sessionId,
@@ -615,6 +404,7 @@ function eventForBatch(event: TrackerEvent): TrackerEvent {
615
404
  : {}),
616
405
  ...(event.currency ? { currency: event.currency } : {}),
617
406
  ...(event.affiliateSlug ? { affiliateSlug: event.affiliateSlug } : {}),
407
+ aggregateCountry: country,
618
408
  };
619
409
  }
620
410
 
@@ -624,19 +414,27 @@ function appendBatchEvents(
624
414
  ) {
625
415
  for (const batch of batches) {
626
416
  for (let index = 0; index < batch.events.length; index += 1) {
627
- const event = batch.events[index];
628
- rows.push({
629
- ...event,
630
- _id: `eventBatch:${batch._id}:${index.toString().padStart(3, "0")}`,
631
- _creationTime: batch._creationTime,
632
- siteId: batch.siteId,
633
- sessionId: batch.sessionId,
634
- visitorId: batch.visitorId,
635
- });
417
+ rows.push(eventFromBatch(batch, index));
636
418
  }
637
419
  }
638
420
  }
639
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
+
640
438
  function eventContext(event: {
641
439
  country?: string;
642
440
  city?: string;