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

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 (164) hide show
  1. package/README.md +579 -57
  2. package/agent/integration.md +661 -0
  3. package/agent/manifest.json +187 -0
  4. package/agent/manifest.schema.json +411 -0
  5. package/dist/client/federation.d.ts +236 -0
  6. package/dist/client/federation.d.ts.map +1 -0
  7. package/dist/client/federation.js +196 -0
  8. package/dist/client/federation.js.map +1 -0
  9. package/dist/client/index.d.ts +2467 -18
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +461 -32
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +18 -0
  14. package/dist/component/_generated/api.d.ts.map +1 -1
  15. package/dist/component/_generated/api.js.map +1 -1
  16. package/dist/component/_generated/component.d.ts +302 -4
  17. package/dist/component/_generated/component.d.ts.map +1 -1
  18. package/dist/component/_generated/server.d.ts +4 -0
  19. package/dist/component/_generated/server.d.ts.map +1 -1
  20. package/dist/component/_generated/server.js.map +1 -1
  21. package/dist/component/affiliates.d.ts.map +1 -1
  22. package/dist/component/affiliates.js +6 -2
  23. package/dist/component/affiliates.js.map +1 -1
  24. package/dist/component/cardinality.d.ts +12 -0
  25. package/dist/component/cardinality.d.ts.map +1 -0
  26. package/dist/component/cardinality.js +94 -0
  27. package/dist/component/cardinality.js.map +1 -0
  28. package/dist/component/constants.d.ts +34 -7
  29. package/dist/component/constants.d.ts.map +1 -1
  30. package/dist/component/constants.js +47 -7
  31. package/dist/component/constants.js.map +1 -1
  32. package/dist/component/convex.config.d.ts +6 -1
  33. package/dist/component/convex.config.js +9 -1
  34. package/dist/component/convex.config.js.map +1 -1
  35. package/dist/component/coverage.d.ts +19 -0
  36. package/dist/component/coverage.d.ts.map +1 -0
  37. package/dist/component/coverage.js +30 -0
  38. package/dist/component/coverage.js.map +1 -0
  39. package/dist/component/diagnostics.d.ts +9 -0
  40. package/dist/component/diagnostics.d.ts.map +1 -0
  41. package/dist/component/diagnostics.js +47 -0
  42. package/dist/component/diagnostics.js.map +1 -0
  43. package/dist/component/errors.d.ts +1 -1
  44. package/dist/component/errors.d.ts.map +1 -1
  45. package/dist/component/errors.js.map +1 -1
  46. package/dist/component/eventStore.d.ts +106 -0
  47. package/dist/component/eventStore.d.ts.map +1 -0
  48. package/dist/component/eventStore.js +294 -0
  49. package/dist/component/eventStore.js.map +1 -0
  50. package/dist/component/funnels.d.ts.map +1 -1
  51. package/dist/component/funnels.js +5 -3
  52. package/dist/component/funnels.js.map +1 -1
  53. package/dist/component/geo.d.ts +71 -0
  54. package/dist/component/geo.d.ts.map +1 -0
  55. package/dist/component/geo.js +611 -0
  56. package/dist/component/geo.js.map +1 -0
  57. package/dist/component/goals.d.ts.map +1 -1
  58. package/dist/component/goals.js +25 -9
  59. package/dist/component/goals.js.map +1 -1
  60. package/dist/component/guards.d.ts.map +1 -1
  61. package/dist/component/guards.js.map +1 -1
  62. package/dist/component/http.d.ts.map +1 -1
  63. package/dist/component/http.js +284 -53
  64. package/dist/component/http.js.map +1 -1
  65. package/dist/component/identity.d.ts +13 -0
  66. package/dist/component/identity.d.ts.map +1 -0
  67. package/dist/component/identity.js +58 -0
  68. package/dist/component/identity.js.map +1 -0
  69. package/dist/component/ingest.d.ts +3 -1
  70. package/dist/component/ingest.d.ts.map +1 -1
  71. package/dist/component/ingest.js +1105 -243
  72. package/dist/component/ingest.js.map +1 -1
  73. package/dist/component/live.d.ts +8 -0
  74. package/dist/component/live.d.ts.map +1 -1
  75. package/dist/component/live.js +74 -9
  76. package/dist/component/live.js.map +1 -1
  77. package/dist/component/reports.d.ts +273 -24
  78. package/dist/component/reports.d.ts.map +1 -1
  79. package/dist/component/reports.js +1320 -150
  80. package/dist/component/reports.js.map +1 -1
  81. package/dist/component/retention.d.ts +75 -1
  82. package/dist/component/retention.d.ts.map +1 -1
  83. package/dist/component/retention.js +541 -31
  84. package/dist/component/retention.js.map +1 -1
  85. package/dist/component/sanitize.d.ts +24 -1
  86. package/dist/component/sanitize.d.ts.map +1 -1
  87. package/dist/component/sanitize.js +107 -19
  88. package/dist/component/sanitize.js.map +1 -1
  89. package/dist/component/schema.d.ts +462 -55
  90. package/dist/component/schema.js +195 -15
  91. package/dist/component/schema.js.map +1 -1
  92. package/dist/component/sites.d.ts.map +1 -1
  93. package/dist/component/sites.js +38 -25
  94. package/dist/component/sites.js.map +1 -1
  95. package/dist/component/useragent.d.ts +9 -0
  96. package/dist/component/useragent.d.ts.map +1 -0
  97. package/dist/component/useragent.js +152 -0
  98. package/dist/component/useragent.js.map +1 -0
  99. package/dist/component/validators.d.ts +101 -16
  100. package/dist/component/validators.d.ts.map +1 -1
  101. package/dist/component/validators.js +19 -2
  102. package/dist/component/validators.js.map +1 -1
  103. package/dist/component/visitors.d.ts +19 -0
  104. package/dist/component/visitors.d.ts.map +1 -0
  105. package/dist/component/visitors.js +86 -0
  106. package/dist/component/visitors.js.map +1 -0
  107. package/dist/component/vitals.d.ts +41 -0
  108. package/dist/component/vitals.d.ts.map +1 -0
  109. package/dist/component/vitals.js +115 -0
  110. package/dist/component/vitals.js.map +1 -0
  111. package/dist/react/index.d.ts.map +1 -1
  112. package/dist/react/index.js.map +1 -1
  113. package/dist/tracker/generated.d.ts +8 -4
  114. package/dist/tracker/generated.d.ts.map +1 -1
  115. package/dist/tracker/generated.js +8 -4
  116. package/dist/tracker/generated.js.map +1 -1
  117. package/dist/tracker/tracker.d.ts +1 -1
  118. package/dist/tracker/tracker.d.ts.map +1 -1
  119. package/dist/tracker/tracker.js +81 -33
  120. package/dist/tracker/tracker.js.map +1 -1
  121. package/dist/tracker/vitals.d.ts +10 -0
  122. package/dist/tracker/vitals.d.ts.map +1 -0
  123. package/dist/tracker/vitals.js +140 -0
  124. package/dist/tracker/vitals.js.map +1 -0
  125. package/dist/tracker.min.js +1 -1
  126. package/dist/vitals.min.js +1 -0
  127. package/docs/benchmarks/2026-08-20-realistic.md +171 -0
  128. package/docs/benchmarks/2026-08-21-formal-certification.md +353 -0
  129. package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
  130. package/docs/federation-setup.md +397 -0
  131. package/docs/federation.md +262 -0
  132. package/docs/upgrading.md +259 -0
  133. package/llms.txt +71 -0
  134. package/package.json +61 -11
  135. package/scripts/benchmark-ingest.mjs +601 -0
  136. package/scripts/generate-federation-keys.mjs +20 -0
  137. package/src/component/_generated/api.ts +18 -0
  138. package/src/component/_generated/component.ts +366 -4
  139. package/src/component/_generated/server.ts +4 -0
  140. package/src/component/affiliates.ts +20 -5
  141. package/src/component/cardinality.ts +117 -0
  142. package/src/component/constants.ts +47 -7
  143. package/src/component/convex.config.ts +11 -1
  144. package/src/component/coverage.ts +36 -0
  145. package/src/component/diagnostics.ts +65 -0
  146. package/src/component/errors.ts +2 -1
  147. package/src/component/eventStore.ts +489 -0
  148. package/src/component/funnels.ts +19 -16
  149. package/src/component/geo.ts +781 -0
  150. package/src/component/goals.ts +45 -16
  151. package/src/component/guards.ts +3 -1
  152. package/src/component/http.ts +403 -60
  153. package/src/component/identity.ts +74 -0
  154. package/src/component/ingest.ts +1652 -291
  155. package/src/component/live.ts +90 -9
  156. package/src/component/reports.ts +1929 -199
  157. package/src/component/retention.ts +774 -97
  158. package/src/component/sanitize.ts +154 -32
  159. package/src/component/schema.ts +219 -15
  160. package/src/component/sites.ts +56 -28
  161. package/src/component/useragent.ts +171 -0
  162. package/src/component/validators.ts +29 -1
  163. package/src/component/visitors.ts +116 -0
  164. package/src/component/vitals.ts +146 -0
@@ -1,8 +1,16 @@
1
1
  import { v } from "convex/values";
2
- import { mutation } from "./_generated/server.js";
3
- import { MAX_RETENTION_BATCH } from "./constants.js";
2
+ import { internal } from "./_generated/api.js";
3
+ import { internalMutation, mutation, query } from "./_generated/server.js";
4
+ import { DAY_MS, HOUR_MS, MAX_RETENTION_BATCH, MINUTE_MS, RATE_LIMIT_WINDOW_MS, RETENTION_LEASE_MS, RETENTION_POLICY_INTERVAL_MS, } from "./constants.js";
5
+ import { ensureAnalyticsControl } from "./coverage.js";
4
6
  import { fail } from "./errors.js";
5
- const cleanupKindValidator = v.union(v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goalCompletions"), v.literal("aggregateHour"), v.literal("aggregateDay"), v.literal("funnelProgress"), v.literal("funnelSteps"), v.literal("affiliateStats"), v.literal("attributions"), v.literal("liveSessions"), v.literal("ingestWindows"), v.literal("siteIngestWindows"));
7
+ import { deleteTelemetryBefore } from "./eventStore.js";
8
+ export const cleanupKindValidator = v.union(v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goalCompletions"), v.literal("aggregateHour"), v.literal("aggregateDay"), v.literal("funnelProgress"), v.literal("funnelSteps"), v.literal("affiliateStats"), v.literal("goalRollups"), v.literal("funnelRollups"), v.literal("affiliateRollups"), v.literal("vitalRollups"), v.literal("siteMapRollups"), v.literal("attributions"), v.literal("liveSessions"), v.literal("ingestWindows"), v.literal("siteIngestWindows"));
9
+ const cleanupResultValidator = v.object({
10
+ deleted: v.number(),
11
+ hasMore: v.boolean(),
12
+ before: v.number(),
13
+ });
6
14
  export const cleanup = mutation({
7
15
  args: {
8
16
  siteId: v.id("sites"),
@@ -10,54 +18,526 @@ export const cleanup = mutation({
10
18
  before: v.number(),
11
19
  batchSize: v.optional(v.number()),
12
20
  },
13
- returns: v.object({ deleted: v.number(), hasMore: v.boolean() }),
21
+ returns: cleanupResultValidator,
22
+ handler: async (ctx, args) => {
23
+ const policy = await ctx.db
24
+ .query("retentionPolicies")
25
+ .withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
26
+ .unique();
27
+ if (policy) {
28
+ fail("CONFLICT", "disable the retention policy before running manual cleanup");
29
+ }
30
+ const before = safeCleanupBoundary(args.kind, args.before, Date.now());
31
+ const result = await executeCleanupBatch(ctx, { ...args, before });
32
+ await persistDirectState(ctx, { ...args, before }, result);
33
+ await recordRetentionBoundary(ctx, args.siteId, args.kind, before);
34
+ return { ...result, before };
35
+ },
36
+ });
37
+ const policyValidator = v.object({
38
+ eventsDays: v.number(),
39
+ sessionsDays: v.number(),
40
+ conversionsDays: v.number(),
41
+ aggregatesDays: v.number(),
42
+ reportsDays: v.number(),
43
+ });
44
+ export const setPolicy = mutation({
45
+ args: { siteId: v.id("sites"), ...policyValidator.fields },
46
+ returns: v.null(),
47
+ handler: async (ctx, args) => {
48
+ validatePolicy(args);
49
+ const now = Date.now();
50
+ const existingPolicy = await ctx.db
51
+ .query("retentionPolicies")
52
+ .withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
53
+ .unique();
54
+ const policy = { ...args, updatedAt: now };
55
+ if (existingPolicy) {
56
+ await ctx.db.replace("retentionPolicies", existingPolicy._id, policy);
57
+ }
58
+ else {
59
+ await ctx.db.insert("retentionPolicies", policy);
60
+ }
61
+ for (const kind of POLICY_CLEANUP_KINDS) {
62
+ const requestedBefore = now - retentionDaysForKind(policy, kind) * DAY_MS;
63
+ const before = safeCleanupBoundary(kind, requestedBefore, now);
64
+ const existing = await loadState(ctx, args.siteId, kind);
65
+ const generation = (existing?.generation ?? 0) + 1;
66
+ const fields = {
67
+ siteId: args.siteId,
68
+ kind,
69
+ before,
70
+ status: "pending",
71
+ generation,
72
+ attempt: 0,
73
+ scheduledAt: now,
74
+ deleted: 0,
75
+ updatedAt: now,
76
+ };
77
+ const stateId = existing
78
+ ? (await ctx.db.replace("retentionStates", existing._id, fields),
79
+ existing._id)
80
+ : await ctx.db.insert("retentionStates", fields);
81
+ await ctx.scheduler.runAfter(0, internal.retention.runScheduled, {
82
+ stateId,
83
+ generation,
84
+ });
85
+ }
86
+ return null;
87
+ },
88
+ });
89
+ export const disablePolicy = mutation({
90
+ args: { siteId: v.id("sites") },
91
+ returns: v.null(),
92
+ handler: async (ctx, args) => {
93
+ const policy = await ctx.db
94
+ .query("retentionPolicies")
95
+ .withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
96
+ .unique();
97
+ if (policy)
98
+ await ctx.db.delete("retentionPolicies", policy._id);
99
+ const now = Date.now();
100
+ const states = await ctx.db
101
+ .query("retentionStates")
102
+ .withIndex("by_siteId_and_kind", (range) => range.eq("siteId", args.siteId))
103
+ .take(POLICY_CLEANUP_KINDS.length + 1);
104
+ for (const state of states) {
105
+ await ctx.db.patch("retentionStates", state._id, {
106
+ status: "complete",
107
+ generation: state.generation + 1,
108
+ leaseExpiresAt: undefined,
109
+ scheduledAt: undefined,
110
+ updatedAt: now,
111
+ });
112
+ }
113
+ return null;
114
+ },
115
+ });
116
+ export const runPolicy = mutation({
117
+ args: {
118
+ siteId: v.id("sites"),
119
+ kind: cleanupKindValidator,
120
+ batchSize: v.optional(v.number()),
121
+ },
122
+ returns: cleanupResultValidator,
14
123
  handler: async (ctx, args) => {
15
- if (!Number.isFinite(args.before) || args.before < 0) {
16
- fail("INVALID_ARGUMENT", "before must be a positive timestamp");
124
+ const policy = await ctx.db
125
+ .query("retentionPolicies")
126
+ .withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
127
+ .unique();
128
+ if (!policy)
129
+ fail("NOT_FOUND", "retention policy not found");
130
+ const now = Date.now();
131
+ const existing = await loadState(ctx, args.siteId, args.kind);
132
+ if (existing?.status === "running" &&
133
+ existing.leaseExpiresAt !== undefined &&
134
+ existing.leaseExpiresAt > now) {
135
+ return { deleted: 0, hasMore: true, before: existing.before };
136
+ }
137
+ const before = existing && existing.status !== "complete"
138
+ ? existing.before
139
+ : safeCleanupBoundary(args.kind, now - retentionDaysForKind(policy, args.kind) * DAY_MS, now);
140
+ const result = await executeCleanupBatch(ctx, { ...args, before });
141
+ const generation = existing?.generation ?? 1;
142
+ const fields = {
143
+ siteId: args.siteId,
144
+ kind: args.kind,
145
+ before,
146
+ status: result.hasMore ? "pending" : "complete",
147
+ generation,
148
+ attempt: existing?.attempt ?? 0,
149
+ scheduledAt: result.hasMore ? now : now + RETENTION_POLICY_INTERVAL_MS,
150
+ deleted: existing?.before === before
151
+ ? existing.deleted + result.deleted
152
+ : result.deleted,
153
+ updatedAt: now,
154
+ };
155
+ const stateId = existing
156
+ ? (await ctx.db.replace("retentionStates", existing._id, fields),
157
+ existing._id)
158
+ : await ctx.db.insert("retentionStates", fields);
159
+ await recordRetentionBoundary(ctx, args.siteId, args.kind, before);
160
+ if (result.hasMore) {
161
+ await ctx.scheduler.runAfter(0, internal.retention.runScheduled, {
162
+ stateId,
163
+ generation,
164
+ });
165
+ }
166
+ else {
167
+ await ctx.scheduler.runAt(now + RETENTION_POLICY_INTERVAL_MS, internal.retention.startScheduledCycle, { stateId, generation });
17
168
  }
18
- const batchSize = args.batchSize ?? 100;
19
- if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > MAX_RETENTION_BATCH) {
20
- fail("INVALID_ARGUMENT", `batchSize must be an integer between 1 and ${MAX_RETENTION_BATCH}`);
169
+ return { ...result, before };
170
+ },
171
+ });
172
+ export const runScheduled = internalMutation({
173
+ args: { stateId: v.id("retentionStates"), generation: v.number() },
174
+ returns: v.object({
175
+ claimed: v.boolean(),
176
+ stale: v.boolean(),
177
+ attempt: v.number(),
178
+ }),
179
+ handler: async (ctx, args) => {
180
+ const state = await ctx.db.get("retentionStates", args.stateId);
181
+ const now = Date.now();
182
+ if (!state ||
183
+ state.generation !== args.generation ||
184
+ state.status === "complete" ||
185
+ (state.status === "running" &&
186
+ state.leaseExpiresAt !== undefined &&
187
+ state.leaseExpiresAt > now)) {
188
+ return { claimed: false, stale: true, attempt: state?.attempt ?? 0 };
21
189
  }
22
- const deletions = await deletionsForCleanup(ctx, args.siteId, args.kind, args.before, batchSize + 1);
23
- for (const remove of deletions.slice(0, batchSize)) {
24
- await remove();
190
+ const attempt = state.attempt + 1;
191
+ const leaseExpiresAt = now + RETENTION_LEASE_MS;
192
+ await ctx.db.patch("retentionStates", state._id, {
193
+ status: "running",
194
+ attempt,
195
+ leaseExpiresAt,
196
+ scheduledAt: now,
197
+ updatedAt: now,
198
+ });
199
+ await ctx.scheduler.runAfter(0, internal.retention.executeScheduled, {
200
+ stateId: state._id,
201
+ generation: state.generation,
202
+ attempt,
203
+ });
204
+ await ctx.scheduler.runAt(leaseExpiresAt, internal.retention.runScheduled, {
205
+ stateId: state._id,
206
+ generation: state.generation,
207
+ });
208
+ return { claimed: true, stale: false, attempt };
209
+ },
210
+ });
211
+ export const executeScheduled = internalMutation({
212
+ args: {
213
+ stateId: v.id("retentionStates"),
214
+ generation: v.number(),
215
+ attempt: v.number(),
216
+ },
217
+ returns: v.object({
218
+ deleted: v.number(),
219
+ hasMore: v.boolean(),
220
+ stale: v.boolean(),
221
+ }),
222
+ handler: async (ctx, args) => {
223
+ const state = await ctx.db.get("retentionStates", args.stateId);
224
+ if (!state ||
225
+ state.generation !== args.generation ||
226
+ state.attempt !== args.attempt ||
227
+ state.status !== "running") {
228
+ return { deleted: 0, hasMore: false, stale: true };
229
+ }
230
+ const result = await executeCleanupBatch(ctx, {
231
+ siteId: state.siteId,
232
+ kind: state.kind,
233
+ before: state.before,
234
+ batchSize: 100,
235
+ });
236
+ const now = Date.now();
237
+ const policy = result.hasMore
238
+ ? null
239
+ : await ctx.db
240
+ .query("retentionPolicies")
241
+ .withIndex("by_siteId", (range) => range.eq("siteId", state.siteId))
242
+ .unique();
243
+ const nextCycleAt = policy ? now + RETENTION_POLICY_INTERVAL_MS : undefined;
244
+ await ctx.db.patch("retentionStates", state._id, {
245
+ status: result.hasMore ? "pending" : "complete",
246
+ leaseExpiresAt: undefined,
247
+ scheduledAt: result.hasMore ? now : nextCycleAt,
248
+ deleted: state.deleted + result.deleted,
249
+ updatedAt: now,
250
+ });
251
+ await recordRetentionBoundary(ctx, state.siteId, state.kind, state.before);
252
+ if (result.hasMore) {
253
+ await ctx.scheduler.runAfter(0, internal.retention.runScheduled, {
254
+ stateId: state._id,
255
+ generation: state.generation,
256
+ });
257
+ }
258
+ else if (nextCycleAt !== undefined) {
259
+ await ctx.scheduler.runAt(nextCycleAt, internal.retention.startScheduledCycle, { stateId: state._id, generation: state.generation });
260
+ }
261
+ return { ...result, stale: false };
262
+ },
263
+ });
264
+ export const startScheduledCycle = internalMutation({
265
+ args: { stateId: v.id("retentionStates"), generation: v.number() },
266
+ returns: v.object({
267
+ started: v.boolean(),
268
+ stale: v.boolean(),
269
+ before: v.union(v.number(), v.null()),
270
+ }),
271
+ handler: async (ctx, args) => {
272
+ const state = await ctx.db.get("retentionStates", args.stateId);
273
+ const now = Date.now();
274
+ if (!state ||
275
+ state.generation !== args.generation ||
276
+ state.status !== "complete" ||
277
+ (state.scheduledAt !== undefined && state.scheduledAt > now)) {
278
+ return { started: false, stale: true, before: null };
25
279
  }
280
+ const policy = await ctx.db
281
+ .query("retentionPolicies")
282
+ .withIndex("by_siteId", (range) => range.eq("siteId", state.siteId))
283
+ .unique();
284
+ if (!policy)
285
+ return { started: false, stale: true, before: null };
286
+ const kind = state.kind;
287
+ const before = safeCleanupBoundary(kind, now - retentionDaysForKind(policy, kind) * DAY_MS, now);
288
+ await ctx.db.patch("retentionStates", state._id, {
289
+ before,
290
+ status: "pending",
291
+ attempt: 0,
292
+ leaseExpiresAt: undefined,
293
+ scheduledAt: now,
294
+ deleted: 0,
295
+ updatedAt: now,
296
+ });
297
+ await ctx.scheduler.runAfter(0, internal.retention.runScheduled, {
298
+ stateId: state._id,
299
+ generation: state.generation,
300
+ });
301
+ return { started: true, stale: false, before };
302
+ },
303
+ });
304
+ export const status = query({
305
+ args: { siteId: v.id("sites") },
306
+ returns: v.object({
307
+ policy: v.union(policyValidator.extend({ updatedAt: v.number() }), v.null()),
308
+ jobs: v.array(v.object({
309
+ kind: cleanupKindValidator,
310
+ before: v.number(),
311
+ status: v.union(v.literal("pending"), v.literal("running"), v.literal("complete")),
312
+ deleted: v.number(),
313
+ scheduledAt: v.union(v.number(), v.null()),
314
+ updatedAt: v.number(),
315
+ })),
316
+ backlog: v.number(),
317
+ }),
318
+ handler: async (ctx, args) => {
319
+ const policy = await ctx.db
320
+ .query("retentionPolicies")
321
+ .withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
322
+ .unique();
323
+ const jobs = await ctx.db
324
+ .query("retentionStates")
325
+ .withIndex("by_siteId_and_kind", (range) => range.eq("siteId", args.siteId))
326
+ .take(POLICY_CLEANUP_KINDS.length + 1);
327
+ const publicJobs = jobs.map(({ kind, before, status, deleted, scheduledAt, updatedAt }) => ({
328
+ kind: kind,
329
+ before,
330
+ status,
331
+ deleted,
332
+ scheduledAt: scheduledAt ?? null,
333
+ updatedAt,
334
+ }));
26
335
  return {
27
- deleted: Math.min(deletions.length, batchSize),
28
- hasMore: deletions.length > batchSize,
336
+ policy: policy
337
+ ? {
338
+ eventsDays: policy.eventsDays,
339
+ sessionsDays: policy.sessionsDays,
340
+ conversionsDays: policy.conversionsDays,
341
+ aggregatesDays: policy.aggregatesDays,
342
+ reportsDays: policy.reportsDays,
343
+ updatedAt: policy.updatedAt,
344
+ }
345
+ : null,
346
+ jobs: publicJobs,
347
+ backlog: publicJobs.filter((job) => job.status !== "complete").length,
29
348
  };
30
349
  },
31
350
  });
32
- async function deletionsForCleanup(ctx, siteId, kind, before, limit) {
33
- if (kind === "events") {
34
- return (await ctx.db
35
- .query("events")
36
- .withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId).lt("timestamp", before))
37
- .take(limit)).map((row) => () => ctx.db.delete("events", row._id));
351
+ const POLICY_CLEANUP_KINDS = [
352
+ "events",
353
+ "sessions",
354
+ "conversions",
355
+ "aggregateHour",
356
+ "aggregateDay",
357
+ "goalCompletions",
358
+ "funnelProgress",
359
+ "funnelSteps",
360
+ "affiliateStats",
361
+ "goalRollups",
362
+ "funnelRollups",
363
+ "affiliateRollups",
364
+ "vitalRollups",
365
+ "siteMapRollups",
366
+ "attributions",
367
+ "ingestWindows",
368
+ "siteIngestWindows",
369
+ ];
370
+ function validatePolicy(policy) {
371
+ for (const [name, days] of Object.entries(policy)) {
372
+ if (!name.endsWith("Days"))
373
+ continue;
374
+ if (!Number.isSafeInteger(days) || days < 1 || days > 3_650) {
375
+ fail("INVALID_ARGUMENT", `${name} must be an integer between 1 and 3650`);
376
+ }
38
377
  }
378
+ }
379
+ function retentionDaysForKind(policy, kind) {
380
+ if (kind === "events")
381
+ return policy.eventsDays;
382
+ if (kind === "sessions")
383
+ return policy.sessionsDays;
384
+ if (kind === "conversions")
385
+ return policy.conversionsDays;
386
+ if (kind === "aggregateHour" || kind === "aggregateDay") {
387
+ return policy.aggregatesDays;
388
+ }
389
+ return policy.reportsDays;
390
+ }
391
+ function safeCleanupBoundary(kind, requested, now) {
392
+ if (!Number.isFinite(requested) || requested < 0) {
393
+ fail("INVALID_ARGUMENT", "before must be a positive timestamp");
394
+ }
395
+ if (kind === "events")
396
+ return safeBucketBoundary(requested, now, MINUTE_MS);
397
+ if (kind === "aggregateHour")
398
+ return safeBucketBoundary(requested, now, HOUR_MS);
399
+ if (kind === "aggregateDay" ||
400
+ kind === "funnelSteps" ||
401
+ kind === "affiliateStats" ||
402
+ kind === "goalRollups" ||
403
+ kind === "funnelRollups" ||
404
+ kind === "affiliateRollups" ||
405
+ kind === "vitalRollups" ||
406
+ kind === "siteMapRollups") {
407
+ return safeBucketBoundary(requested, now, DAY_MS);
408
+ }
409
+ if (kind === "ingestWindows" || kind === "siteIngestWindows") {
410
+ return Math.min(Math.floor(requested / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS, Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS);
411
+ }
412
+ return Math.trunc(requested);
413
+ }
414
+ function safeBucketBoundary(requested, now, intervalMs) {
415
+ return Math.min(Math.floor(requested / intervalMs) * intervalMs, Math.floor(now / intervalMs) * intervalMs);
416
+ }
417
+ async function loadState(ctx, siteId, kind) {
418
+ return ctx.db
419
+ .query("retentionStates")
420
+ .withIndex("by_siteId_and_kind", (range) => range.eq("siteId", siteId).eq("kind", kind))
421
+ .unique();
422
+ }
423
+ async function persistDirectState(ctx, args, result) {
424
+ const now = Date.now();
425
+ const existing = await loadState(ctx, args.siteId, args.kind);
426
+ const fields = {
427
+ siteId: args.siteId,
428
+ kind: args.kind,
429
+ before: args.before,
430
+ status: result.hasMore ? "pending" : "complete",
431
+ generation: (existing?.generation ?? 0) + 1,
432
+ attempt: 0,
433
+ deleted: existing?.before === args.before
434
+ ? existing.deleted + result.deleted
435
+ : result.deleted,
436
+ updatedAt: now,
437
+ };
438
+ if (existing) {
439
+ await ctx.db.replace("retentionStates", existing._id, fields);
440
+ }
441
+ else {
442
+ await ctx.db.insert("retentionStates", fields);
443
+ }
444
+ }
445
+ async function recordRetentionBoundary(ctx, siteId, kind, before) {
446
+ const datasets = coverageDatasetsForKind(kind);
447
+ if (datasets.length === 0)
448
+ return;
449
+ const now = Date.now();
450
+ const coverage = await ensureAnalyticsControl(ctx, siteId, now, datasets.map((dataset) => ({ dataset, before })));
451
+ const retained = coverage.retained.filter((item) => !datasets.includes(item.dataset));
452
+ let changed = retained.length !== coverage.retained.length;
453
+ for (const dataset of datasets) {
454
+ const prior = coverage.retained.find((item) => item.dataset === dataset)?.before ?? 0;
455
+ const next = Math.max(prior, before);
456
+ retained.push({ dataset, before: next });
457
+ changed ||= next !== prior;
458
+ }
459
+ if (changed) {
460
+ await ctx.db.patch("analyticsCoverage", coverage._id, {
461
+ retained,
462
+ updatedAt: now,
463
+ });
464
+ }
465
+ }
466
+ function coverageDatasetsForKind(kind) {
467
+ if (kind === "events")
468
+ return ["events"];
469
+ if (kind === "sessions")
470
+ return ["sessions"];
471
+ if (kind === "conversions")
472
+ return ["conversions"];
473
+ if (kind === "aggregateHour")
474
+ return ["overviewHour"];
475
+ if (kind === "aggregateDay")
476
+ return ["overviewDay"];
477
+ if (kind === "goalCompletions")
478
+ return ["goalCompletions"];
479
+ if (kind === "funnelSteps")
480
+ return ["funnelSteps"];
481
+ if (kind === "affiliateStats")
482
+ return ["affiliateStats"];
483
+ if (kind === "goalRollups")
484
+ return ["goalRollups"];
485
+ if (kind === "funnelRollups")
486
+ return ["funnelRollups"];
487
+ if (kind === "affiliateRollups")
488
+ return ["affiliateRollups"];
489
+ if (kind === "vitalRollups")
490
+ return ["vitalRollups"];
491
+ if (kind === "siteMapRollups")
492
+ return ["siteMapRollups"];
493
+ return [];
494
+ }
495
+ async function executeCleanupBatch(ctx, args) {
496
+ const batchSize = args.batchSize ?? 100;
497
+ if (!Number.isSafeInteger(batchSize) ||
498
+ batchSize < 1 ||
499
+ batchSize > MAX_RETENTION_BATCH) {
500
+ fail("INVALID_ARGUMENT", `batchSize must be an integer between 1 and ${MAX_RETENTION_BATCH}`);
501
+ }
502
+ const effectiveBatchSize = args.kind === "events" ? Math.min(batchSize, 32) : batchSize;
503
+ if (args.kind === "events") {
504
+ return deleteTelemetryBefore(ctx, {
505
+ siteId: args.siteId,
506
+ before: args.before,
507
+ batchSize: effectiveBatchSize,
508
+ });
509
+ }
510
+ const deletions = await deletionsForCleanup(ctx, args.siteId, args.kind, args.before, effectiveBatchSize + 1);
511
+ for (const remove of deletions.slice(0, effectiveBatchSize))
512
+ await remove();
513
+ return {
514
+ deleted: Math.min(deletions.length, effectiveBatchSize),
515
+ hasMore: deletions.length > effectiveBatchSize,
516
+ };
517
+ }
518
+ async function deletionsForCleanup(ctx, siteId, kind, before, limit) {
39
519
  if (kind === "sessions") {
40
520
  return (await ctx.db
41
521
  .query("sessions")
42
- .withIndex("by_siteId_and_lastSeenAt", (range) => range.eq("siteId", siteId).lt("lastSeenAt", before))
522
+ .withIndex("by_siteId_and_lastSeenAt", (q) => q.eq("siteId", siteId).lt("lastSeenAt", before))
43
523
  .take(limit)).map((row) => () => ctx.db.delete("sessions", row._id));
44
524
  }
45
525
  if (kind === "conversions") {
46
526
  return (await ctx.db
47
527
  .query("conversions")
48
- .withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId).lt("timestamp", before))
528
+ .withIndex("by_siteId_and_timestamp", (q) => q.eq("siteId", siteId).lt("timestamp", before))
49
529
  .take(limit)).map((row) => () => ctx.db.delete("conversions", row._id));
50
530
  }
51
531
  if (kind === "goalCompletions") {
52
532
  return (await ctx.db
53
533
  .query("goalCompletions")
54
- .withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", siteId).lt("timestamp", before))
534
+ .withIndex("by_siteId_and_timestamp", (q) => q.eq("siteId", siteId).lt("timestamp", before))
55
535
  .take(limit)).map((row) => () => ctx.db.delete("goalCompletions", row._id));
56
536
  }
57
537
  if (kind === "aggregateHour" || kind === "aggregateDay") {
58
538
  return (await ctx.db
59
539
  .query("aggregateBuckets")
60
- .withIndex("by_siteId_and_granularity_and_bucketStart", (range) => range
540
+ .withIndex("by_siteId_and_granularity_and_bucketStart", (q) => q
61
541
  .eq("siteId", siteId)
62
542
  .eq("granularity", kind === "aggregateHour" ? "hour" : "day")
63
543
  .lt("bucketStart", before))
@@ -66,42 +546,72 @@ async function deletionsForCleanup(ctx, siteId, kind, before, limit) {
66
546
  if (kind === "funnelProgress") {
67
547
  return (await ctx.db
68
548
  .query("funnelProgress")
69
- .withIndex("by_siteId_and_lastStepAt", (range) => range.eq("siteId", siteId).lt("lastStepAt", before))
549
+ .withIndex("by_siteId_and_lastStepAt", (q) => q.eq("siteId", siteId).lt("lastStepAt", before))
70
550
  .take(limit)).map((row) => () => ctx.db.delete("funnelProgress", row._id));
71
551
  }
72
552
  if (kind === "funnelSteps") {
73
553
  return (await ctx.db
74
554
  .query("funnelStepBuckets")
75
- .withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId).lt("bucketStart", before))
555
+ .withIndex("by_siteId_and_bucketStart", (q) => q.eq("siteId", siteId).lt("bucketStart", before))
76
556
  .take(limit)).map((row) => () => ctx.db.delete("funnelStepBuckets", row._id));
77
557
  }
78
558
  if (kind === "affiliateStats") {
79
559
  return (await ctx.db
80
560
  .query("affiliateDailyStats")
81
- .withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", siteId).lt("bucketStart", before))
561
+ .withIndex("by_siteId_and_bucketStart", (q) => q.eq("siteId", siteId).lt("bucketStart", before))
82
562
  .take(limit)).map((row) => () => ctx.db.delete("affiliateDailyStats", row._id));
83
563
  }
564
+ if (kind === "goalRollups") {
565
+ return (await ctx.db
566
+ .query("goalDailyRollups")
567
+ .withIndex("by_siteId_and_bucketStart", (q) => q.eq("siteId", siteId).lt("bucketStart", before))
568
+ .take(limit)).map((row) => () => ctx.db.delete("goalDailyRollups", row._id));
569
+ }
570
+ if (kind === "funnelRollups") {
571
+ return (await ctx.db
572
+ .query("funnelDailyRollups")
573
+ .withIndex("by_siteId_and_bucketStart", (q) => q.eq("siteId", siteId).lt("bucketStart", before))
574
+ .take(limit)).map((row) => () => ctx.db.delete("funnelDailyRollups", row._id));
575
+ }
576
+ if (kind === "affiliateRollups") {
577
+ return (await ctx.db
578
+ .query("affiliateDailyRollups")
579
+ .withIndex("by_siteId_and_bucketStart", (q) => q.eq("siteId", siteId).lt("bucketStart", before))
580
+ .take(limit)).map((row) => () => ctx.db.delete("affiliateDailyRollups", row._id));
581
+ }
582
+ if (kind === "vitalRollups") {
583
+ return (await ctx.db
584
+ .query("vitalRollups")
585
+ .withIndex("by_siteId_and_bucketStart", (q) => q.eq("siteId", siteId).lt("bucketStart", before))
586
+ .take(limit)).map((row) => () => ctx.db.delete("vitalRollups", row._id));
587
+ }
588
+ if (kind === "siteMapRollups") {
589
+ return (await ctx.db
590
+ .query("routeDailyRollups")
591
+ .withIndex("by_siteId_and_bucketStart", (q) => q.eq("siteId", siteId).lt("bucketStart", before))
592
+ .take(limit)).map((row) => () => ctx.db.delete("routeDailyRollups", row._id));
593
+ }
84
594
  if (kind === "attributions") {
85
595
  return (await ctx.db
86
596
  .query("visitorAttributions")
87
- .withIndex("by_siteId_and_expiresAt", (range) => range.eq("siteId", siteId).lt("expiresAt", before))
597
+ .withIndex("by_siteId_and_expiresAt", (q) => q.eq("siteId", siteId).lt("expiresAt", before))
88
598
  .take(limit)).map((row) => () => ctx.db.delete("visitorAttributions", row._id));
89
599
  }
90
600
  if (kind === "liveSessions") {
91
601
  return (await ctx.db
92
602
  .query("liveSessions")
93
- .withIndex("by_siteId_and_expiresAt", (range) => range.eq("siteId", siteId).lt("expiresAt", before))
603
+ .withIndex("by_siteId_and_expiresAt", (q) => q.eq("siteId", siteId).lt("expiresAt", before))
94
604
  .take(limit)).map((row) => () => ctx.db.delete("liveSessions", row._id));
95
605
  }
96
606
  if (kind === "siteIngestWindows") {
97
607
  return (await ctx.db
98
608
  .query("siteIngestWindows")
99
- .withIndex("by_siteId_and_updatedAt", (range) => range.eq("siteId", siteId).lt("updatedAt", before))
609
+ .withIndex("by_siteId_and_updatedAt", (q) => q.eq("siteId", siteId).lt("updatedAt", before))
100
610
  .take(limit)).map((row) => () => ctx.db.delete("siteIngestWindows", row._id));
101
611
  }
102
612
  return (await ctx.db
103
613
  .query("ingestWindows")
104
- .withIndex("by_siteId_and_updatedAt", (range) => range.eq("siteId", siteId).lt("updatedAt", before))
614
+ .withIndex("by_siteId_and_updatedAt", (q) => q.eq("siteId", siteId).lt("updatedAt", before))
105
615
  .take(limit)).map((row) => () => ctx.db.delete("ingestWindows", row._id));
106
616
  }
107
617
  //# sourceMappingURL=retention.js.map