@iann29/rastro 0.1.0-alpha.0 → 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 +98 -18
- package/dist/component/_generated/api.d.ts +7 -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 +11 -6
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +11 -6
- 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 +102 -0
- package/dist/component/eventStore.d.ts.map +1 -0
- package/dist/component/eventStore.js +463 -0
- package/dist/component/eventStore.js.map +1 -0
- package/dist/component/goals.d.ts.map +1 -1
- package/dist/component/goals.js +17 -4
- package/dist/component/goals.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +39 -7
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +520 -207
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts +8 -0
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +72 -8
- package/dist/component/live.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 +16 -16
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +228 -65
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +15 -10
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +11 -3
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +132 -9
- package/dist/component/schema.js +32 -7
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +27 -18
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +96 -0
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +12 -0
- 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 +33 -22
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/benchmarks/2026-08-20-realistic.md +171 -0
- package/package.json +15 -5
- package/scripts/benchmark-ingest.mjs +499 -0
- package/src/component/_generated/api.ts +7 -1
- package/src/component/constants.ts +11 -6
- package/src/component/convex.config.ts +5 -1
- package/src/component/eventStore.ts +684 -0
- package/src/component/goals.ts +25 -4
- package/src/component/http.ts +49 -6
- package/src/component/ingest.ts +671 -215
- package/src/component/live.ts +83 -8
- package/src/component/migrations.ts +52 -0
- package/src/component/reports.ts +295 -86
- package/src/component/retention.ts +15 -12
- package/src/component/sanitize.ts +10 -3
- package/src/component/schema.ts +35 -7
- package/src/component/sites.ts +31 -18
- package/src/component/validators.ts +16 -0
- package/src/test.ts +2 -0
package/src/component/live.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { internal } from "./_generated/api.js";
|
|
3
3
|
import { internalMutation } from "./_generated/server.js";
|
|
4
|
+
import { LIVE_SWEEP_INTERVAL_MS } from "./constants.js";
|
|
5
|
+
|
|
6
|
+
const SWEEP_BATCH_SIZE = 500;
|
|
4
7
|
|
|
5
8
|
export const expire = internalMutation({
|
|
6
9
|
args: {
|
|
@@ -21,16 +24,88 @@ export const expire = internalMutation({
|
|
|
21
24
|
return { expired: false, rescheduled: false, stale: true };
|
|
22
25
|
}
|
|
23
26
|
if (liveSession.expiresAt > Date.now()) {
|
|
24
|
-
await ctx.db
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const state = await ctx.db
|
|
28
|
+
.query("liveSweepStates")
|
|
29
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", liveSession.siteId))
|
|
30
|
+
.unique();
|
|
31
|
+
const scheduledAt = Math.min(
|
|
32
|
+
liveSession.expiresAt,
|
|
33
|
+
Date.now() + LIVE_SWEEP_INTERVAL_MS,
|
|
34
|
+
);
|
|
35
|
+
if (!state || state.scheduledAt > scheduledAt) {
|
|
36
|
+
if (state) {
|
|
37
|
+
await ctx.db.patch("liveSweepStates", state._id, {
|
|
38
|
+
scheduledAt,
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
await ctx.db.insert("liveSweepStates", {
|
|
42
|
+
siteId: liveSession.siteId,
|
|
43
|
+
scheduledAt,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
await ctx.scheduler.runAt(scheduledAt, internal.live.sweep, {
|
|
47
|
+
siteId: liveSession.siteId,
|
|
48
|
+
scheduledAt,
|
|
49
|
+
});
|
|
50
|
+
return { expired: false, rescheduled: true, stale: false };
|
|
51
|
+
}
|
|
52
|
+
return { expired: false, rescheduled: false, stale: false };
|
|
32
53
|
}
|
|
33
54
|
await ctx.db.delete("liveSessions", liveSession._id);
|
|
34
55
|
return { expired: true, rescheduled: false, stale: false };
|
|
35
56
|
},
|
|
36
57
|
});
|
|
58
|
+
|
|
59
|
+
export const sweep = internalMutation({
|
|
60
|
+
args: {
|
|
61
|
+
siteId: v.id("sites"),
|
|
62
|
+
scheduledAt: v.number(),
|
|
63
|
+
},
|
|
64
|
+
returns: v.object({
|
|
65
|
+
expired: v.number(),
|
|
66
|
+
rescheduled: v.boolean(),
|
|
67
|
+
stale: v.boolean(),
|
|
68
|
+
}),
|
|
69
|
+
handler: async (ctx, args) => {
|
|
70
|
+
const state = await ctx.db
|
|
71
|
+
.query("liveSweepStates")
|
|
72
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
|
|
73
|
+
.unique();
|
|
74
|
+
if (!state || state.scheduledAt !== args.scheduledAt) {
|
|
75
|
+
return { expired: 0, rescheduled: false, stale: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const expired = await ctx.db
|
|
80
|
+
.query("liveSessions")
|
|
81
|
+
.withIndex("by_siteId_and_expiresAt", (range) =>
|
|
82
|
+
range.eq("siteId", args.siteId).lte("expiresAt", now),
|
|
83
|
+
)
|
|
84
|
+
.take(SWEEP_BATCH_SIZE);
|
|
85
|
+
for (const session of expired) {
|
|
86
|
+
await ctx.db.delete("liveSessions", session._id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const next = await ctx.db
|
|
90
|
+
.query("liveSessions")
|
|
91
|
+
.withIndex("by_siteId_and_expiresAt", (range) =>
|
|
92
|
+
range.eq("siteId", args.siteId),
|
|
93
|
+
)
|
|
94
|
+
.order("asc")
|
|
95
|
+
.first();
|
|
96
|
+
if (!next) {
|
|
97
|
+
await ctx.db.delete("liveSweepStates", state._id);
|
|
98
|
+
return { expired: expired.length, rescheduled: false, stale: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const scheduledAt = expired.length === SWEEP_BATCH_SIZE
|
|
102
|
+
? now
|
|
103
|
+
: now + LIVE_SWEEP_INTERVAL_MS;
|
|
104
|
+
await ctx.db.patch("liveSweepStates", state._id, { scheduledAt });
|
|
105
|
+
await ctx.scheduler.runAt(scheduledAt, internal.live.sweep, {
|
|
106
|
+
siteId: args.siteId,
|
|
107
|
+
scheduledAt,
|
|
108
|
+
});
|
|
109
|
+
return { expired: expired.length, rescheduled: true, stale: false };
|
|
110
|
+
},
|
|
111
|
+
});
|
|
@@ -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
|
@@ -3,22 +3,31 @@ import {
|
|
|
3
3
|
paginationResultValidator,
|
|
4
4
|
} from "convex/server";
|
|
5
5
|
import { v } from "convex/values";
|
|
6
|
+
import type { Doc } from "./_generated/dataModel.js";
|
|
6
7
|
import { query } from "./_generated/server.js";
|
|
7
8
|
import {
|
|
8
9
|
AGGREGATE_SHARDS,
|
|
9
10
|
DAY_MS,
|
|
10
11
|
HOUR_MS,
|
|
12
|
+
HOURLY_AGGREGATE_SHARDS,
|
|
11
13
|
MAX_AFFILIATES_PER_SITE,
|
|
12
14
|
MAX_FUNNELS_PER_SITE,
|
|
13
15
|
MAX_GOALS_PER_SITE,
|
|
14
16
|
MAX_JOURNEY_EVENTS,
|
|
15
17
|
MAX_LIVE_VISITORS,
|
|
16
|
-
MAX_MANUAL_PAGINATION_OFFSET,
|
|
17
18
|
MAX_OVERVIEW_AGGREGATE_ROWS,
|
|
18
19
|
MAX_REPORT_RANGE_DAYS,
|
|
19
20
|
MAX_REPORT_SITES,
|
|
20
21
|
} from "./constants.js";
|
|
21
22
|
import { fail } from "./errors.js";
|
|
23
|
+
import {
|
|
24
|
+
compareJourneyEvents,
|
|
25
|
+
hydrateEventContexts,
|
|
26
|
+
type JourneyCursor,
|
|
27
|
+
loadSessionJourneyEvents,
|
|
28
|
+
loadVisitorJourneyEvents,
|
|
29
|
+
type RawJourneyEvent,
|
|
30
|
+
} from "./eventStore.js";
|
|
22
31
|
import { isPlainRecord } from "./guards.js";
|
|
23
32
|
import {
|
|
24
33
|
affiliateFieldsValidator,
|
|
@@ -33,7 +42,7 @@ const sessionDocumentValidator = sessionFieldsValidator.extend({
|
|
|
33
42
|
_creationTime: v.number(),
|
|
34
43
|
});
|
|
35
44
|
const eventDocumentValidator = eventFieldsValidator.extend({
|
|
36
|
-
_id: v.
|
|
45
|
+
_id: v.string(),
|
|
37
46
|
_creationTime: v.number(),
|
|
38
47
|
});
|
|
39
48
|
const liveSessionDocumentValidator = v.object({
|
|
@@ -131,15 +140,16 @@ export const overview = query({
|
|
|
131
140
|
}
|
|
132
141
|
const currency = [...currencies][0];
|
|
133
142
|
const { from, to } = validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
|
|
134
|
-
const interval = args.interval ?? (to - from <=
|
|
135
|
-
if (interval === "hour" && to - from >
|
|
136
|
-
fail("INVALID_ARGUMENT", "hourly reports are limited to
|
|
143
|
+
const interval = args.interval ?? (to - from <= DAY_MS ? "hour" : "day");
|
|
144
|
+
if (interval === "hour" && to - from > DAY_MS) {
|
|
145
|
+
fail("INVALID_ARGUMENT", "hourly reports are limited to 24 hours");
|
|
137
146
|
}
|
|
138
147
|
const intervalMs = interval === "hour" ? HOUR_MS : DAY_MS;
|
|
139
148
|
const firstBucket = Math.floor(from / intervalMs) * intervalMs;
|
|
140
149
|
const lastBucket = Math.floor(to / intervalMs) * intervalMs;
|
|
141
150
|
const bucketCount = Math.floor((lastBucket - firstBucket) / intervalMs) + 1;
|
|
142
|
-
const perSiteLimit = bucketCount *
|
|
151
|
+
const perSiteLimit = bucketCount *
|
|
152
|
+
(interval === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
143
153
|
if (perSiteLimit * siteIds.length > MAX_OVERVIEW_AGGREGATE_ROWS) {
|
|
144
154
|
fail(
|
|
145
155
|
"LIMIT_EXCEEDED",
|
|
@@ -272,18 +282,59 @@ export const listSessions = query({
|
|
|
272
282
|
returns: paginationResultValidator(sessionDocumentValidator),
|
|
273
283
|
handler: async (ctx, args) => {
|
|
274
284
|
validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
|
|
275
|
-
const pagination =
|
|
276
|
-
|
|
277
|
-
.
|
|
278
|
-
.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
.
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
const pagination = parseKeysetPagination(args.paginationOpts, {
|
|
286
|
+
kind: "sessions",
|
|
287
|
+
siteId: String(args.siteId),
|
|
288
|
+
from: args.from,
|
|
289
|
+
to: args.to,
|
|
290
|
+
});
|
|
291
|
+
const rows: Doc<"sessions">[] = [];
|
|
292
|
+
const take = pagination.numItems + 1;
|
|
293
|
+
if (pagination.cursor) {
|
|
294
|
+
rows.push(
|
|
295
|
+
...(await ctx.db
|
|
296
|
+
.query("sessions")
|
|
297
|
+
.withIndex("by_siteId_and_lastSeenAt", (range) =>
|
|
298
|
+
range
|
|
299
|
+
.eq("siteId", args.siteId)
|
|
300
|
+
.eq("lastSeenAt", pagination.cursor!.timestamp)
|
|
301
|
+
.lt("_creationTime", pagination.cursor!.creationTime),
|
|
302
|
+
)
|
|
303
|
+
.order("desc")
|
|
304
|
+
.take(take)),
|
|
305
|
+
);
|
|
306
|
+
if (rows.length < take) {
|
|
307
|
+
rows.push(
|
|
308
|
+
...(await ctx.db
|
|
309
|
+
.query("sessions")
|
|
310
|
+
.withIndex("by_siteId_and_lastSeenAt", (range) =>
|
|
311
|
+
range
|
|
312
|
+
.eq("siteId", args.siteId)
|
|
313
|
+
.gte("lastSeenAt", args.from)
|
|
314
|
+
.lt("lastSeenAt", pagination.cursor!.timestamp),
|
|
315
|
+
)
|
|
316
|
+
.order("desc")
|
|
317
|
+
.take(take - rows.length)),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
rows.push(
|
|
322
|
+
...(await ctx.db
|
|
323
|
+
.query("sessions")
|
|
324
|
+
.withIndex("by_siteId_and_lastSeenAt", (range) =>
|
|
325
|
+
range
|
|
326
|
+
.eq("siteId", args.siteId)
|
|
327
|
+
.gte("lastSeenAt", args.from)
|
|
328
|
+
.lte("lastSeenAt", args.to),
|
|
329
|
+
)
|
|
330
|
+
.order("desc")
|
|
331
|
+
.take(take)),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return keysetPaginationResult(rows, pagination, (row) => ({
|
|
335
|
+
timestamp: row.lastSeenAt,
|
|
336
|
+
creationTime: row._creationTime,
|
|
337
|
+
}));
|
|
287
338
|
},
|
|
288
339
|
});
|
|
289
340
|
|
|
@@ -298,19 +349,23 @@ export const sessionJourney = query({
|
|
|
298
349
|
returns: paginationResultValidator(eventDocumentValidator),
|
|
299
350
|
handler: async (ctx, args) => {
|
|
300
351
|
validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
|
|
301
|
-
const pagination =
|
|
302
|
-
|
|
303
|
-
.
|
|
304
|
-
.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
.
|
|
312
|
-
|
|
313
|
-
|
|
352
|
+
const pagination = parseJourneyPagination(args.paginationOpts, {
|
|
353
|
+
kind: "events",
|
|
354
|
+
siteId: String(args.siteId),
|
|
355
|
+
sessionId: args.sessionId,
|
|
356
|
+
from: args.from,
|
|
357
|
+
to: args.to,
|
|
358
|
+
});
|
|
359
|
+
const rows = await loadSessionJourneyEvents(ctx, {
|
|
360
|
+
siteId: args.siteId,
|
|
361
|
+
sessionId: args.sessionId,
|
|
362
|
+
from: args.from,
|
|
363
|
+
to: args.to,
|
|
364
|
+
cursor: pagination.cursor,
|
|
365
|
+
resultLimit: pagination.numItems + 1,
|
|
366
|
+
});
|
|
367
|
+
const hydratedRows = await hydrateEventContexts(ctx, rows);
|
|
368
|
+
return journeyPaginationResult(hydratedRows, pagination);
|
|
314
369
|
},
|
|
315
370
|
});
|
|
316
371
|
|
|
@@ -324,18 +379,59 @@ export const listConversions = query({
|
|
|
324
379
|
returns: paginationResultValidator(conversionDocumentValidator),
|
|
325
380
|
handler: async (ctx, args) => {
|
|
326
381
|
validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
|
|
327
|
-
const pagination =
|
|
328
|
-
|
|
329
|
-
.
|
|
330
|
-
.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
.
|
|
337
|
-
|
|
338
|
-
|
|
382
|
+
const pagination = parseKeysetPagination(args.paginationOpts, {
|
|
383
|
+
kind: "conversions",
|
|
384
|
+
siteId: String(args.siteId),
|
|
385
|
+
from: args.from,
|
|
386
|
+
to: args.to,
|
|
387
|
+
});
|
|
388
|
+
const rows: Doc<"conversions">[] = [];
|
|
389
|
+
const take = pagination.numItems + 1;
|
|
390
|
+
if (pagination.cursor) {
|
|
391
|
+
rows.push(
|
|
392
|
+
...(await ctx.db
|
|
393
|
+
.query("conversions")
|
|
394
|
+
.withIndex("by_siteId_and_timestamp", (range) =>
|
|
395
|
+
range
|
|
396
|
+
.eq("siteId", args.siteId)
|
|
397
|
+
.eq("timestamp", pagination.cursor!.timestamp)
|
|
398
|
+
.lt("_creationTime", pagination.cursor!.creationTime),
|
|
399
|
+
)
|
|
400
|
+
.order("desc")
|
|
401
|
+
.take(take)),
|
|
402
|
+
);
|
|
403
|
+
if (rows.length < take) {
|
|
404
|
+
rows.push(
|
|
405
|
+
...(await ctx.db
|
|
406
|
+
.query("conversions")
|
|
407
|
+
.withIndex("by_siteId_and_timestamp", (range) =>
|
|
408
|
+
range
|
|
409
|
+
.eq("siteId", args.siteId)
|
|
410
|
+
.gte("timestamp", args.from)
|
|
411
|
+
.lt("timestamp", pagination.cursor!.timestamp),
|
|
412
|
+
)
|
|
413
|
+
.order("desc")
|
|
414
|
+
.take(take - rows.length)),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
rows.push(
|
|
419
|
+
...(await ctx.db
|
|
420
|
+
.query("conversions")
|
|
421
|
+
.withIndex("by_siteId_and_timestamp", (range) =>
|
|
422
|
+
range
|
|
423
|
+
.eq("siteId", args.siteId)
|
|
424
|
+
.gte("timestamp", args.from)
|
|
425
|
+
.lte("timestamp", args.to),
|
|
426
|
+
)
|
|
427
|
+
.order("desc")
|
|
428
|
+
.take(take)),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return keysetPaginationResult(rows, pagination, (row) => ({
|
|
432
|
+
timestamp: row.timestamp,
|
|
433
|
+
creationTime: row._creationTime,
|
|
434
|
+
}));
|
|
339
435
|
},
|
|
340
436
|
});
|
|
341
437
|
|
|
@@ -355,25 +451,22 @@ export const visitorJourney = query({
|
|
|
355
451
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_JOURNEY_EVENTS) {
|
|
356
452
|
fail("INVALID_ARGUMENT", `limit must be an integer between 1 and ${MAX_JOURNEY_EVENTS}`);
|
|
357
453
|
}
|
|
358
|
-
const rows = [];
|
|
454
|
+
const rows: RawJourneyEvent[] = [];
|
|
359
455
|
for (const siteId of siteIds) {
|
|
360
|
-
rows.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
);
|
|
456
|
+
const cutoff = rows.length >= limit
|
|
457
|
+
? rows[limit - 1]!.timestamp
|
|
458
|
+
: args.to;
|
|
459
|
+
rows.push(...await loadVisitorJourneyEvents(ctx, {
|
|
460
|
+
siteId,
|
|
461
|
+
visitorId: args.visitorId,
|
|
462
|
+
from: args.from,
|
|
463
|
+
to: cutoff,
|
|
464
|
+
resultLimit: limit,
|
|
465
|
+
}));
|
|
466
|
+
rows.sort(compareJourneyEvents);
|
|
467
|
+
if (rows.length > limit) rows.length = limit;
|
|
373
468
|
}
|
|
374
|
-
return rows
|
|
375
|
-
.sort((left, right) => left.timestamp - right.timestamp || left.sequence - right.sequence)
|
|
376
|
-
.slice(0, limit);
|
|
469
|
+
return hydrateEventContexts(ctx, rows);
|
|
377
470
|
},
|
|
378
471
|
});
|
|
379
472
|
|
|
@@ -633,24 +726,45 @@ function validateRange(from: number, to: number, maxDays: number) {
|
|
|
633
726
|
return { from: Math.trunc(from), to: Math.trunc(to) };
|
|
634
727
|
}
|
|
635
728
|
|
|
636
|
-
type
|
|
729
|
+
type KeysetPaginationInput = {
|
|
637
730
|
cursor: string | null;
|
|
638
731
|
numItems: number;
|
|
639
732
|
maximumRowsRead?: number;
|
|
640
733
|
maximumBytesRead?: number;
|
|
641
734
|
};
|
|
642
735
|
|
|
643
|
-
type
|
|
644
|
-
|
|
736
|
+
type PaginationScope = {
|
|
737
|
+
kind: "sessions" | "events" | "conversions";
|
|
738
|
+
siteId: string;
|
|
739
|
+
sessionId?: string;
|
|
740
|
+
from: number;
|
|
741
|
+
to: number;
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
type KeysetCursor = {
|
|
745
|
+
timestamp: number;
|
|
746
|
+
creationTime: number;
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
type KeysetPagination = {
|
|
750
|
+
numItems: number;
|
|
751
|
+
scope: PaginationScope;
|
|
752
|
+
cursor: KeysetCursor | null;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
type JourneyPagination = {
|
|
645
756
|
numItems: number;
|
|
757
|
+
scope: PaginationScope & { kind: "events"; sessionId: string };
|
|
758
|
+
cursor: JourneyCursor | null;
|
|
646
759
|
};
|
|
647
760
|
|
|
648
|
-
function
|
|
649
|
-
options:
|
|
650
|
-
|
|
761
|
+
function parseKeysetPagination(
|
|
762
|
+
options: KeysetPaginationInput,
|
|
763
|
+
scope: PaginationScope,
|
|
764
|
+
): KeysetPagination {
|
|
651
765
|
validatePagination(options);
|
|
652
766
|
if (options.cursor === null) {
|
|
653
|
-
return {
|
|
767
|
+
return { numItems: options.numItems, scope, cursor: null };
|
|
654
768
|
}
|
|
655
769
|
let decoded: unknown;
|
|
656
770
|
try {
|
|
@@ -660,35 +774,130 @@ function parseManualPagination(
|
|
|
660
774
|
}
|
|
661
775
|
if (
|
|
662
776
|
!isPlainRecord(decoded) ||
|
|
663
|
-
decoded.v !==
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
decoded.
|
|
667
|
-
decoded.
|
|
777
|
+
decoded.v !== 2 ||
|
|
778
|
+
decoded.k !== scope.kind ||
|
|
779
|
+
decoded.s !== scope.siteId ||
|
|
780
|
+
decoded.n !== (scope.sessionId ?? null) ||
|
|
781
|
+
decoded.f !== scope.from ||
|
|
782
|
+
decoded.t !== scope.to ||
|
|
783
|
+
typeof decoded.x !== "number" ||
|
|
784
|
+
!Number.isFinite(decoded.x) ||
|
|
785
|
+
decoded.x < scope.from ||
|
|
786
|
+
decoded.x > scope.to ||
|
|
787
|
+
typeof decoded.c !== "number" ||
|
|
788
|
+
!Number.isFinite(decoded.c) ||
|
|
789
|
+
decoded.c < 0
|
|
668
790
|
) {
|
|
669
|
-
fail("INVALID_ARGUMENT", "pagination cursor is invalid
|
|
791
|
+
fail("INVALID_ARGUMENT", "pagination cursor is invalid for this query");
|
|
670
792
|
}
|
|
671
|
-
return {
|
|
793
|
+
return {
|
|
794
|
+
numItems: options.numItems,
|
|
795
|
+
scope,
|
|
796
|
+
cursor: {
|
|
797
|
+
timestamp: decoded.x,
|
|
798
|
+
creationTime: decoded.c,
|
|
799
|
+
},
|
|
800
|
+
};
|
|
672
801
|
}
|
|
673
802
|
|
|
674
|
-
function
|
|
803
|
+
function parseJourneyPagination(
|
|
804
|
+
options: KeysetPaginationInput,
|
|
805
|
+
scope: PaginationScope & { kind: "events"; sessionId: string },
|
|
806
|
+
): JourneyPagination {
|
|
807
|
+
validatePagination(options);
|
|
808
|
+
if (options.cursor === null) {
|
|
809
|
+
return { numItems: options.numItems, scope, cursor: null };
|
|
810
|
+
}
|
|
811
|
+
let decoded: unknown;
|
|
812
|
+
try {
|
|
813
|
+
decoded = JSON.parse(atob(options.cursor));
|
|
814
|
+
} catch {
|
|
815
|
+
fail("INVALID_ARGUMENT", "pagination cursor is malformed");
|
|
816
|
+
}
|
|
817
|
+
if (
|
|
818
|
+
!isPlainRecord(decoded) ||
|
|
819
|
+
decoded.v !== 4 ||
|
|
820
|
+
decoded.k !== scope.kind ||
|
|
821
|
+
decoded.s !== scope.siteId ||
|
|
822
|
+
decoded.n !== scope.sessionId ||
|
|
823
|
+
decoded.f !== scope.from ||
|
|
824
|
+
decoded.t !== scope.to ||
|
|
825
|
+
typeof decoded.x !== "number" ||
|
|
826
|
+
!Number.isFinite(decoded.x) ||
|
|
827
|
+
decoded.x < scope.from ||
|
|
828
|
+
decoded.x > scope.to ||
|
|
829
|
+
typeof decoded.c !== "number" ||
|
|
830
|
+
!Number.isFinite(decoded.c) ||
|
|
831
|
+
decoded.c < 0 ||
|
|
832
|
+
typeof decoded.i !== "string" ||
|
|
833
|
+
decoded.i.length === 0
|
|
834
|
+
) {
|
|
835
|
+
fail("INVALID_ARGUMENT", "pagination cursor is invalid for this query");
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
numItems: options.numItems,
|
|
839
|
+
scope,
|
|
840
|
+
cursor: {
|
|
841
|
+
timestamp: decoded.x,
|
|
842
|
+
creationTime: decoded.c,
|
|
843
|
+
id: decoded.i,
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function keysetPaginationResult<T>(
|
|
675
849
|
rows: T[],
|
|
676
|
-
pagination:
|
|
850
|
+
pagination: KeysetPagination,
|
|
851
|
+
cursorFor: (row: T) => KeysetCursor,
|
|
677
852
|
) {
|
|
678
|
-
const page = rows.slice(
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
853
|
+
const page = rows.slice(0, pagination.numItems);
|
|
854
|
+
const isDone = rows.length <= pagination.numItems;
|
|
855
|
+
const next = page.length > 0
|
|
856
|
+
? cursorFor(page[page.length - 1])
|
|
857
|
+
: pagination.cursor;
|
|
858
|
+
return {
|
|
859
|
+
page,
|
|
860
|
+
isDone,
|
|
861
|
+
continueCursor: btoa(JSON.stringify({
|
|
862
|
+
v: 2,
|
|
863
|
+
k: pagination.scope.kind,
|
|
864
|
+
s: pagination.scope.siteId,
|
|
865
|
+
n: pagination.scope.sessionId ?? null,
|
|
866
|
+
f: pagination.scope.from,
|
|
867
|
+
t: pagination.scope.to,
|
|
868
|
+
x: next?.timestamp ?? pagination.scope.to,
|
|
869
|
+
c: next?.creationTime ?? Number.MAX_SAFE_INTEGER,
|
|
870
|
+
})),
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function journeyPaginationResult(
|
|
875
|
+
rows: Awaited<ReturnType<typeof hydrateEventContexts>>,
|
|
876
|
+
pagination: JourneyPagination,
|
|
877
|
+
) {
|
|
878
|
+
const page = rows.slice(0, pagination.numItems);
|
|
879
|
+
const isDone = rows.length <= pagination.numItems;
|
|
880
|
+
const last = page.at(-1);
|
|
881
|
+
const next = last
|
|
882
|
+
? {
|
|
883
|
+
timestamp: last.timestamp,
|
|
884
|
+
creationTime: last._creationTime,
|
|
885
|
+
id: last._id,
|
|
886
|
+
}
|
|
887
|
+
: pagination.cursor;
|
|
686
888
|
return {
|
|
687
889
|
page,
|
|
688
890
|
isDone,
|
|
689
891
|
continueCursor: btoa(JSON.stringify({
|
|
690
|
-
v:
|
|
691
|
-
|
|
892
|
+
v: 4,
|
|
893
|
+
k: pagination.scope.kind,
|
|
894
|
+
s: pagination.scope.siteId,
|
|
895
|
+
n: pagination.scope.sessionId,
|
|
896
|
+
f: pagination.scope.from,
|
|
897
|
+
t: pagination.scope.to,
|
|
898
|
+
x: next?.timestamp ?? pagination.scope.to,
|
|
899
|
+
c: next?.creationTime ?? Number.MAX_SAFE_INTEGER,
|
|
900
|
+
i: next?.id ?? "~",
|
|
692
901
|
})),
|
|
693
902
|
};
|
|
694
903
|
}
|