@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.
Files changed (79) hide show
  1. package/README.md +98 -18
  2. package/dist/component/_generated/api.d.ts +7 -1
  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/constants.d.ts +11 -6
  6. package/dist/component/constants.d.ts.map +1 -1
  7. package/dist/component/constants.js +11 -6
  8. package/dist/component/constants.js.map +1 -1
  9. package/dist/component/convex.config.d.ts +2 -2
  10. package/dist/component/convex.config.d.ts.map +1 -1
  11. package/dist/component/convex.config.js +4 -1
  12. package/dist/component/convex.config.js.map +1 -1
  13. package/dist/component/eventStore.d.ts +102 -0
  14. package/dist/component/eventStore.d.ts.map +1 -0
  15. package/dist/component/eventStore.js +463 -0
  16. package/dist/component/eventStore.js.map +1 -0
  17. package/dist/component/goals.d.ts.map +1 -1
  18. package/dist/component/goals.js +17 -4
  19. package/dist/component/goals.js.map +1 -1
  20. package/dist/component/http.d.ts.map +1 -1
  21. package/dist/component/http.js +39 -7
  22. package/dist/component/http.js.map +1 -1
  23. package/dist/component/ingest.d.ts.map +1 -1
  24. package/dist/component/ingest.js +520 -207
  25. package/dist/component/ingest.js.map +1 -1
  26. package/dist/component/live.d.ts +8 -0
  27. package/dist/component/live.d.ts.map +1 -1
  28. package/dist/component/live.js +72 -8
  29. package/dist/component/live.js.map +1 -1
  30. package/dist/component/migrations.d.ts +17 -0
  31. package/dist/component/migrations.d.ts.map +1 -0
  32. package/dist/component/migrations.js +44 -0
  33. package/dist/component/migrations.js.map +1 -0
  34. package/dist/component/reports.d.ts +16 -16
  35. package/dist/component/reports.d.ts.map +1 -1
  36. package/dist/component/reports.js +228 -65
  37. package/dist/component/reports.js.map +1 -1
  38. package/dist/component/retention.d.ts.map +1 -1
  39. package/dist/component/retention.js +15 -10
  40. package/dist/component/retention.js.map +1 -1
  41. package/dist/component/sanitize.d.ts.map +1 -1
  42. package/dist/component/sanitize.js +11 -3
  43. package/dist/component/sanitize.js.map +1 -1
  44. package/dist/component/schema.d.ts +132 -9
  45. package/dist/component/schema.js +32 -7
  46. package/dist/component/schema.js.map +1 -1
  47. package/dist/component/sites.d.ts.map +1 -1
  48. package/dist/component/sites.js +27 -18
  49. package/dist/component/sites.js.map +1 -1
  50. package/dist/component/validators.d.ts +96 -0
  51. package/dist/component/validators.d.ts.map +1 -1
  52. package/dist/component/validators.js +12 -0
  53. package/dist/component/validators.js.map +1 -1
  54. package/dist/tracker/generated.d.ts +4 -4
  55. package/dist/tracker/generated.d.ts.map +1 -1
  56. package/dist/tracker/generated.js +4 -4
  57. package/dist/tracker/generated.js.map +1 -1
  58. package/dist/tracker/tracker.js +33 -22
  59. package/dist/tracker/tracker.js.map +1 -1
  60. package/dist/tracker.min.js +1 -1
  61. package/docs/benchmarks/2026-08-20-realistic.md +171 -0
  62. package/package.json +15 -5
  63. package/scripts/benchmark-ingest.mjs +499 -0
  64. package/src/component/_generated/api.ts +7 -1
  65. package/src/component/constants.ts +11 -6
  66. package/src/component/convex.config.ts +5 -1
  67. package/src/component/eventStore.ts +684 -0
  68. package/src/component/goals.ts +25 -4
  69. package/src/component/http.ts +49 -6
  70. package/src/component/ingest.ts +671 -215
  71. package/src/component/live.ts +83 -8
  72. package/src/component/migrations.ts +52 -0
  73. package/src/component/reports.ts +295 -86
  74. package/src/component/retention.ts +15 -12
  75. package/src/component/sanitize.ts +10 -3
  76. package/src/component/schema.ts +35 -7
  77. package/src/component/sites.ts +31 -18
  78. package/src/component/validators.ts +16 -0
  79. 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
- batchSize + 1,
56
+ effectiveBatchSize + 1,
46
57
  );
47
- for (const remove of deletions.slice(0, batchSize)) {
58
+ for (const remove of deletions.slice(0, effectiveBatchSize)) {
48
59
  await remove();
49
60
  }
50
61
  return {
51
- deleted: Math.min(deletions.length, batchSize),
52
- hasMore: deletions.length > batchSize,
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 === undefined
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`);
@@ -3,7 +3,8 @@ import { v } from "convex/values";
3
3
  import {
4
4
  affiliateFieldsValidator,
5
5
  aggregateFieldsValidator,
6
- eventFieldsValidator,
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(eventFieldsValidator)
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
- .index("by_siteId_and_sessionId_and_timestamp", [
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
- "timestamp",
87
+ "bucketStart",
66
88
  ])
67
- .index("by_siteId_and_type_and_timestamp", ["siteId", "type", "timestamp"]),
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(),
@@ -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) return existing._id;
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: args.cookieless ?? true,
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 !== undefined &&
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 };