@iann29/rastro 0.1.0-alpha.0

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 (126) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +352 -0
  3. package/assets/rastro-live-atlas.png +0 -0
  4. package/dist/client/_generated/_ignore.d.ts +1 -0
  5. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  6. package/dist/client/_generated/_ignore.js +3 -0
  7. package/dist/client/_generated/_ignore.js.map +1 -0
  8. package/dist/client/index.d.ts +960 -0
  9. package/dist/client/index.d.ts.map +1 -0
  10. package/dist/client/index.js +591 -0
  11. package/dist/client/index.js.map +1 -0
  12. package/dist/component/_generated/api.d.ts +60 -0
  13. package/dist/component/_generated/api.d.ts.map +1 -0
  14. package/dist/component/_generated/api.js +31 -0
  15. package/dist/component/_generated/api.js.map +1 -0
  16. package/dist/component/_generated/component.d.ts +569 -0
  17. package/dist/component/_generated/component.d.ts.map +1 -0
  18. package/dist/component/_generated/component.js +11 -0
  19. package/dist/component/_generated/component.js.map +1 -0
  20. package/dist/component/_generated/dataModel.d.ts +46 -0
  21. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  22. package/dist/component/_generated/dataModel.js +11 -0
  23. package/dist/component/_generated/dataModel.js.map +1 -0
  24. package/dist/component/_generated/server.d.ts +133 -0
  25. package/dist/component/_generated/server.d.ts.map +1 -0
  26. package/dist/component/_generated/server.js +80 -0
  27. package/dist/component/_generated/server.js.map +1 -0
  28. package/dist/component/affiliates.d.ts +28 -0
  29. package/dist/component/affiliates.d.ts.map +1 -0
  30. package/dist/component/affiliates.js +110 -0
  31. package/dist/component/affiliates.js.map +1 -0
  32. package/dist/component/constants.d.ts +28 -0
  33. package/dist/component/constants.d.ts.map +1 -0
  34. package/dist/component/constants.js +28 -0
  35. package/dist/component/constants.js.map +1 -0
  36. package/dist/component/convex.config.d.ts +3 -0
  37. package/dist/component/convex.config.d.ts.map +1 -0
  38. package/dist/component/convex.config.js +3 -0
  39. package/dist/component/convex.config.js.map +1 -0
  40. package/dist/component/errors.d.ts +3 -0
  41. package/dist/component/errors.d.ts.map +1 -0
  42. package/dist/component/errors.js +5 -0
  43. package/dist/component/errors.js.map +1 -0
  44. package/dist/component/funnels.d.ts +36 -0
  45. package/dist/component/funnels.d.ts.map +1 -0
  46. package/dist/component/funnels.js +117 -0
  47. package/dist/component/funnels.js.map +1 -0
  48. package/dist/component/goals.d.ts +30 -0
  49. package/dist/component/goals.d.ts.map +1 -0
  50. package/dist/component/goals.js +110 -0
  51. package/dist/component/goals.js.map +1 -0
  52. package/dist/component/guards.d.ts +2 -0
  53. package/dist/component/guards.d.ts.map +1 -0
  54. package/dist/component/guards.js +4 -0
  55. package/dist/component/guards.js.map +1 -0
  56. package/dist/component/http.d.ts +3 -0
  57. package/dist/component/http.d.ts.map +1 -0
  58. package/dist/component/http.js +270 -0
  59. package/dist/component/http.js.map +1 -0
  60. package/dist/component/ingest.d.ts +51 -0
  61. package/dist/component/ingest.d.ts.map +1 -0
  62. package/dist/component/ingest.js +991 -0
  63. package/dist/component/ingest.js.map +1 -0
  64. package/dist/component/live.d.ts +9 -0
  65. package/dist/component/live.d.ts.map +1 -0
  66. package/dist/component/live.js +36 -0
  67. package/dist/component/live.js.map +1 -0
  68. package/dist/component/reports.d.ts +330 -0
  69. package/dist/component/reports.d.ts.map +1 -0
  70. package/dist/component/reports.js +595 -0
  71. package/dist/component/reports.js.map +1 -0
  72. package/dist/component/retention.d.ts +10 -0
  73. package/dist/component/retention.d.ts.map +1 -0
  74. package/dist/component/retention.js +107 -0
  75. package/dist/component/retention.js.map +1 -0
  76. package/dist/component/sanitize.d.ts +20 -0
  77. package/dist/component/sanitize.d.ts.map +1 -0
  78. package/dist/component/sanitize.js +269 -0
  79. package/dist/component/sanitize.js.map +1 -0
  80. package/dist/component/schema.d.ts +517 -0
  81. package/dist/component/schema.d.ts.map +1 -0
  82. package/dist/component/schema.js +209 -0
  83. package/dist/component/schema.js.map +1 -0
  84. package/dist/component/sites.d.ts +54 -0
  85. package/dist/component/sites.d.ts.map +1 -0
  86. package/dist/component/sites.js +164 -0
  87. package/dist/component/sites.js.map +1 -0
  88. package/dist/component/validators.d.ts +377 -0
  89. package/dist/component/validators.d.ts.map +1 -0
  90. package/dist/component/validators.js +176 -0
  91. package/dist/component/validators.js.map +1 -0
  92. package/dist/react/index.d.ts +40 -0
  93. package/dist/react/index.d.ts.map +1 -0
  94. package/dist/react/index.js +31 -0
  95. package/dist/react/index.js.map +1 -0
  96. package/dist/tracker/generated.d.ts +5 -0
  97. package/dist/tracker/generated.d.ts.map +1 -0
  98. package/dist/tracker/generated.js +6 -0
  99. package/dist/tracker/generated.js.map +1 -0
  100. package/dist/tracker/tracker.d.ts +27 -0
  101. package/dist/tracker/tracker.d.ts.map +1 -0
  102. package/dist/tracker/tracker.js +108 -0
  103. package/dist/tracker/tracker.js.map +1 -0
  104. package/dist/tracker.min.js +1 -0
  105. package/package.json +121 -0
  106. package/src/component/_generated/api.ts +76 -0
  107. package/src/component/_generated/component.ts +706 -0
  108. package/src/component/_generated/dataModel.ts +60 -0
  109. package/src/component/_generated/server.ts +169 -0
  110. package/src/component/affiliates.ts +119 -0
  111. package/src/component/constants.ts +28 -0
  112. package/src/component/convex.config.ts +3 -0
  113. package/src/component/errors.ts +20 -0
  114. package/src/component/funnels.ts +134 -0
  115. package/src/component/goals.ts +122 -0
  116. package/src/component/guards.ts +3 -0
  117. package/src/component/http.ts +301 -0
  118. package/src/component/ingest.ts +1364 -0
  119. package/src/component/live.ts +36 -0
  120. package/src/component/reports.ts +720 -0
  121. package/src/component/retention.ts +180 -0
  122. package/src/component/sanitize.ts +278 -0
  123. package/src/component/schema.ts +232 -0
  124. package/src/component/sites.ts +188 -0
  125. package/src/component/validators.ts +226 -0
  126. package/src/test.ts +19 -0
@@ -0,0 +1,188 @@
1
+ import { v } from "convex/values";
2
+ import { internalQuery, mutation, query } from "./_generated/server.js";
3
+ import { MAX_SITES_PER_OWNER } from "./constants.js";
4
+ import { fail } from "./errors.js";
5
+ import {
6
+ cleanString,
7
+ normalizeDomains,
8
+ originAllowed,
9
+ sanitizeCurrency,
10
+ sanitizeOpaqueId,
11
+ validateTimezone,
12
+ } from "./sanitize.js";
13
+ import { siteFieldsValidator } from "./validators.js";
14
+
15
+ const siteDocumentValidator = siteFieldsValidator.extend({
16
+ _id: v.id("sites"),
17
+ _creationTime: v.number(),
18
+ });
19
+
20
+ export const create = mutation({
21
+ args: {
22
+ ownerId: v.string(),
23
+ name: v.string(),
24
+ domains: v.array(v.string()),
25
+ networkId: v.optional(v.string()),
26
+ timezone: v.optional(v.string()),
27
+ currency: v.optional(v.string()),
28
+ cookieless: v.optional(v.boolean()),
29
+ },
30
+ returns: v.id("sites"),
31
+ handler: async (ctx, args) => {
32
+ const ownerId = cleanString(args.ownerId, 128);
33
+ const name = cleanString(args.name, 120);
34
+ if (!ownerId || !name) fail("INVALID_SITE", "ownerId and name are required");
35
+
36
+ const existing = await ctx.db
37
+ .query("sites")
38
+ .withIndex("by_ownerId_and_name", (range) =>
39
+ range.eq("ownerId", ownerId).eq("name", name),
40
+ )
41
+ .unique();
42
+ if (existing) return existing._id;
43
+
44
+ const sites = await ctx.db
45
+ .query("sites")
46
+ .withIndex("by_ownerId", (range) => range.eq("ownerId", ownerId))
47
+ .take(MAX_SITES_PER_OWNER + 1);
48
+ if (sites.length >= MAX_SITES_PER_OWNER) {
49
+ fail("LIMIT_EXCEEDED", "site limit reached", {
50
+ limit: MAX_SITES_PER_OWNER,
51
+ });
52
+ }
53
+
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
+ const now = Date.now();
69
+ return await ctx.db.insert("sites", {
70
+ ownerId,
71
+ name,
72
+ domains,
73
+ networkId,
74
+ timezone,
75
+ currency,
76
+ cookieless: args.cookieless ?? true,
77
+ createdAt: now,
78
+ updatedAt: now,
79
+ });
80
+ },
81
+ });
82
+
83
+ export const update = mutation({
84
+ args: {
85
+ siteId: v.id("sites"),
86
+ name: v.optional(v.string()),
87
+ domains: v.optional(v.array(v.string())),
88
+ networkId: v.optional(v.union(v.string(), v.null())),
89
+ timezone: v.optional(v.union(v.string(), v.null())),
90
+ currency: v.optional(v.string()),
91
+ cookieless: v.optional(v.boolean()),
92
+ },
93
+ returns: v.null(),
94
+ handler: async (ctx, args) => {
95
+ const site = await ctx.db.get("sites", args.siteId);
96
+ if (!site) fail("NOT_FOUND", "site not found");
97
+
98
+ const patch: {
99
+ name?: string;
100
+ domains?: string[];
101
+ networkId?: string | undefined;
102
+ timezone?: string | undefined;
103
+ currency?: string;
104
+ cookieless?: boolean;
105
+ updatedAt: number;
106
+ } = { updatedAt: Date.now() };
107
+ if (args.name !== undefined) {
108
+ const name = cleanString(args.name, 120);
109
+ if (!name) fail("INVALID_SITE", "name is required");
110
+ const existing = await ctx.db
111
+ .query("sites")
112
+ .withIndex("by_ownerId_and_name", (range) =>
113
+ range.eq("ownerId", site.ownerId).eq("name", name),
114
+ )
115
+ .unique();
116
+ if (existing && existing._id !== site._id) {
117
+ fail("CONFLICT", "another site already uses this name");
118
+ }
119
+ patch.name = name;
120
+ }
121
+ try {
122
+ if (args.domains !== undefined) patch.domains = normalizeDomains(args.domains);
123
+ if (args.timezone !== undefined) {
124
+ patch.timezone = args.timezone === null
125
+ ? undefined
126
+ : validateTimezone(args.timezone);
127
+ }
128
+ if (args.networkId !== undefined) {
129
+ patch.networkId = args.networkId === null
130
+ ? undefined
131
+ : sanitizeOpaqueId(args.networkId, "networkId");
132
+ }
133
+ if (args.currency !== undefined) {
134
+ patch.currency = sanitizeCurrency(args.currency);
135
+ }
136
+ } catch (error) {
137
+ fail("INVALID_SITE", error instanceof Error ? error.message : "invalid site");
138
+ }
139
+ if (
140
+ patch.currency !== undefined &&
141
+ site.currency !== undefined &&
142
+ site.currency !== patch.currency
143
+ ) {
144
+ fail("CONFLICT", "site currency cannot be changed");
145
+ }
146
+ if (args.cookieless !== undefined) patch.cookieless = args.cookieless;
147
+ await ctx.db.patch("sites", args.siteId, patch);
148
+ return null;
149
+ },
150
+ });
151
+
152
+ export const get = query({
153
+ args: { siteId: v.id("sites") },
154
+ returns: v.union(siteDocumentValidator, v.null()),
155
+ handler: async (ctx, args) => {
156
+ return await ctx.db.get("sites", args.siteId);
157
+ },
158
+ });
159
+
160
+ export const listByOwner = query({
161
+ args: {
162
+ ownerId: v.string(),
163
+ limit: v.optional(v.number()),
164
+ },
165
+ returns: v.array(siteDocumentValidator),
166
+ handler: async (ctx, args) => {
167
+ const limit = args.limit ?? 50;
168
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
169
+ fail("INVALID_ARGUMENT", "limit must be an integer between 1 and 100");
170
+ }
171
+ return await ctx.db
172
+ .query("sites")
173
+ .withIndex("by_ownerId", (range) =>
174
+ range.eq("ownerId", cleanString(args.ownerId, 128)),
175
+ )
176
+ .order("desc")
177
+ .take(limit);
178
+ },
179
+ });
180
+
181
+ export const checkOrigin = internalQuery({
182
+ args: { siteId: v.id("sites"), origin: v.string() },
183
+ returns: v.boolean(),
184
+ handler: async (ctx, args) => {
185
+ const site = await ctx.db.get("sites", args.siteId);
186
+ return site !== null && originAllowed(args.origin, site.domains);
187
+ },
188
+ });
@@ -0,0 +1,226 @@
1
+ import { v, type Infer } from "convex/values";
2
+
3
+ export const eventTypeValidator = v.union(
4
+ v.literal("pageview"),
5
+ v.literal("click"),
6
+ v.literal("custom"),
7
+ v.literal("conversion"),
8
+ v.literal("heartbeat"),
9
+ v.literal("outbound"),
10
+ );
11
+
12
+ export const propertyValueValidator = v.union(
13
+ v.string(),
14
+ v.number(),
15
+ v.boolean(),
16
+ v.null(),
17
+ );
18
+
19
+ export const eventPropertiesValidator = v.record(
20
+ v.string(),
21
+ propertyValueValidator,
22
+ );
23
+
24
+ export const trackerEventValidator = v.object({
25
+ eventId: v.string(),
26
+ sessionId: v.string(),
27
+ visitorId: v.string(),
28
+ type: eventTypeValidator,
29
+ name: v.optional(v.string()),
30
+ path: v.string(),
31
+ referrer: v.optional(v.string()),
32
+ timestamp: v.number(),
33
+ sequence: v.number(),
34
+ target: v.optional(v.string()),
35
+ href: v.optional(v.string()),
36
+ properties: v.optional(eventPropertiesValidator),
37
+ revenueCents: v.optional(v.number()),
38
+ currency: v.optional(v.string()),
39
+ affiliateSlug: v.optional(v.string()),
40
+ });
41
+
42
+ export const ingestContextValidator = v.object({
43
+ country: v.optional(v.string()),
44
+ city: v.optional(v.string()),
45
+ latitude: v.optional(v.number()),
46
+ longitude: v.optional(v.number()),
47
+ browser: v.optional(v.string()),
48
+ os: v.optional(v.string()),
49
+ device: v.optional(v.string()),
50
+ });
51
+
52
+ export const siteFieldsValidator = v.object({
53
+ ownerId: v.string(),
54
+ name: v.string(),
55
+ domains: v.array(v.string()),
56
+ networkId: v.optional(v.string()),
57
+ timezone: v.optional(v.string()),
58
+ currency: v.optional(v.string()),
59
+ cookieless: v.boolean(),
60
+ createdAt: v.number(),
61
+ updatedAt: v.number(),
62
+ });
63
+
64
+ export const sessionFieldsValidator = v.object({
65
+ siteId: v.id("sites"),
66
+ sessionId: v.string(),
67
+ visitorId: v.string(),
68
+ startedAt: v.number(),
69
+ lastSeenAt: v.number(),
70
+ entryPath: v.string(),
71
+ exitPath: v.string(),
72
+ referrer: v.optional(v.string()),
73
+ source: v.string(),
74
+ utmSource: v.optional(v.string()),
75
+ utmMedium: v.optional(v.string()),
76
+ utmCampaign: v.optional(v.string()),
77
+ country: v.optional(v.string()),
78
+ city: v.optional(v.string()),
79
+ latitude: v.optional(v.number()),
80
+ longitude: v.optional(v.number()),
81
+ browser: v.string(),
82
+ os: v.string(),
83
+ device: v.string(),
84
+ eventCount: v.number(),
85
+ pageviewCount: v.number(),
86
+ durationMs: v.number(),
87
+ conversionCount: v.number(),
88
+ revenueCents: v.number(),
89
+ currency: v.optional(v.string()),
90
+ affiliateSlug: v.optional(v.string()),
91
+ affiliateId: v.optional(v.id("affiliates")),
92
+ affiliateAttributedAt: v.optional(v.number()),
93
+ affiliateExpiresAt: v.optional(v.number()),
94
+ });
95
+
96
+ export const eventFieldsValidator = v.object({
97
+ siteId: v.id("sites"),
98
+ eventId: v.string(),
99
+ sessionId: v.string(),
100
+ visitorId: v.string(),
101
+ type: eventTypeValidator,
102
+ name: v.optional(v.string()),
103
+ path: v.string(),
104
+ referrer: v.optional(v.string()),
105
+ timestamp: v.number(),
106
+ sequence: v.number(),
107
+ target: v.optional(v.string()),
108
+ href: v.optional(v.string()),
109
+ properties: v.optional(eventPropertiesValidator),
110
+ revenueCents: v.optional(v.number()),
111
+ currency: v.optional(v.string()),
112
+ affiliateSlug: v.optional(v.string()),
113
+ country: v.optional(v.string()),
114
+ city: v.optional(v.string()),
115
+ latitude: v.optional(v.number()),
116
+ longitude: v.optional(v.number()),
117
+ browser: v.string(),
118
+ os: v.string(),
119
+ device: v.string(),
120
+ });
121
+
122
+ export const dimensionTypeValidator = v.union(
123
+ v.literal("source"),
124
+ v.literal("page"),
125
+ v.literal("country"),
126
+ v.literal("device"),
127
+ v.literal("browser"),
128
+ v.literal("event"),
129
+ v.literal("goal"),
130
+ );
131
+
132
+ export const dimensionSlotValidator = v.object({
133
+ type: dimensionTypeValidator,
134
+ slot: v.number(),
135
+ value: v.string(),
136
+ count: v.number(),
137
+ revenueCents: v.number(),
138
+ });
139
+
140
+ export const aggregateFieldsValidator = v.object({
141
+ siteId: v.id("sites"),
142
+ granularity: v.union(v.literal("hour"), v.literal("day")),
143
+ bucketStart: v.number(),
144
+ shard: v.number(),
145
+ events: v.number(),
146
+ pageviews: v.number(),
147
+ clicks: v.number(),
148
+ customEvents: v.number(),
149
+ outboundClicks: v.number(),
150
+ sessions: v.number(),
151
+ visitors: v.number(),
152
+ conversions: v.number(),
153
+ revenueCents: v.number(),
154
+ dimensions: v.array(dimensionSlotValidator),
155
+ });
156
+
157
+ export const goalMatchTypeValidator = v.union(
158
+ v.literal("pageview"),
159
+ v.literal("custom"),
160
+ );
161
+
162
+ export const goalFieldsValidator = v.object({
163
+ siteId: v.id("sites"),
164
+ key: v.string(),
165
+ name: v.string(),
166
+ matchType: goalMatchTypeValidator,
167
+ matchValue: v.string(),
168
+ valueCents: v.optional(v.number()),
169
+ active: v.boolean(),
170
+ createdAt: v.number(),
171
+ updatedAt: v.number(),
172
+ });
173
+
174
+ export const funnelStepValidator = v.object({
175
+ type: v.union(v.literal("pageview"), v.literal("custom")),
176
+ value: v.string(),
177
+ name: v.string(),
178
+ });
179
+
180
+ export const funnelFieldsValidator = v.object({
181
+ siteId: v.id("sites"),
182
+ key: v.string(),
183
+ name: v.string(),
184
+ steps: v.array(funnelStepValidator),
185
+ conversionWindowMs: v.number(),
186
+ active: v.boolean(),
187
+ createdAt: v.number(),
188
+ updatedAt: v.number(),
189
+ });
190
+
191
+ export const affiliateFieldsValidator = v.object({
192
+ siteId: v.id("sites"),
193
+ slug: v.string(),
194
+ name: v.string(),
195
+ commissionBps: v.number(),
196
+ attributionWindowDays: v.number(),
197
+ active: v.boolean(),
198
+ createdAt: v.number(),
199
+ updatedAt: v.number(),
200
+ });
201
+
202
+ export const ingestResultValidator = v.object({
203
+ accepted: v.number(),
204
+ duplicates: v.number(),
205
+ rejected: v.number(),
206
+ });
207
+
208
+ export const trustedConversionValidator = v.object({
209
+ siteId: v.id("sites"),
210
+ eventId: v.string(),
211
+ sessionId: v.string(),
212
+ visitorId: v.string(),
213
+ timestamp: v.number(),
214
+ revenueCents: v.number(),
215
+ currency: v.string(),
216
+ path: v.optional(v.string()),
217
+ goalKey: v.optional(v.string()),
218
+ properties: v.optional(eventPropertiesValidator),
219
+ });
220
+
221
+
222
+ export type TrackerEvent = Infer<typeof trackerEventValidator>;
223
+ export type IngestContext = Infer<typeof ingestContextValidator>;
224
+ export type EventProperties = Infer<typeof eventPropertiesValidator>;
225
+ export type FunnelStep = Infer<typeof funnelStepValidator>;
226
+ export type EventType = Infer<typeof eventTypeValidator>;
package/src/test.ts ADDED
@@ -0,0 +1,19 @@
1
+ /// <reference types="vite/client" />
2
+ import type { TestConvex } from "convex-test";
3
+ import type { GenericSchema, SchemaDefinition } from "convex/server";
4
+ import schema from "./component/schema.js";
5
+ const modules = import.meta.glob("./component/**/*.ts");
6
+ export { schema, modules };
7
+
8
+ /**
9
+ * Register the component with the test convex instance.
10
+ * @param t - The test convex instance, e.g. from calling `convexTest`.
11
+ * @param name - The name of the component, as registered in convex.config.ts.
12
+ */
13
+ export function register(
14
+ t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
15
+ name: string = "rastroAnalytics",
16
+ ) {
17
+ t.registerComponent(name, schema, modules);
18
+ }
19
+ export default { register, schema, modules };