@iann29/rastro 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (111) hide show
  1. package/README.md +374 -62
  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 +1658 -8
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +228 -2
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +10 -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 +169 -1
  17. package/dist/component/_generated/component.d.ts.map +1 -1
  18. package/dist/component/cardinality.d.ts +12 -0
  19. package/dist/component/cardinality.d.ts.map +1 -0
  20. package/dist/component/cardinality.js +94 -0
  21. package/dist/component/cardinality.js.map +1 -0
  22. package/dist/component/constants.d.ts +11 -0
  23. package/dist/component/constants.d.ts.map +1 -1
  24. package/dist/component/constants.js +11 -0
  25. package/dist/component/constants.js.map +1 -1
  26. package/dist/component/coverage.d.ts +18 -0
  27. package/dist/component/coverage.d.ts.map +1 -0
  28. package/dist/component/coverage.js +19 -0
  29. package/dist/component/coverage.js.map +1 -0
  30. package/dist/component/errors.d.ts +1 -1
  31. package/dist/component/errors.d.ts.map +1 -1
  32. package/dist/component/errors.js.map +1 -1
  33. package/dist/component/eventStore.d.ts +1 -0
  34. package/dist/component/eventStore.d.ts.map +1 -1
  35. package/dist/component/http.d.ts.map +1 -1
  36. package/dist/component/http.js +85 -36
  37. package/dist/component/http.js.map +1 -1
  38. package/dist/component/identity.d.ts +13 -0
  39. package/dist/component/identity.d.ts.map +1 -0
  40. package/dist/component/identity.js +58 -0
  41. package/dist/component/identity.js.map +1 -0
  42. package/dist/component/ingest.d.ts +1 -0
  43. package/dist/component/ingest.d.ts.map +1 -1
  44. package/dist/component/ingest.js +258 -19
  45. package/dist/component/ingest.js.map +1 -1
  46. package/dist/component/live.d.ts.map +1 -1
  47. package/dist/component/live.js +6 -3
  48. package/dist/component/live.js.map +1 -1
  49. package/dist/component/reports.d.ts +120 -2
  50. package/dist/component/reports.d.ts.map +1 -1
  51. package/dist/component/reports.js +659 -91
  52. package/dist/component/reports.js.map +1 -1
  53. package/dist/component/retention.d.ts +75 -1
  54. package/dist/component/retention.d.ts.map +1 -1
  55. package/dist/component/retention.js +517 -54
  56. package/dist/component/retention.js.map +1 -1
  57. package/dist/component/sanitize.d.ts +4 -1
  58. package/dist/component/sanitize.d.ts.map +1 -1
  59. package/dist/component/sanitize.js +13 -5
  60. package/dist/component/sanitize.js.map +1 -1
  61. package/dist/component/schema.d.ts +224 -7
  62. package/dist/component/schema.js +113 -1
  63. package/dist/component/schema.js.map +1 -1
  64. package/dist/component/sites.d.ts.map +1 -1
  65. package/dist/component/sites.js +5 -1
  66. package/dist/component/sites.js.map +1 -1
  67. package/dist/component/useragent.d.ts +9 -0
  68. package/dist/component/useragent.d.ts.map +1 -0
  69. package/dist/component/useragent.js +152 -0
  70. package/dist/component/useragent.js.map +1 -0
  71. package/dist/component/validators.d.ts +42 -11
  72. package/dist/component/validators.d.ts.map +1 -1
  73. package/dist/component/validators.js +10 -1
  74. package/dist/component/validators.js.map +1 -1
  75. package/dist/component/visitors.d.ts +19 -0
  76. package/dist/component/visitors.d.ts.map +1 -0
  77. package/dist/component/visitors.js +86 -0
  78. package/dist/component/visitors.js.map +1 -0
  79. package/dist/tracker/generated.d.ts +4 -4
  80. package/dist/tracker/generated.d.ts.map +1 -1
  81. package/dist/tracker/generated.js +4 -4
  82. package/dist/tracker/generated.js.map +1 -1
  83. package/dist/tracker/tracker.js +23 -11
  84. package/dist/tracker/tracker.js.map +1 -1
  85. package/dist/tracker.min.js +1 -1
  86. package/docs/benchmarks/2026-08-20-realistic.md +4 -4
  87. package/docs/benchmarks/2026-08-21-formal-certification.md +333 -0
  88. package/docs/federation-setup.md +395 -0
  89. package/docs/federation.md +258 -0
  90. package/docs/upgrading.md +130 -0
  91. package/llms.txt +65 -0
  92. package/package.json +41 -11
  93. package/scripts/generate-federation-keys.mjs +20 -0
  94. package/src/component/_generated/api.ts +10 -0
  95. package/src/component/_generated/component.ts +228 -1
  96. package/src/component/cardinality.ts +116 -0
  97. package/src/component/constants.ts +11 -0
  98. package/src/component/coverage.ts +25 -0
  99. package/src/component/errors.ts +2 -1
  100. package/src/component/http.ts +129 -38
  101. package/src/component/identity.ts +74 -0
  102. package/src/component/ingest.ts +414 -13
  103. package/src/component/live.ts +8 -3
  104. package/src/component/reports.ts +910 -98
  105. package/src/component/retention.ts +624 -88
  106. package/src/component/sanitize.ts +19 -5
  107. package/src/component/schema.ts +125 -0
  108. package/src/component/sites.ts +5 -1
  109. package/src/component/useragent.ts +171 -0
  110. package/src/component/validators.ts +11 -0
  111. package/src/component/visitors.ts +105 -0
@@ -185,6 +185,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
185
185
  latitude?: number;
186
186
  longitude?: number;
187
187
  os?: string;
188
+ visitorKey?: string;
188
189
  };
189
190
  events: Array<{
190
191
  affiliateSlug?: string;
@@ -257,10 +258,59 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
257
258
  updatedAt: number;
258
259
  visits: number;
259
260
  }>;
261
+ metadata: {
262
+ basis: "dailyRollup" | "boundedLegacy";
263
+ coverage: {
264
+ availableFrom: number | null;
265
+ availableThrough: number | null;
266
+ generation: number | null;
267
+ retainedBefore: number | null;
268
+ state: "complete" | "partial" | "retained" | "unavailable";
269
+ };
270
+ range: {
271
+ boundary: "inclusive";
272
+ from: number;
273
+ timezone: "UTC";
274
+ to: number;
275
+ };
276
+ uniqueVisitors: {
277
+ basis: "estimatedUnique" | "exactLegacy";
278
+ exact: boolean;
279
+ };
280
+ };
260
281
  truncated: boolean;
261
282
  },
262
283
  Name
263
284
  >;
285
+ dataCoverage: FunctionReference<
286
+ "query",
287
+ "internal",
288
+ { from: number; siteId: string; to: number },
289
+ {
290
+ datasets: Array<{
291
+ coverage: {
292
+ availableFrom: number | null;
293
+ availableThrough: number | null;
294
+ generation: number | null;
295
+ retainedBefore: number | null;
296
+ state: "complete" | "partial" | "retained" | "unavailable";
297
+ };
298
+ dataset:
299
+ | "overviewHour"
300
+ | "overviewDay"
301
+ | "events"
302
+ | "sessions"
303
+ | "conversions"
304
+ | "goals"
305
+ | "funnels"
306
+ | "affiliates";
307
+ }>;
308
+ from: number;
309
+ siteId: string;
310
+ to: number;
311
+ },
312
+ Name
313
+ >;
264
314
  funnelsReport: FunctionReference<
265
315
  "query",
266
316
  "internal",
@@ -272,6 +322,22 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
272
322
  conversionWindowMs: number;
273
323
  createdAt: number;
274
324
  key: string;
325
+ metadata: {
326
+ basis: "dailyRollup" | "boundedLegacy";
327
+ coverage: {
328
+ availableFrom: number | null;
329
+ availableThrough: number | null;
330
+ generation: number | null;
331
+ retainedBefore: number | null;
332
+ state: "complete" | "partial" | "retained" | "unavailable";
333
+ };
334
+ range: {
335
+ boundary: "inclusive";
336
+ from: number;
337
+ timezone: "UTC";
338
+ to: number;
339
+ };
340
+ };
275
341
  name: string;
276
342
  siteId: string;
277
343
  steps: Array<{
@@ -309,6 +375,22 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
309
375
  updatedAt: number;
310
376
  valueCents: number;
311
377
  }>;
378
+ metadata: {
379
+ basis: "dailyRollup" | "boundedLegacy";
380
+ coverage: {
381
+ availableFrom: number | null;
382
+ availableThrough: number | null;
383
+ generation: number | null;
384
+ retainedBefore: number | null;
385
+ state: "complete" | "partial" | "retained" | "unavailable";
386
+ };
387
+ range: {
388
+ boundary: "inclusive";
389
+ from: number;
390
+ timezone: "UTC";
391
+ to: number;
392
+ };
393
+ };
312
394
  truncated: boolean;
313
395
  },
314
396
  Name
@@ -407,6 +489,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
407
489
  utmMedium?: string;
408
490
  utmSource?: string;
409
491
  visitorId: string;
492
+ visitorKey?: string;
410
493
  }>;
411
494
  pageStatus?: "SplitRecommended" | "SplitRequired" | null;
412
495
  splitCursor?: string | null;
@@ -453,6 +536,44 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
453
536
  currency: string;
454
537
  from: number;
455
538
  interval: "hour" | "day";
539
+ metadata: {
540
+ breakdowns: {
541
+ topBrowsers: "eventVolume";
542
+ topCampaigns: "eventVolume";
543
+ topCountries: "eventVolume";
544
+ topDevices: "eventVolume";
545
+ topEvents: "eventVolume";
546
+ topGoals: "goalCompletionVolume";
547
+ topPages: "eventVolume";
548
+ topSources: "eventVolume";
549
+ };
550
+ coverage: {
551
+ availableFrom: number | null;
552
+ availableThrough: number | null;
553
+ generation: number | null;
554
+ retainedBefore: number | null;
555
+ state: "complete" | "partial" | "retained" | "unavailable";
556
+ };
557
+ features: {
558
+ botDetection: "userAgent";
559
+ errorInsights: "unsupported";
560
+ outboundLinks: "eventVolume";
561
+ sessionReplay: "unsupported";
562
+ };
563
+ range: {
564
+ boundary: "inclusive";
565
+ from: number;
566
+ timezone: "UTC";
567
+ to: number;
568
+ };
569
+ visitors: {
570
+ basis: "estimatedUnique" | "bucketOccurrences";
571
+ exact: boolean;
572
+ legacyIncomplete: boolean;
573
+ precision: number | null;
574
+ sketchVersion: number | null;
575
+ };
576
+ };
456
577
  timeSeries: Array<{
457
578
  clicks: number;
458
579
  conversions: number;
@@ -471,6 +592,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
471
592
  revenueCents: number;
472
593
  value: string;
473
594
  }>;
595
+ topCampaigns: Array<{
596
+ count: number;
597
+ revenueCents: number;
598
+ value: string;
599
+ }>;
474
600
  topCountries: Array<{
475
601
  count: number;
476
602
  revenueCents: number;
@@ -636,13 +762,105 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
636
762
  | "funnelProgress"
637
763
  | "funnelSteps"
638
764
  | "affiliateStats"
765
+ | "goalRollups"
766
+ | "funnelRollups"
767
+ | "affiliateRollups"
768
+ | "attributions"
769
+ | "liveSessions"
770
+ | "ingestWindows"
771
+ | "siteIngestWindows";
772
+ siteId: string;
773
+ },
774
+ { before: number; deleted: number; hasMore: boolean },
775
+ Name
776
+ >;
777
+ disablePolicy: FunctionReference<
778
+ "mutation",
779
+ "internal",
780
+ { siteId: string },
781
+ null,
782
+ Name
783
+ >;
784
+ runPolicy: FunctionReference<
785
+ "mutation",
786
+ "internal",
787
+ {
788
+ batchSize?: number;
789
+ kind:
790
+ | "events"
791
+ | "sessions"
792
+ | "conversions"
793
+ | "goalCompletions"
794
+ | "aggregateHour"
795
+ | "aggregateDay"
796
+ | "funnelProgress"
797
+ | "funnelSteps"
798
+ | "affiliateStats"
799
+ | "goalRollups"
800
+ | "funnelRollups"
801
+ | "affiliateRollups"
639
802
  | "attributions"
640
803
  | "liveSessions"
641
804
  | "ingestWindows"
642
805
  | "siteIngestWindows";
643
806
  siteId: string;
644
807
  },
645
- { deleted: number; hasMore: boolean },
808
+ { before: number; deleted: number; hasMore: boolean },
809
+ Name
810
+ >;
811
+ setPolicy: FunctionReference<
812
+ "mutation",
813
+ "internal",
814
+ {
815
+ aggregatesDays: number;
816
+ conversionsDays: number;
817
+ eventsDays: number;
818
+ reportsDays: number;
819
+ sessionsDays: number;
820
+ siteId: string;
821
+ },
822
+ null,
823
+ Name
824
+ >;
825
+ status: FunctionReference<
826
+ "query",
827
+ "internal",
828
+ { siteId: string },
829
+ {
830
+ backlog: number;
831
+ jobs: Array<{
832
+ before: number;
833
+ deleted: number;
834
+ kind:
835
+ | "events"
836
+ | "sessions"
837
+ | "conversions"
838
+ | "goalCompletions"
839
+ | "aggregateHour"
840
+ | "aggregateDay"
841
+ | "funnelProgress"
842
+ | "funnelSteps"
843
+ | "affiliateStats"
844
+ | "goalRollups"
845
+ | "funnelRollups"
846
+ | "affiliateRollups"
847
+ | "attributions"
848
+ | "liveSessions"
849
+ | "ingestWindows"
850
+ | "siteIngestWindows";
851
+ scheduledAt: number | null;
852
+ status: "pending" | "running" | "complete";
853
+ updatedAt: number;
854
+ }>;
855
+ policy: {
856
+ aggregatesDays: number;
857
+ conversionsDays: number;
858
+ eventsDays: number;
859
+ reportsDays: number;
860
+ sessionsDays: number;
861
+ updatedAt: number;
862
+ } | null;
863
+ },
646
864
  Name
647
865
  >;
648
866
  };
@@ -716,4 +934,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
716
934
  Name
717
935
  >;
718
936
  };
937
+ visitors: {
938
+ link: FunctionReference<
939
+ "mutation",
940
+ "internal",
941
+ { previousVisitorId: string; siteId: string; visitorId: string },
942
+ { aliasCount: number; linked: boolean },
943
+ Name
944
+ >;
945
+ };
719
946
  };
@@ -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,17 @@ 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;
44
+ export const MAX_VISITOR_ALIASES = 10;
@@ -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
+ }
@@ -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,