@iann29/rastro 0.1.0-alpha.0 → 0.1.0-alpha.1

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 (68) hide show
  1. package/README.md +85 -18
  2. package/dist/component/_generated/api.d.ts +2 -0
  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 +9 -6
  6. package/dist/component/constants.d.ts.map +1 -1
  7. package/dist/component/constants.js +9 -6
  8. package/dist/component/constants.js.map +1 -1
  9. package/dist/component/eventStore.d.ts +94 -0
  10. package/dist/component/eventStore.d.ts.map +1 -0
  11. package/dist/component/eventStore.js +304 -0
  12. package/dist/component/eventStore.js.map +1 -0
  13. package/dist/component/goals.d.ts.map +1 -1
  14. package/dist/component/goals.js +17 -4
  15. package/dist/component/goals.js.map +1 -1
  16. package/dist/component/http.d.ts.map +1 -1
  17. package/dist/component/http.js +39 -7
  18. package/dist/component/http.js.map +1 -1
  19. package/dist/component/ingest.d.ts.map +1 -1
  20. package/dist/component/ingest.js +520 -207
  21. package/dist/component/ingest.js.map +1 -1
  22. package/dist/component/live.d.ts +8 -0
  23. package/dist/component/live.d.ts.map +1 -1
  24. package/dist/component/live.js +72 -8
  25. package/dist/component/live.js.map +1 -1
  26. package/dist/component/reports.d.ts +16 -16
  27. package/dist/component/reports.d.ts.map +1 -1
  28. package/dist/component/reports.js +250 -66
  29. package/dist/component/reports.js.map +1 -1
  30. package/dist/component/retention.d.ts.map +1 -1
  31. package/dist/component/retention.js +15 -10
  32. package/dist/component/retention.js.map +1 -1
  33. package/dist/component/sanitize.d.ts.map +1 -1
  34. package/dist/component/sanitize.js +11 -3
  35. package/dist/component/sanitize.js.map +1 -1
  36. package/dist/component/schema.d.ts +101 -10
  37. package/dist/component/schema.js +30 -8
  38. package/dist/component/schema.js.map +1 -1
  39. package/dist/component/sites.d.ts.map +1 -1
  40. package/dist/component/sites.js +27 -18
  41. package/dist/component/sites.js.map +1 -1
  42. package/dist/component/validators.d.ts +49 -0
  43. package/dist/component/validators.d.ts.map +1 -1
  44. package/dist/component/validators.js +11 -0
  45. package/dist/component/validators.js.map +1 -1
  46. package/dist/tracker/generated.d.ts +4 -4
  47. package/dist/tracker/generated.d.ts.map +1 -1
  48. package/dist/tracker/generated.js +4 -4
  49. package/dist/tracker/generated.js.map +1 -1
  50. package/dist/tracker/tracker.js +33 -22
  51. package/dist/tracker/tracker.js.map +1 -1
  52. package/dist/tracker.min.js +1 -1
  53. package/docs/benchmarks/2026-08-20-realistic.md +171 -0
  54. package/package.json +10 -4
  55. package/scripts/benchmark-ingest.mjs +499 -0
  56. package/src/component/_generated/api.ts +2 -0
  57. package/src/component/constants.ts +9 -6
  58. package/src/component/eventStore.ts +465 -0
  59. package/src/component/goals.ts +25 -4
  60. package/src/component/http.ts +49 -6
  61. package/src/component/ingest.ts +671 -215
  62. package/src/component/live.ts +83 -8
  63. package/src/component/reports.ts +320 -82
  64. package/src/component/retention.ts +15 -12
  65. package/src/component/sanitize.ts +10 -3
  66. package/src/component/schema.ts +33 -8
  67. package/src/component/sites.ts +31 -18
  68. package/src/component/validators.ts +12 -0
@@ -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,11 +3,12 @@ import { v } from "convex/values";
3
3
  import {
4
4
  affiliateFieldsValidator,
5
5
  aggregateFieldsValidator,
6
- eventFieldsValidator,
6
+ storedEventFieldsValidator,
7
7
  funnelFieldsValidator,
8
8
  goalFieldsValidator,
9
9
  sessionFieldsValidator,
10
10
  siteFieldsValidator,
11
+ trackerEventValidator,
11
12
  } from "./validators.js";
12
13
 
13
14
  export default defineSchema({
@@ -45,26 +46,49 @@ 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"])
57
- .index("by_siteId_and_visitorId_and_timestamp", [
65
+ .index("by_siteId_and_sessionId_and_timestamp", [
58
66
  "siteId",
59
- "visitorId",
67
+ "sessionId",
60
68
  "timestamp",
61
69
  ])
62
- .index("by_siteId_and_sessionId_and_timestamp", [
70
+ .index("by_siteId_and_visitorId_and_timestamp", {
71
+ fields: ["siteId", "visitorId", "timestamp"],
72
+ staged: true,
73
+ }),
74
+
75
+ eventBatches: defineTable({
76
+ siteId: v.id("sites"),
77
+ sessionId: v.string(),
78
+ visitorId: v.string(),
79
+ bucketStart: v.number(),
80
+ events: v.array(trackerEventValidator),
81
+ })
82
+ .index("by_siteId_and_sessionId_and_bucketStart", [
63
83
  "siteId",
64
84
  "sessionId",
65
- "timestamp",
85
+ "bucketStart",
66
86
  ])
67
- .index("by_siteId_and_type_and_timestamp", ["siteId", "type", "timestamp"]),
87
+ .index("by_siteId_and_bucketStart", ["siteId", "bucketStart"])
88
+ .index("by_siteId_and_visitorId_and_bucketStart", {
89
+ fields: ["siteId", "visitorId", "bucketStart"],
90
+ staged: true,
91
+ }),
68
92
 
69
93
  aggregateBuckets: defineTable(aggregateFieldsValidator)
70
94
  .index("by_siteId_and_granularity_and_bucketStart_and_shard", [
@@ -88,6 +112,7 @@ export default defineSchema({
88
112
  goalId: v.id("goals"),
89
113
  goalKey: v.string(),
90
114
  eventId: v.string(),
115
+ idempotencyVersion: v.optional(v.literal(2)),
91
116
  sessionId: v.string(),
92
117
  visitorId: v.string(),
93
118
  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
  }
@@ -119,6 +119,18 @@ export const eventFieldsValidator = v.object({
119
119
  device: v.string(),
120
120
  });
121
121
 
122
+ export const storedEventFieldsValidator = eventFieldsValidator
123
+ .omit("country", "city", "latitude", "longitude", "browser", "os", "device")
124
+ .extend({
125
+ country: v.optional(v.string()),
126
+ city: v.optional(v.string()),
127
+ latitude: v.optional(v.number()),
128
+ longitude: v.optional(v.number()),
129
+ browser: v.optional(v.string()),
130
+ os: v.optional(v.string()),
131
+ device: v.optional(v.string()),
132
+ });
133
+
122
134
  export const dimensionTypeValidator = v.union(
123
135
  v.literal("source"),
124
136
  v.literal("page"),