@iann29/rastro 0.1.0-alpha.2 → 0.1.0-alpha.4

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 (124) hide show
  1. package/README.md +325 -63
  2. package/agent/integration.md +651 -0
  3. package/agent/manifest.json +183 -0
  4. package/agent/manifest.schema.json +405 -0
  5. package/dist/client/federation.d.ts +205 -0
  6. package/dist/client/federation.d.ts.map +1 -0
  7. package/dist/client/federation.js +179 -0
  8. package/dist/client/federation.js.map +1 -0
  9. package/dist/client/index.d.ts +1600 -8
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +210 -2
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +9 -5
  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 +161 -1
  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/cardinality.d.ts +12 -0
  22. package/dist/component/cardinality.d.ts.map +1 -0
  23. package/dist/component/cardinality.js +94 -0
  24. package/dist/component/cardinality.js.map +1 -0
  25. package/dist/component/constants.d.ts +10 -0
  26. package/dist/component/constants.d.ts.map +1 -1
  27. package/dist/component/constants.js +10 -0
  28. package/dist/component/constants.js.map +1 -1
  29. package/dist/component/convex.config.d.ts +7 -2
  30. package/dist/component/convex.config.d.ts.map +1 -1
  31. package/dist/component/convex.config.js +9 -4
  32. package/dist/component/convex.config.js.map +1 -1
  33. package/dist/component/coverage.d.ts +18 -0
  34. package/dist/component/coverage.d.ts.map +1 -0
  35. package/dist/component/coverage.js +19 -0
  36. package/dist/component/coverage.js.map +1 -0
  37. package/dist/component/diagnostics.d.ts +9 -0
  38. package/dist/component/diagnostics.d.ts.map +1 -0
  39. package/dist/component/diagnostics.js +47 -0
  40. package/dist/component/diagnostics.js.map +1 -0
  41. package/dist/component/errors.d.ts +1 -1
  42. package/dist/component/errors.d.ts.map +1 -1
  43. package/dist/component/errors.js.map +1 -1
  44. package/dist/component/eventStore.d.ts +11 -9
  45. package/dist/component/eventStore.d.ts.map +1 -1
  46. package/dist/component/eventStore.js +55 -227
  47. package/dist/component/eventStore.js.map +1 -1
  48. package/dist/component/geo.d.ts +71 -0
  49. package/dist/component/geo.d.ts.map +1 -0
  50. package/dist/component/geo.js +610 -0
  51. package/dist/component/geo.js.map +1 -0
  52. package/dist/component/http.d.ts.map +1 -1
  53. package/dist/component/http.js +168 -17
  54. package/dist/component/http.js.map +1 -1
  55. package/dist/component/ingest.d.ts.map +1 -1
  56. package/dist/component/ingest.js +272 -32
  57. package/dist/component/ingest.js.map +1 -1
  58. package/dist/component/live.d.ts.map +1 -1
  59. package/dist/component/live.js +6 -3
  60. package/dist/component/live.js.map +1 -1
  61. package/dist/component/reports.d.ts +117 -8
  62. package/dist/component/reports.d.ts.map +1 -1
  63. package/dist/component/reports.js +641 -79
  64. package/dist/component/reports.js.map +1 -1
  65. package/dist/component/retention.d.ts +75 -1
  66. package/dist/component/retention.d.ts.map +1 -1
  67. package/dist/component/retention.js +517 -54
  68. package/dist/component/retention.js.map +1 -1
  69. package/dist/component/sanitize.d.ts +4 -1
  70. package/dist/component/sanitize.d.ts.map +1 -1
  71. package/dist/component/sanitize.js +9 -4
  72. package/dist/component/sanitize.js.map +1 -1
  73. package/dist/component/schema.d.ts +202 -53
  74. package/dist/component/schema.js +113 -16
  75. package/dist/component/schema.js.map +1 -1
  76. package/dist/component/sites.d.ts.map +1 -1
  77. package/dist/component/sites.js +5 -1
  78. package/dist/component/sites.js.map +1 -1
  79. package/dist/component/validators.d.ts +29 -51
  80. package/dist/component/validators.d.ts.map +1 -1
  81. package/dist/component/validators.js +11 -12
  82. package/dist/component/validators.js.map +1 -1
  83. package/dist/tracker/generated.d.ts +4 -4
  84. package/dist/tracker/generated.d.ts.map +1 -1
  85. package/dist/tracker/generated.js +4 -4
  86. package/dist/tracker/generated.js.map +1 -1
  87. package/dist/tracker/tracker.js +7 -6
  88. package/dist/tracker/tracker.js.map +1 -1
  89. package/dist/tracker.min.js +1 -1
  90. package/docs/benchmarks/2026-08-20-realistic.md +5 -5
  91. package/docs/benchmarks/2026-08-21-formal-certification.md +333 -0
  92. package/docs/federation-setup.md +395 -0
  93. package/docs/federation.md +258 -0
  94. package/docs/upgrading.md +130 -0
  95. package/llms.txt +65 -0
  96. package/package.json +29 -7
  97. package/scripts/benchmark-ingest.mjs +81 -32
  98. package/scripts/generate-federation-keys.mjs +20 -0
  99. package/src/component/_generated/api.ts +9 -5
  100. package/src/component/_generated/component.ts +224 -1
  101. package/src/component/_generated/server.ts +4 -0
  102. package/src/component/cardinality.ts +116 -0
  103. package/src/component/constants.ts +10 -0
  104. package/src/component/convex.config.ts +11 -5
  105. package/src/component/coverage.ts +25 -0
  106. package/src/component/diagnostics.ts +64 -0
  107. package/src/component/errors.ts +2 -1
  108. package/src/component/eventStore.ts +78 -280
  109. package/src/component/geo.ts +779 -0
  110. package/src/component/http.ts +259 -22
  111. package/src/component/ingest.ts +436 -27
  112. package/src/component/live.ts +8 -3
  113. package/src/component/reports.ts +895 -87
  114. package/src/component/retention.ts +624 -88
  115. package/src/component/sanitize.ts +15 -4
  116. package/src/component/schema.ts +125 -16
  117. package/src/component/sites.ts +5 -1
  118. package/src/component/validators.ts +12 -13
  119. package/src/test.ts +0 -2
  120. package/dist/component/migrations.d.ts +0 -17
  121. package/dist/component/migrations.d.ts.map +0 -1
  122. package/dist/component/migrations.js +0 -44
  123. package/dist/component/migrations.js.map +0 -1
  124. package/src/component/migrations.ts +0 -52
@@ -65,6 +65,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
65
65
  Name
66
66
  >;
67
67
  };
68
+ diagnostics: {
69
+ sessionEventTotals: FunctionReference<
70
+ "query",
71
+ "internal",
72
+ { sessionIds: Array<string>; siteId: string },
73
+ {
74
+ foundSessions: number;
75
+ requestedSessions: number;
76
+ storedEvents: number;
77
+ },
78
+ Name
79
+ >;
80
+ };
68
81
  funnels: {
69
82
  list: FunctionReference<
70
83
  "query",
@@ -244,10 +257,59 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
244
257
  updatedAt: number;
245
258
  visits: number;
246
259
  }>;
260
+ metadata: {
261
+ basis: "dailyRollup" | "boundedLegacy";
262
+ coverage: {
263
+ availableFrom: number | null;
264
+ availableThrough: number | null;
265
+ generation: number | null;
266
+ retainedBefore: number | null;
267
+ state: "complete" | "partial" | "retained" | "unavailable";
268
+ };
269
+ range: {
270
+ boundary: "inclusive";
271
+ from: number;
272
+ timezone: "UTC";
273
+ to: number;
274
+ };
275
+ uniqueVisitors: {
276
+ basis: "estimatedUnique" | "exactLegacy";
277
+ exact: boolean;
278
+ };
279
+ };
247
280
  truncated: boolean;
248
281
  },
249
282
  Name
250
283
  >;
284
+ dataCoverage: FunctionReference<
285
+ "query",
286
+ "internal",
287
+ { from: number; siteId: string; to: number },
288
+ {
289
+ datasets: Array<{
290
+ coverage: {
291
+ availableFrom: number | null;
292
+ availableThrough: number | null;
293
+ generation: number | null;
294
+ retainedBefore: number | null;
295
+ state: "complete" | "partial" | "retained" | "unavailable";
296
+ };
297
+ dataset:
298
+ | "overviewHour"
299
+ | "overviewDay"
300
+ | "events"
301
+ | "sessions"
302
+ | "conversions"
303
+ | "goals"
304
+ | "funnels"
305
+ | "affiliates";
306
+ }>;
307
+ from: number;
308
+ siteId: string;
309
+ to: number;
310
+ },
311
+ Name
312
+ >;
251
313
  funnelsReport: FunctionReference<
252
314
  "query",
253
315
  "internal",
@@ -259,6 +321,22 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
259
321
  conversionWindowMs: number;
260
322
  createdAt: number;
261
323
  key: string;
324
+ metadata: {
325
+ basis: "dailyRollup" | "boundedLegacy";
326
+ coverage: {
327
+ availableFrom: number | null;
328
+ availableThrough: number | null;
329
+ generation: number | null;
330
+ retainedBefore: number | null;
331
+ state: "complete" | "partial" | "retained" | "unavailable";
332
+ };
333
+ range: {
334
+ boundary: "inclusive";
335
+ from: number;
336
+ timezone: "UTC";
337
+ to: number;
338
+ };
339
+ };
262
340
  name: string;
263
341
  siteId: string;
264
342
  steps: Array<{
@@ -296,6 +374,22 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
296
374
  updatedAt: number;
297
375
  valueCents: number;
298
376
  }>;
377
+ metadata: {
378
+ basis: "dailyRollup" | "boundedLegacy";
379
+ coverage: {
380
+ availableFrom: number | null;
381
+ availableThrough: number | null;
382
+ generation: number | null;
383
+ retainedBefore: number | null;
384
+ state: "complete" | "partial" | "retained" | "unavailable";
385
+ };
386
+ range: {
387
+ boundary: "inclusive";
388
+ from: number;
389
+ timezone: "UTC";
390
+ to: number;
391
+ };
392
+ };
299
393
  truncated: boolean;
300
394
  },
301
395
  Name
@@ -440,6 +534,43 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
440
534
  currency: string;
441
535
  from: number;
442
536
  interval: "hour" | "day";
537
+ metadata: {
538
+ breakdowns: {
539
+ topBrowsers: "eventVolume";
540
+ topCountries: "eventVolume";
541
+ topDevices: "eventVolume";
542
+ topEvents: "eventVolume";
543
+ topGoals: "goalCompletionVolume";
544
+ topPages: "eventVolume";
545
+ topSources: "eventVolume";
546
+ };
547
+ coverage: {
548
+ availableFrom: number | null;
549
+ availableThrough: number | null;
550
+ generation: number | null;
551
+ retainedBefore: number | null;
552
+ state: "complete" | "partial" | "retained" | "unavailable";
553
+ };
554
+ features: {
555
+ botDetection: "unsupported";
556
+ errorInsights: "unsupported";
557
+ outboundLinks: "eventVolume";
558
+ sessionReplay: "unsupported";
559
+ };
560
+ range: {
561
+ boundary: "inclusive";
562
+ from: number;
563
+ timezone: "UTC";
564
+ to: number;
565
+ };
566
+ visitors: {
567
+ basis: "estimatedUnique" | "bucketOccurrences";
568
+ exact: boolean;
569
+ legacyIncomplete: boolean;
570
+ precision: number | null;
571
+ sketchVersion: number | null;
572
+ };
573
+ };
443
574
  timeSeries: Array<{
444
575
  clicks: number;
445
576
  conversions: number;
@@ -623,13 +754,105 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
623
754
  | "funnelProgress"
624
755
  | "funnelSteps"
625
756
  | "affiliateStats"
757
+ | "goalRollups"
758
+ | "funnelRollups"
759
+ | "affiliateRollups"
760
+ | "attributions"
761
+ | "liveSessions"
762
+ | "ingestWindows"
763
+ | "siteIngestWindows";
764
+ siteId: string;
765
+ },
766
+ { before: number; deleted: number; hasMore: boolean },
767
+ Name
768
+ >;
769
+ disablePolicy: FunctionReference<
770
+ "mutation",
771
+ "internal",
772
+ { siteId: string },
773
+ null,
774
+ Name
775
+ >;
776
+ runPolicy: FunctionReference<
777
+ "mutation",
778
+ "internal",
779
+ {
780
+ batchSize?: number;
781
+ kind:
782
+ | "events"
783
+ | "sessions"
784
+ | "conversions"
785
+ | "goalCompletions"
786
+ | "aggregateHour"
787
+ | "aggregateDay"
788
+ | "funnelProgress"
789
+ | "funnelSteps"
790
+ | "affiliateStats"
791
+ | "goalRollups"
792
+ | "funnelRollups"
793
+ | "affiliateRollups"
626
794
  | "attributions"
627
795
  | "liveSessions"
628
796
  | "ingestWindows"
629
797
  | "siteIngestWindows";
630
798
  siteId: string;
631
799
  },
632
- { deleted: number; hasMore: boolean },
800
+ { before: number; deleted: number; hasMore: boolean },
801
+ Name
802
+ >;
803
+ setPolicy: FunctionReference<
804
+ "mutation",
805
+ "internal",
806
+ {
807
+ aggregatesDays: number;
808
+ conversionsDays: number;
809
+ eventsDays: number;
810
+ reportsDays: number;
811
+ sessionsDays: number;
812
+ siteId: string;
813
+ },
814
+ null,
815
+ Name
816
+ >;
817
+ status: FunctionReference<
818
+ "query",
819
+ "internal",
820
+ { siteId: string },
821
+ {
822
+ backlog: number;
823
+ jobs: Array<{
824
+ before: number;
825
+ deleted: number;
826
+ kind:
827
+ | "events"
828
+ | "sessions"
829
+ | "conversions"
830
+ | "goalCompletions"
831
+ | "aggregateHour"
832
+ | "aggregateDay"
833
+ | "funnelProgress"
834
+ | "funnelSteps"
835
+ | "affiliateStats"
836
+ | "goalRollups"
837
+ | "funnelRollups"
838
+ | "affiliateRollups"
839
+ | "attributions"
840
+ | "liveSessions"
841
+ | "ingestWindows"
842
+ | "siteIngestWindows";
843
+ scheduledAt: number | null;
844
+ status: "pending" | "running" | "complete";
845
+ updatedAt: number;
846
+ }>;
847
+ policy: {
848
+ aggregatesDays: number;
849
+ conversionsDays: number;
850
+ eventsDays: number;
851
+ reportsDays: number;
852
+ sessionsDays: number;
853
+ updatedAt: number;
854
+ } | null;
855
+ },
633
856
  Name
634
857
  >;
635
858
  };
@@ -39,6 +39,10 @@ import type { DataModel } from "./dataModel.js";
39
39
  type Env = {
40
40
  readonly CONVEX_CLOUD_URL: string;
41
41
  readonly CONVEX_SITE_URL: string;
42
+ readonly RASTRO_GEOIP_DAILY_LIMIT: string | undefined;
43
+ readonly RASTRO_GEOIP_PROVIDER: "ipinfo" | "ipwhois" | undefined;
44
+ readonly RASTRO_GEOIP_TOKEN: string | undefined;
45
+ readonly RASTRO_TRUST_PROXY: "true" | undefined;
42
46
  };
43
47
 
44
48
  /**
@@ -0,0 +1,116 @@
1
+ const SKETCH_VERSION = 1 as const;
2
+ const DEFAULT_PRECISION = 6;
3
+
4
+ export type VisitorSketch = {
5
+ version: typeof SKETCH_VERSION;
6
+ precision: number;
7
+ registers: ArrayBuffer;
8
+ };
9
+
10
+ export function createVisitorSketch(
11
+ precision = DEFAULT_PRECISION,
12
+ ): VisitorSketch {
13
+ if (!Number.isSafeInteger(precision) || precision < 4 || precision > 10) {
14
+ throw new Error("visitor sketch precision must be between 4 and 10");
15
+ }
16
+ return {
17
+ version: SKETCH_VERSION,
18
+ precision,
19
+ registers: new Uint8Array(1 << precision).buffer,
20
+ };
21
+ }
22
+
23
+ export function addVisitor(
24
+ sketch: VisitorSketch,
25
+ visitorId: string,
26
+ ): VisitorSketch {
27
+ const registers = checkedRegisters(sketch).slice();
28
+ const hash = fnv1a64(visitorId);
29
+ const remainingBits = 64 - sketch.precision;
30
+ const index = Number(hash >> BigInt(remainingBits));
31
+ const remaining = hash & ((1n << BigInt(remainingBits)) - 1n);
32
+ const rank = leadingZeroRank(remaining, remainingBits);
33
+ registers[index] = Math.max(registers[index] ?? 0, rank);
34
+ return { ...sketch, registers: registers.buffer };
35
+ }
36
+
37
+ export function unionVisitorSketches(
38
+ left: VisitorSketch,
39
+ right: VisitorSketch,
40
+ ): VisitorSketch {
41
+ const leftRegisters = checkedRegisters(left);
42
+ const rightRegisters = checkedRegisters(right);
43
+ if (
44
+ left.version !== right.version ||
45
+ left.precision !== right.precision ||
46
+ leftRegisters.length !== rightRegisters.length
47
+ ) {
48
+ throw new Error("visitor sketches are incompatible");
49
+ }
50
+ const registers = leftRegisters.slice();
51
+ for (let index = 0; index < registers.length; index += 1) {
52
+ registers[index] = Math.max(
53
+ registers[index] ?? 0,
54
+ rightRegisters[index] ?? 0,
55
+ );
56
+ }
57
+ return { ...left, registers: registers.buffer };
58
+ }
59
+
60
+ export function estimateVisitors(sketch: VisitorSketch): number {
61
+ const registers = checkedRegisters(sketch);
62
+ const count = registers.length;
63
+ const alpha = count === 16
64
+ ? 0.673
65
+ : count === 32
66
+ ? 0.697
67
+ : count === 64
68
+ ? 0.709
69
+ : 0.7213 / (1 + 1.079 / count);
70
+ let denominator = 0;
71
+ let empty = 0;
72
+ for (const register of registers) {
73
+ denominator += 2 ** -register;
74
+ if (register === 0) empty += 1;
75
+ }
76
+ let estimate = (alpha * count * count) / denominator;
77
+ if (estimate <= 2.5 * count && empty > 0) {
78
+ estimate = count * Math.log(count / empty);
79
+ }
80
+ return Math.max(0, Math.round(estimate));
81
+ }
82
+
83
+ function checkedRegisters(sketch: VisitorSketch): Uint8Array<ArrayBuffer> {
84
+ if (sketch.version !== SKETCH_VERSION) {
85
+ throw new Error("unsupported visitor sketch version");
86
+ }
87
+ const registers = new Uint8Array(sketch.registers);
88
+ if (registers.length !== 1 << sketch.precision) {
89
+ throw new Error("visitor sketch has invalid register storage");
90
+ }
91
+ return registers;
92
+ }
93
+
94
+ function leadingZeroRank(value: bigint, bits: number): number {
95
+ if (value === 0n) return bits + 1;
96
+ let rank = 1;
97
+ for (let bit = bits - 1; bit >= 0; bit -= 1) {
98
+ if ((value & (1n << BigInt(bit))) !== 0n) break;
99
+ rank += 1;
100
+ }
101
+ return rank;
102
+ }
103
+
104
+ function fnv1a64(value: string): bigint {
105
+ let hash = 0xcbf29ce484222325n;
106
+ for (const byte of new TextEncoder().encode(value)) {
107
+ hash ^= BigInt(byte);
108
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
109
+ }
110
+ hash ^= hash >> 33n;
111
+ hash = BigInt.asUintN(64, hash * 0xff51afd7ed558ccdn);
112
+ hash ^= hash >> 33n;
113
+ hash = BigInt.asUintN(64, hash * 0xc4ceb9fe1a85ec53n);
114
+ hash ^= hash >> 33n;
115
+ return hash;
116
+ }
@@ -28,6 +28,16 @@ export const JOURNEY_READ_BYTE_RESERVE = 4 * 1024 * 1024;
28
28
  export const JOURNEY_READ_DOCUMENT_RESERVE = 1_000;
29
29
  export const MAX_RETENTION_BATCH = 500;
30
30
  export const MAX_OVERVIEW_AGGREGATE_ROWS = 13_000;
31
+ export const MAX_REPORT_BUCKETS = 5_000;
32
+ export const MAX_FEATURE_ROLLUP_DAYS = 90;
33
+ export const MAX_FEATURE_ROLLUP_ROWS = 5_000;
34
+ export const FEATURE_REPORT_BYTE_RESERVE = 2 * 1024 * 1024;
35
+ export const FEATURE_REPORT_DOCUMENT_RESERVE = 500;
36
+ export const REPORT_ROLLUP_SHARDS = 2;
37
+ export const ROLLUP_SAFETY_DELAY_MS = 5 * 60_000;
38
+ export const RETENTION_LEASE_MS = 60_000;
31
39
 
32
40
  export const DAY_MS = 86_400_000;
33
41
  export const HOUR_MS = 3_600_000;
42
+ export const MINUTE_MS = 60_000;
43
+ export const RETENTION_POLICY_INTERVAL_MS = DAY_MS;
@@ -1,7 +1,13 @@
1
- import migrations from "@convex-dev/migrations/convex.config.js";
2
1
  import { defineComponent } from "convex/server";
2
+ import { v } from "convex/values";
3
3
 
4
- const component = defineComponent("rastroAnalytics");
5
- component.use(migrations);
6
-
7
- export default component;
4
+ export default defineComponent("rastroAnalytics", {
5
+ env: {
6
+ RASTRO_GEOIP_PROVIDER: v.optional(
7
+ v.union(v.literal("ipinfo"), v.literal("ipwhois")),
8
+ ),
9
+ RASTRO_GEOIP_TOKEN: v.optional(v.string()),
10
+ RASTRO_GEOIP_DAILY_LIMIT: v.optional(v.string()),
11
+ RASTRO_TRUST_PROXY: v.optional(v.literal("true")),
12
+ },
13
+ });
@@ -0,0 +1,25 @@
1
+ import type { Id } from "./_generated/dataModel.js";
2
+ import type { MutationCtx } from "./_generated/server.js";
3
+ import { ROLLUP_SAFETY_DELAY_MS } from "./constants.js";
4
+
5
+ export async function ensureAnalyticsControl(
6
+ ctx: MutationCtx,
7
+ siteId: Id<"sites">,
8
+ now: number,
9
+ retained: Array<{ dataset: string; before: number }> = [],
10
+ ) {
11
+ const existing = await ctx.db
12
+ .query("analyticsCoverage")
13
+ .withIndex("by_siteId", (range) => range.eq("siteId", siteId))
14
+ .unique();
15
+ if (existing) return existing;
16
+ const fields = {
17
+ siteId,
18
+ generation: 1 as const,
19
+ rollupCompleteFrom: now + ROLLUP_SAFETY_DELAY_MS,
20
+ retained,
21
+ updatedAt: now,
22
+ };
23
+ const _id = await ctx.db.insert("analyticsCoverage", fields);
24
+ return { ...fields, _id, _creationTime: now };
25
+ }
@@ -0,0 +1,64 @@
1
+ import { v } from "convex/values";
2
+ import { query } from "./_generated/server.js";
3
+ import { fail } from "./errors.js";
4
+ import { sanitizeOpaqueId } from "./sanitize.js";
5
+
6
+ const MAX_SESSION_TOTAL_IDS = 100;
7
+
8
+ export const sessionEventTotals = query({
9
+ args: {
10
+ siteId: v.id("sites"),
11
+ sessionIds: v.array(v.string()),
12
+ },
13
+ returns: v.object({
14
+ requestedSessions: v.number(),
15
+ foundSessions: v.number(),
16
+ storedEvents: v.number(),
17
+ }),
18
+ handler: async (ctx, args) => {
19
+ if (
20
+ args.sessionIds.length < 1 ||
21
+ args.sessionIds.length > MAX_SESSION_TOTAL_IDS
22
+ ) {
23
+ fail(
24
+ "INVALID_ARGUMENT",
25
+ `sessionIds must contain between 1 and ${MAX_SESSION_TOTAL_IDS} IDs`,
26
+ );
27
+ }
28
+ let sessionIds: string[];
29
+ try {
30
+ sessionIds = args.sessionIds.map((sessionId) => {
31
+ const sanitized = sanitizeOpaqueId(sessionId, "sessionId");
32
+ if (sanitized !== sessionId) throw new Error("sessionId is not canonical");
33
+ return sanitized;
34
+ });
35
+ } catch (error) {
36
+ fail(
37
+ "INVALID_ARGUMENT",
38
+ error instanceof Error ? error.message : "invalid sessionId",
39
+ );
40
+ }
41
+ if (new Set(sessionIds).size !== sessionIds.length) {
42
+ fail("INVALID_ARGUMENT", "sessionIds must be unique");
43
+ }
44
+
45
+ const sessions = await Promise.all(
46
+ sessionIds.map((sessionId) =>
47
+ ctx.db
48
+ .query("sessions")
49
+ .withIndex("by_siteId_and_sessionId", (range) =>
50
+ range.eq("siteId", args.siteId).eq("sessionId", sessionId),
51
+ )
52
+ .unique()
53
+ ),
54
+ );
55
+ return {
56
+ requestedSessions: sessionIds.length,
57
+ foundSessions: sessions.filter((session) => session !== null).length,
58
+ storedEvents: sessions.reduce(
59
+ (total, session) => total + (session?.eventCount ?? 0),
60
+ 0,
61
+ ),
62
+ };
63
+ },
64
+ });
@@ -9,7 +9,8 @@ export type RastroErrorCode =
9
9
  | "LIMIT_EXCEEDED"
10
10
  | "CONFLICT"
11
11
  | "CURRENCY_MISMATCH"
12
- | "RATE_LIMITED";
12
+ | "RATE_LIMITED"
13
+ | "REPORT_INCOMPLETE";
13
14
 
14
15
  export function fail(
15
16
  code: RastroErrorCode,