@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
|
@@ -4,6 +4,7 @@ import type { MutationCtx } from "./_generated/server.js";
|
|
|
4
4
|
import { mutation } from "./_generated/server.js";
|
|
5
5
|
import { MAX_RETENTION_BATCH } from "./constants.js";
|
|
6
6
|
import { fail } from "./errors.js";
|
|
7
|
+
import { deleteTelemetryBefore } from "./eventStore.js";
|
|
7
8
|
|
|
8
9
|
const cleanupKindValidator = v.union(
|
|
9
10
|
v.literal("events"),
|
|
@@ -37,19 +38,29 @@ export const cleanup = mutation({
|
|
|
37
38
|
if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > MAX_RETENTION_BATCH) {
|
|
38
39
|
fail("INVALID_ARGUMENT", `batchSize must be an integer between 1 and ${MAX_RETENTION_BATCH}`);
|
|
39
40
|
}
|
|
41
|
+
const effectiveBatchSize = args.kind === "events"
|
|
42
|
+
? Math.min(batchSize, 32)
|
|
43
|
+
: batchSize;
|
|
44
|
+
if (args.kind === "events") {
|
|
45
|
+
return await deleteTelemetryBefore(ctx, {
|
|
46
|
+
siteId: args.siteId,
|
|
47
|
+
before: args.before,
|
|
48
|
+
batchSize: effectiveBatchSize,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
40
51
|
const deletions = await deletionsForCleanup(
|
|
41
52
|
ctx,
|
|
42
53
|
args.siteId,
|
|
43
54
|
args.kind,
|
|
44
55
|
args.before,
|
|
45
|
-
|
|
56
|
+
effectiveBatchSize + 1,
|
|
46
57
|
);
|
|
47
|
-
for (const remove of deletions.slice(0,
|
|
58
|
+
for (const remove of deletions.slice(0, effectiveBatchSize)) {
|
|
48
59
|
await remove();
|
|
49
60
|
}
|
|
50
61
|
return {
|
|
51
|
-
deleted: Math.min(deletions.length,
|
|
52
|
-
hasMore: deletions.length >
|
|
62
|
+
deleted: Math.min(deletions.length, effectiveBatchSize),
|
|
63
|
+
hasMore: deletions.length > effectiveBatchSize,
|
|
53
64
|
};
|
|
54
65
|
},
|
|
55
66
|
});
|
|
@@ -78,14 +89,6 @@ async function deletionsForCleanup(
|
|
|
78
89
|
before: number,
|
|
79
90
|
limit: number,
|
|
80
91
|
): Promise<CleanupDelete[]> {
|
|
81
|
-
if (kind === "events") {
|
|
82
|
-
return (await ctx.db
|
|
83
|
-
.query("events")
|
|
84
|
-
.withIndex("by_siteId_and_timestamp", (range) =>
|
|
85
|
-
range.eq("siteId", siteId).lt("timestamp", before),
|
|
86
|
-
)
|
|
87
|
-
.take(limit)).map((row) => () => ctx.db.delete("events", row._id));
|
|
88
|
-
}
|
|
89
92
|
if (kind === "sessions") {
|
|
90
93
|
return (await ctx.db
|
|
91
94
|
.query("sessions")
|
|
@@ -152,12 +152,19 @@ export function sanitizeEvent(event: TrackerEvent, now: number): TrackerEvent {
|
|
|
152
152
|
properties: sanitizeProperties(event.properties),
|
|
153
153
|
revenueCents: event.revenueCents,
|
|
154
154
|
currency,
|
|
155
|
-
affiliateSlug: event.affiliateSlug
|
|
156
|
-
? undefined
|
|
157
|
-
: sanitizeSlug(event.affiliateSlug, "affiliateSlug"),
|
|
155
|
+
affiliateSlug: optionalAffiliateSlug(event.affiliateSlug),
|
|
158
156
|
};
|
|
159
157
|
}
|
|
160
158
|
|
|
159
|
+
function optionalAffiliateSlug(value: string | undefined): string | undefined {
|
|
160
|
+
if (value === undefined) return undefined;
|
|
161
|
+
try {
|
|
162
|
+
return sanitizeSlug(value, "affiliateSlug");
|
|
163
|
+
} catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
161
168
|
export function normalizeDomains(domains: string[]): string[] {
|
|
162
169
|
if (domains.length === 0 || domains.length > MAX_SITE_DOMAINS) {
|
|
163
170
|
throw new Error(`domains must contain between 1 and ${MAX_SITE_DOMAINS} entries`);
|
package/src/component/schema.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { v } from "convex/values";
|
|
|
3
3
|
import {
|
|
4
4
|
affiliateFieldsValidator,
|
|
5
5
|
aggregateFieldsValidator,
|
|
6
|
-
|
|
6
|
+
batchedEventValidator,
|
|
7
|
+
storedEventFieldsValidator,
|
|
7
8
|
funnelFieldsValidator,
|
|
8
9
|
goalFieldsValidator,
|
|
9
10
|
sessionFieldsValidator,
|
|
@@ -45,26 +46,52 @@ export default defineSchema({
|
|
|
45
46
|
device: v.string(),
|
|
46
47
|
pageviewCount: v.number(),
|
|
47
48
|
lastHeartbeatEventId: v.optional(v.string()),
|
|
49
|
+
recentHeartbeatEventIds: v.optional(v.array(v.string())),
|
|
48
50
|
eventCount: v.number(),
|
|
49
51
|
})
|
|
50
52
|
.index("by_siteId_and_sessionId", ["siteId", "sessionId"])
|
|
51
53
|
.index("by_siteId_and_expiresAt", ["siteId", "expiresAt"])
|
|
52
|
-
.index("by_siteId_and_lastSeenAt", ["siteId", "lastSeenAt"])
|
|
54
|
+
.index("by_siteId_and_lastSeenAt", ["siteId", "lastSeenAt"])
|
|
55
|
+
.index("by_expiresAt", { fields: ["expiresAt"], staged: true }),
|
|
56
|
+
|
|
57
|
+
liveSweepStates: defineTable({
|
|
58
|
+
siteId: v.id("sites"),
|
|
59
|
+
scheduledAt: v.number(),
|
|
60
|
+
}).index("by_siteId", ["siteId"]),
|
|
53
61
|
|
|
54
|
-
events: defineTable(
|
|
62
|
+
events: defineTable(storedEventFieldsValidator)
|
|
55
63
|
.index("by_siteId_and_eventId", ["siteId", "eventId"])
|
|
56
64
|
.index("by_siteId_and_timestamp", ["siteId", "timestamp"])
|
|
65
|
+
.index("by_siteId_and_sessionId_and_timestamp", [
|
|
66
|
+
"siteId",
|
|
67
|
+
"sessionId",
|
|
68
|
+
"timestamp",
|
|
69
|
+
])
|
|
57
70
|
.index("by_siteId_and_visitorId_and_timestamp", [
|
|
58
71
|
"siteId",
|
|
59
72
|
"visitorId",
|
|
60
73
|
"timestamp",
|
|
61
|
-
])
|
|
62
|
-
|
|
74
|
+
]),
|
|
75
|
+
|
|
76
|
+
eventBatches: defineTable({
|
|
77
|
+
siteId: v.id("sites"),
|
|
78
|
+
sessionId: v.string(),
|
|
79
|
+
visitorId: v.string(),
|
|
80
|
+
bucketStart: v.number(),
|
|
81
|
+
events: v.array(batchedEventValidator),
|
|
82
|
+
migrationVersion: v.optional(v.literal(1)),
|
|
83
|
+
})
|
|
84
|
+
.index("by_siteId_and_sessionId_and_bucketStart", [
|
|
63
85
|
"siteId",
|
|
64
86
|
"sessionId",
|
|
65
|
-
"
|
|
87
|
+
"bucketStart",
|
|
66
88
|
])
|
|
67
|
-
.index("
|
|
89
|
+
.index("by_siteId_and_bucketStart", ["siteId", "bucketStart"])
|
|
90
|
+
.index("by_siteId_and_visitorId_and_bucketStart", [
|
|
91
|
+
"siteId",
|
|
92
|
+
"visitorId",
|
|
93
|
+
"bucketStart",
|
|
94
|
+
]),
|
|
68
95
|
|
|
69
96
|
aggregateBuckets: defineTable(aggregateFieldsValidator)
|
|
70
97
|
.index("by_siteId_and_granularity_and_bucketStart_and_shard", [
|
|
@@ -88,6 +115,7 @@ export default defineSchema({
|
|
|
88
115
|
goalId: v.id("goals"),
|
|
89
116
|
goalKey: v.string(),
|
|
90
117
|
eventId: v.string(),
|
|
118
|
+
idempotencyVersion: v.optional(v.literal(2)),
|
|
91
119
|
sessionId: v.string(),
|
|
92
120
|
visitorId: v.string(),
|
|
93
121
|
timestamp: v.number(),
|
package/src/component/sites.ts
CHANGED
|
@@ -33,13 +33,41 @@ export const create = mutation({
|
|
|
33
33
|
const name = cleanString(args.name, 120);
|
|
34
34
|
if (!ownerId || !name) fail("INVALID_SITE", "ownerId and name are required");
|
|
35
35
|
|
|
36
|
+
let domains: string[];
|
|
37
|
+
let timezone: string | undefined;
|
|
38
|
+
let networkId: string | undefined;
|
|
39
|
+
let currency: string;
|
|
40
|
+
try {
|
|
41
|
+
domains = normalizeDomains(args.domains);
|
|
42
|
+
timezone = validateTimezone(args.timezone);
|
|
43
|
+
networkId = args.networkId
|
|
44
|
+
? sanitizeOpaqueId(args.networkId, "networkId")
|
|
45
|
+
: undefined;
|
|
46
|
+
currency = sanitizeCurrency(args.currency ?? "USD");
|
|
47
|
+
} catch (error) {
|
|
48
|
+
fail("INVALID_SITE", error instanceof Error ? error.message : "invalid site");
|
|
49
|
+
}
|
|
50
|
+
const cookieless = args.cookieless ?? true;
|
|
51
|
+
|
|
36
52
|
const existing = await ctx.db
|
|
37
53
|
.query("sites")
|
|
38
54
|
.withIndex("by_ownerId_and_name", (range) =>
|
|
39
55
|
range.eq("ownerId", ownerId).eq("name", name),
|
|
40
56
|
)
|
|
41
57
|
.unique();
|
|
42
|
-
if (existing)
|
|
58
|
+
if (existing) {
|
|
59
|
+
if (
|
|
60
|
+
existing.domains.length !== domains.length ||
|
|
61
|
+
existing.domains.some((domain, index) => domain !== domains[index]) ||
|
|
62
|
+
existing.networkId !== networkId ||
|
|
63
|
+
existing.timezone !== timezone ||
|
|
64
|
+
(existing.currency ?? "USD") !== currency ||
|
|
65
|
+
existing.cookieless !== cookieless
|
|
66
|
+
) {
|
|
67
|
+
fail("CONFLICT", "site name is already associated with different settings");
|
|
68
|
+
}
|
|
69
|
+
return existing._id;
|
|
70
|
+
}
|
|
43
71
|
|
|
44
72
|
const sites = await ctx.db
|
|
45
73
|
.query("sites")
|
|
@@ -51,20 +79,6 @@ export const create = mutation({
|
|
|
51
79
|
});
|
|
52
80
|
}
|
|
53
81
|
|
|
54
|
-
let domains: string[];
|
|
55
|
-
let timezone: string | undefined;
|
|
56
|
-
let networkId: string | undefined;
|
|
57
|
-
let currency: string;
|
|
58
|
-
try {
|
|
59
|
-
domains = normalizeDomains(args.domains);
|
|
60
|
-
timezone = validateTimezone(args.timezone);
|
|
61
|
-
networkId = args.networkId
|
|
62
|
-
? sanitizeOpaqueId(args.networkId, "networkId")
|
|
63
|
-
: undefined;
|
|
64
|
-
currency = sanitizeCurrency(args.currency ?? "USD");
|
|
65
|
-
} catch (error) {
|
|
66
|
-
fail("INVALID_SITE", error instanceof Error ? error.message : "invalid site");
|
|
67
|
-
}
|
|
68
82
|
const now = Date.now();
|
|
69
83
|
return await ctx.db.insert("sites", {
|
|
70
84
|
ownerId,
|
|
@@ -73,7 +87,7 @@ export const create = mutation({
|
|
|
73
87
|
networkId,
|
|
74
88
|
timezone,
|
|
75
89
|
currency,
|
|
76
|
-
cookieless
|
|
90
|
+
cookieless,
|
|
77
91
|
createdAt: now,
|
|
78
92
|
updatedAt: now,
|
|
79
93
|
});
|
|
@@ -138,8 +152,7 @@ export const update = mutation({
|
|
|
138
152
|
}
|
|
139
153
|
if (
|
|
140
154
|
patch.currency !== undefined &&
|
|
141
|
-
site.currency !==
|
|
142
|
-
site.currency !== patch.currency
|
|
155
|
+
(site.currency ?? "USD") !== patch.currency
|
|
143
156
|
) {
|
|
144
157
|
fail("CONFLICT", "site currency cannot be changed");
|
|
145
158
|
}
|
|
@@ -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(),
|
|
@@ -119,6 +123,18 @@ export const eventFieldsValidator = v.object({
|
|
|
119
123
|
device: v.string(),
|
|
120
124
|
});
|
|
121
125
|
|
|
126
|
+
export const storedEventFieldsValidator = eventFieldsValidator
|
|
127
|
+
.omit("country", "city", "latitude", "longitude", "browser", "os", "device")
|
|
128
|
+
.extend({
|
|
129
|
+
country: v.optional(v.string()),
|
|
130
|
+
city: v.optional(v.string()),
|
|
131
|
+
latitude: v.optional(v.number()),
|
|
132
|
+
longitude: v.optional(v.number()),
|
|
133
|
+
browser: v.optional(v.string()),
|
|
134
|
+
os: v.optional(v.string()),
|
|
135
|
+
device: v.optional(v.string()),
|
|
136
|
+
});
|
|
137
|
+
|
|
122
138
|
export const dimensionTypeValidator = v.union(
|
|
123
139
|
v.literal("source"),
|
|
124
140
|
v.literal("page"),
|
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 };
|