@iann29/rastro 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +20 -7
- package/dist/component/_generated/api.d.ts +5 -1
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.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 +2 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +4 -1
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/eventStore.d.ts +8 -0
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/eventStore.js +176 -17
- package/dist/component/eventStore.js.map +1 -1
- package/dist/component/migrations.d.ts +17 -0
- package/dist/component/migrations.d.ts.map +1 -0
- package/dist/component/migrations.js +44 -0
- package/dist/component/migrations.js.map +1 -0
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +8 -29
- package/dist/component/reports.js.map +1 -1
- package/dist/component/schema.d.ts +34 -2
- package/dist/component/schema.js +13 -10
- package/dist/component/schema.js.map +1 -1
- package/dist/component/validators.d.ts +47 -0
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +1 -0
- package/dist/component/validators.js.map +1 -1
- package/package.json +6 -2
- package/src/component/_generated/api.ts +5 -1
- package/src/component/constants.ts +2 -0
- package/src/component/convex.config.ts +5 -1
- package/src/component/eventStore.ts +237 -18
- package/src/component/migrations.ts +52 -0
- package/src/component/reports.ts +8 -37
- package/src/component/schema.ts +13 -10
- package/src/component/validators.ts +4 -0
- package/src/test.ts +2 -0
|
@@ -1,9 +1,13 @@
|
|
|
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,
|
|
6
|
+
MAX_BATCH_EVENTS,
|
|
4
7
|
MAX_EVENTS_PER_SESSION_WINDOW,
|
|
5
8
|
RATE_LIMIT_WINDOW_MS,
|
|
6
9
|
} from "./constants.js";
|
|
10
|
+
import { fail } from "./errors.js";
|
|
7
11
|
import type { TrackerEvent } from "./validators.js";
|
|
8
12
|
|
|
9
13
|
export type TelemetryMatch = {
|
|
@@ -28,6 +32,16 @@ type BufferedEventBatch = {
|
|
|
28
32
|
|
|
29
33
|
export type EventBatchBuffer = Map<string, BufferedEventBatch>;
|
|
30
34
|
|
|
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
|
+
|
|
31
45
|
export type JourneyCursor = {
|
|
32
46
|
timestamp: number;
|
|
33
47
|
creationTime: number;
|
|
@@ -107,12 +121,24 @@ export async function findTelemetry(
|
|
|
107
121
|
}
|
|
108
122
|
const saturated = persistedCount >= MAX_EVENTS_PER_SESSION_WINDOW;
|
|
109
123
|
if (legacyEvent) return { match: legacyEvent, saturated };
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
124
|
+
let batch: Doc<"eventBatches"> | undefined;
|
|
125
|
+
let nestedEvent: Doc<"eventBatches">["events"][number] | undefined;
|
|
126
|
+
for (const candidate of batches) {
|
|
127
|
+
nestedEvent = candidate.events.find((nested) =>
|
|
128
|
+
nested.eventId === event.eventId
|
|
129
|
+
);
|
|
130
|
+
if (nestedEvent) {
|
|
131
|
+
batch = candidate;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
113
135
|
return {
|
|
114
|
-
match: batch
|
|
115
|
-
? {
|
|
136
|
+
match: batch && nestedEvent
|
|
137
|
+
? {
|
|
138
|
+
sessionId: batch.sessionId,
|
|
139
|
+
visitorId: batch.visitorId,
|
|
140
|
+
...eventContext(nestedEvent),
|
|
141
|
+
}
|
|
116
142
|
: null,
|
|
117
143
|
saturated,
|
|
118
144
|
};
|
|
@@ -165,6 +191,84 @@ export async function flushEventBatches(
|
|
|
165
191
|
}
|
|
166
192
|
}
|
|
167
193
|
|
|
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
|
+
|
|
168
272
|
export async function loadSessionJourneyEvents(
|
|
169
273
|
ctx: QueryCtx,
|
|
170
274
|
input: {
|
|
@@ -255,19 +359,7 @@ export async function loadSessionJourneyEvents(
|
|
|
255
359
|
...event,
|
|
256
360
|
_id: String(event._id),
|
|
257
361
|
}));
|
|
258
|
-
|
|
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
|
-
}
|
|
362
|
+
appendBatchEvents(rows, batches);
|
|
271
363
|
return rows
|
|
272
364
|
.filter((event) =>
|
|
273
365
|
event.timestamp >= input.from &&
|
|
@@ -278,6 +370,77 @@ export async function loadSessionJourneyEvents(
|
|
|
278
370
|
.slice(0, input.resultLimit);
|
|
279
371
|
}
|
|
280
372
|
|
|
373
|
+
export async function loadVisitorJourneyEvents(
|
|
374
|
+
ctx: QueryCtx,
|
|
375
|
+
input: {
|
|
376
|
+
siteId: Id<"sites">;
|
|
377
|
+
visitorId: string;
|
|
378
|
+
from: number;
|
|
379
|
+
to: number;
|
|
380
|
+
resultLimit: number;
|
|
381
|
+
},
|
|
382
|
+
): Promise<RawJourneyEvent[]> {
|
|
383
|
+
const firstBucket = eventBucketStart(input.from);
|
|
384
|
+
const lastBucket = eventBucketStart(input.to);
|
|
385
|
+
const loadBatches = async () => {
|
|
386
|
+
const batches: Doc<"eventBatches">[] = [];
|
|
387
|
+
let qualifyingEvents = 0;
|
|
388
|
+
let completedBucket: number | undefined;
|
|
389
|
+
const query = ctx.db
|
|
390
|
+
.query("eventBatches")
|
|
391
|
+
.withIndex("by_siteId_and_visitorId_and_bucketStart", (range) =>
|
|
392
|
+
range
|
|
393
|
+
.eq("siteId", input.siteId)
|
|
394
|
+
.eq("visitorId", input.visitorId)
|
|
395
|
+
.gte("bucketStart", firstBucket)
|
|
396
|
+
.lte("bucketStart", lastBucket),
|
|
397
|
+
)
|
|
398
|
+
.order("asc");
|
|
399
|
+
for await (const batch of query) {
|
|
400
|
+
if (completedBucket !== undefined && batch.bucketStart !== completedBucket) {
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
batches.push(batch);
|
|
404
|
+
await requireJourneyReadHeadroom(ctx);
|
|
405
|
+
qualifyingEvents += batch.events.filter((event) =>
|
|
406
|
+
event.timestamp >= input.from && event.timestamp <= input.to
|
|
407
|
+
).length;
|
|
408
|
+
if (qualifyingEvents >= input.resultLimit) {
|
|
409
|
+
completedBucket = batch.bucketStart;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return batches;
|
|
413
|
+
};
|
|
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
|
+
const batches = await loadBatches();
|
|
431
|
+
const rows: RawJourneyEvent[] = legacyEvents.map((event) => ({
|
|
432
|
+
...event,
|
|
433
|
+
_id: String(event._id),
|
|
434
|
+
}));
|
|
435
|
+
appendBatchEvents(rows, batches);
|
|
436
|
+
return rows
|
|
437
|
+
.filter((event) =>
|
|
438
|
+
event.timestamp >= input.from && event.timestamp <= input.to
|
|
439
|
+
)
|
|
440
|
+
.sort(compareJourneyEvents)
|
|
441
|
+
.slice(0, input.resultLimit);
|
|
442
|
+
}
|
|
443
|
+
|
|
281
444
|
export function compareJourneyEvents(
|
|
282
445
|
left: Pick<RawJourneyEvent, "timestamp" | "_creationTime" | "_id">,
|
|
283
446
|
right: Pick<RawJourneyEvent, "timestamp" | "_creationTime" | "_id">,
|
|
@@ -455,6 +618,62 @@ function eventForBatch(event: TrackerEvent): TrackerEvent {
|
|
|
455
618
|
};
|
|
456
619
|
}
|
|
457
620
|
|
|
621
|
+
function appendBatchEvents(
|
|
622
|
+
rows: RawJourneyEvent[],
|
|
623
|
+
batches: Doc<"eventBatches">[],
|
|
624
|
+
) {
|
|
625
|
+
for (const batch of batches) {
|
|
626
|
+
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
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function eventContext(event: {
|
|
641
|
+
country?: string;
|
|
642
|
+
city?: string;
|
|
643
|
+
latitude?: number;
|
|
644
|
+
longitude?: number;
|
|
645
|
+
browser?: string;
|
|
646
|
+
os?: string;
|
|
647
|
+
device?: string;
|
|
648
|
+
}) {
|
|
649
|
+
return {
|
|
650
|
+
...(event.country ? { country: event.country } : {}),
|
|
651
|
+
...(event.city ? { city: event.city } : {}),
|
|
652
|
+
...(event.latitude !== undefined ? { latitude: event.latitude } : {}),
|
|
653
|
+
...(event.longitude !== undefined ? { longitude: event.longitude } : {}),
|
|
654
|
+
...(event.browser ? { browser: event.browser } : {}),
|
|
655
|
+
...(event.os ? { os: event.os } : {}),
|
|
656
|
+
...(event.device ? { device: event.device } : {}),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function requireJourneyReadHeadroom(ctx: QueryCtx) {
|
|
661
|
+
const metrics = await ctx.meta.getTransactionMetrics();
|
|
662
|
+
if (
|
|
663
|
+
metrics.bytesRead.remaining < JOURNEY_READ_BYTE_RESERVE ||
|
|
664
|
+
metrics.documentsRead.remaining < JOURNEY_READ_DOCUMENT_RESERVE
|
|
665
|
+
) {
|
|
666
|
+
fail(
|
|
667
|
+
"LIMIT_EXCEEDED",
|
|
668
|
+
"visitor journey exceeds the safe read budget",
|
|
669
|
+
{
|
|
670
|
+
bytesRead: metrics.bytesRead.used,
|
|
671
|
+
documentsRead: metrics.documentsRead.used,
|
|
672
|
+
},
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
458
677
|
function compareJourneyEventToCursor(
|
|
459
678
|
event: Pick<RawJourneyEvent, "timestamp" | "_creationTime" | "_id">,
|
|
460
679
|
cursor: JourneyCursor,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Migrations } from "@convex-dev/migrations";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import { components } from "./_generated/api.js";
|
|
4
|
+
import { internalMutation, internalQuery } from "./_generated/server.js";
|
|
5
|
+
import { migrateLegacyEventToBatch } from "./eventStore.js";
|
|
6
|
+
import schema from "./schema.js";
|
|
7
|
+
|
|
8
|
+
const migrations = new Migrations(components.migrations, {
|
|
9
|
+
internalMutation,
|
|
10
|
+
schema,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const migrateLegacyEvents = migrations.define({
|
|
14
|
+
table: "events",
|
|
15
|
+
batchSize: 16,
|
|
16
|
+
migrateOne: migrateLegacyEventToBatch,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const legacyEventsStatus = internalQuery({
|
|
20
|
+
args: {},
|
|
21
|
+
returns: v.object({
|
|
22
|
+
complete: v.boolean(),
|
|
23
|
+
sourceEmpty: v.boolean(),
|
|
24
|
+
migrationState: v.union(
|
|
25
|
+
v.literal("inProgress"),
|
|
26
|
+
v.literal("success"),
|
|
27
|
+
v.literal("failed"),
|
|
28
|
+
v.literal("canceled"),
|
|
29
|
+
v.literal("unknown"),
|
|
30
|
+
),
|
|
31
|
+
processed: v.number(),
|
|
32
|
+
sampleRemainingId: v.union(v.id("events"), v.null()),
|
|
33
|
+
}),
|
|
34
|
+
handler: async (ctx) => {
|
|
35
|
+
const [remaining, statuses] = await Promise.all([
|
|
36
|
+
ctx.db.query("events").first(),
|
|
37
|
+
migrations.getStatus(ctx, {
|
|
38
|
+
migrations: ["migrations:migrateLegacyEvents"],
|
|
39
|
+
}),
|
|
40
|
+
]);
|
|
41
|
+
const migrationStatus = statuses[0];
|
|
42
|
+
const migrationState = migrationStatus?.state ?? "unknown";
|
|
43
|
+
const sourceEmpty = remaining === null;
|
|
44
|
+
return {
|
|
45
|
+
complete: sourceEmpty && migrationState === "success",
|
|
46
|
+
sourceEmpty,
|
|
47
|
+
migrationState,
|
|
48
|
+
processed: migrationStatus?.processed ?? 0,
|
|
49
|
+
sampleRemainingId: remaining?._id ?? null,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
package/src/component/reports.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
hydrateEventContexts,
|
|
26
26
|
type JourneyCursor,
|
|
27
27
|
loadSessionJourneyEvents,
|
|
28
|
+
loadVisitorJourneyEvents,
|
|
28
29
|
type RawJourneyEvent,
|
|
29
30
|
} from "./eventStore.js";
|
|
30
31
|
import { isPlainRecord } from "./guards.js";
|
|
@@ -450,47 +451,17 @@ export const visitorJourney = query({
|
|
|
450
451
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_JOURNEY_EVENTS) {
|
|
451
452
|
fail("INVALID_ARGUMENT", `limit must be an integer between 1 and ${MAX_JOURNEY_EVENTS}`);
|
|
452
453
|
}
|
|
453
|
-
const sessionGroups = await Promise.all(
|
|
454
|
-
siteIds.map((siteId) =>
|
|
455
|
-
ctx.db
|
|
456
|
-
.query("sessions")
|
|
457
|
-
.withIndex("by_siteId_and_visitorId_and_lastSeenAt", (range) =>
|
|
458
|
-
range
|
|
459
|
-
.eq("siteId", siteId)
|
|
460
|
-
.eq("visitorId", args.visitorId)
|
|
461
|
-
.gte("lastSeenAt", args.from),
|
|
462
|
-
)
|
|
463
|
-
.order("asc")
|
|
464
|
-
.take(MAX_JOURNEY_EVENTS + 1),
|
|
465
|
-
),
|
|
466
|
-
);
|
|
467
|
-
const sessions = sessionGroups
|
|
468
|
-
.flat()
|
|
469
|
-
.filter((session) => session.startedAt <= args.to)
|
|
470
|
-
.sort((left, right) => left.startedAt - right.startedAt)
|
|
471
|
-
.slice(0, MAX_JOURNEY_EVENTS);
|
|
472
454
|
const rows: RawJourneyEvent[] = [];
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const resultIsFull = rows.length >= limit;
|
|
476
|
-
const cutoff = resultIsFull
|
|
455
|
+
for (const siteId of siteIds) {
|
|
456
|
+
const cutoff = rows.length >= limit
|
|
477
457
|
? rows[limit - 1]!.timestamp
|
|
478
458
|
: args.to;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
"LIMIT_EXCEEDED",
|
|
483
|
-
"visitor journey has too many overlapping sessions",
|
|
484
|
-
{ maximumOverlappingSessions: 8 },
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
rows.push(...await loadSessionJourneyEvents(ctx, {
|
|
488
|
-
siteId: session.siteId,
|
|
489
|
-
sessionId: session.sessionId,
|
|
459
|
+
rows.push(...await loadVisitorJourneyEvents(ctx, {
|
|
460
|
+
siteId,
|
|
461
|
+
visitorId: args.visitorId,
|
|
490
462
|
from: args.from,
|
|
491
|
-
to:
|
|
492
|
-
|
|
493
|
-
resultLimit: resultIsFull ? limit : limit - rows.length,
|
|
463
|
+
to: cutoff,
|
|
464
|
+
resultLimit: limit,
|
|
494
465
|
}));
|
|
495
466
|
rows.sort(compareJourneyEvents);
|
|
496
467
|
if (rows.length > limit) rows.length = limit;
|
package/src/component/schema.ts
CHANGED
|
@@ -3,12 +3,12 @@ import { v } from "convex/values";
|
|
|
3
3
|
import {
|
|
4
4
|
affiliateFieldsValidator,
|
|
5
5
|
aggregateFieldsValidator,
|
|
6
|
+
batchedEventValidator,
|
|
6
7
|
storedEventFieldsValidator,
|
|
7
8
|
funnelFieldsValidator,
|
|
8
9
|
goalFieldsValidator,
|
|
9
10
|
sessionFieldsValidator,
|
|
10
11
|
siteFieldsValidator,
|
|
11
|
-
trackerEventValidator,
|
|
12
12
|
} from "./validators.js";
|
|
13
13
|
|
|
14
14
|
export default defineSchema({
|
|
@@ -67,17 +67,19 @@ export default defineSchema({
|
|
|
67
67
|
"sessionId",
|
|
68
68
|
"timestamp",
|
|
69
69
|
])
|
|
70
|
-
.index("by_siteId_and_visitorId_and_timestamp",
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
.index("by_siteId_and_visitorId_and_timestamp", [
|
|
71
|
+
"siteId",
|
|
72
|
+
"visitorId",
|
|
73
|
+
"timestamp",
|
|
74
|
+
]),
|
|
74
75
|
|
|
75
76
|
eventBatches: defineTable({
|
|
76
77
|
siteId: v.id("sites"),
|
|
77
78
|
sessionId: v.string(),
|
|
78
79
|
visitorId: v.string(),
|
|
79
80
|
bucketStart: v.number(),
|
|
80
|
-
events: v.array(
|
|
81
|
+
events: v.array(batchedEventValidator),
|
|
82
|
+
migrationVersion: v.optional(v.literal(1)),
|
|
81
83
|
})
|
|
82
84
|
.index("by_siteId_and_sessionId_and_bucketStart", [
|
|
83
85
|
"siteId",
|
|
@@ -85,10 +87,11 @@ export default defineSchema({
|
|
|
85
87
|
"bucketStart",
|
|
86
88
|
])
|
|
87
89
|
.index("by_siteId_and_bucketStart", ["siteId", "bucketStart"])
|
|
88
|
-
.index("by_siteId_and_visitorId_and_bucketStart",
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
.index("by_siteId_and_visitorId_and_bucketStart", [
|
|
91
|
+
"siteId",
|
|
92
|
+
"visitorId",
|
|
93
|
+
"bucketStart",
|
|
94
|
+
]),
|
|
92
95
|
|
|
93
96
|
aggregateBuckets: defineTable(aggregateFieldsValidator)
|
|
94
97
|
.index("by_siteId_and_granularity_and_bucketStart_and_shard", [
|
|
@@ -49,6 +49,10 @@ export const ingestContextValidator = v.object({
|
|
|
49
49
|
device: v.optional(v.string()),
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
export const batchedEventValidator = trackerEventValidator.extend(
|
|
53
|
+
ingestContextValidator.fields,
|
|
54
|
+
);
|
|
55
|
+
|
|
52
56
|
export const siteFieldsValidator = v.object({
|
|
53
57
|
ownerId: v.string(),
|
|
54
58
|
name: v.string(),
|
package/src/test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
|
+
import migrationsComponent from "@convex-dev/migrations/test";
|
|
2
3
|
import type { TestConvex } from "convex-test";
|
|
3
4
|
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
5
|
import schema from "./component/schema.js";
|
|
@@ -15,5 +16,6 @@ export function register(
|
|
|
15
16
|
name: string = "rastroAnalytics",
|
|
16
17
|
) {
|
|
17
18
|
t.registerComponent(name, schema, modules);
|
|
19
|
+
migrationsComponent.register(t, `${name}/migrations`);
|
|
18
20
|
}
|
|
19
21
|
export default { register, schema, modules };
|