@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
@@ -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",
@@ -172,6 +185,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
172
185
  latitude?: number;
173
186
  longitude?: number;
174
187
  os?: string;
188
+ visitorKey?: string;
175
189
  };
176
190
  events: Array<{
177
191
  affiliateSlug?: string;
@@ -193,7 +207,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
193
207
  | "custom"
194
208
  | "conversion"
195
209
  | "heartbeat"
196
- | "outbound";
210
+ | "leave"
211
+ | "outbound"
212
+ | "vital";
213
+ value?: number;
197
214
  visitorId: string;
198
215
  }>;
199
216
  origin: string;
@@ -244,10 +261,61 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
244
261
  updatedAt: number;
245
262
  visits: number;
246
263
  }>;
264
+ metadata: {
265
+ basis: "dailyRollup" | "boundedLegacy";
266
+ coverage: {
267
+ availableFrom: number | null;
268
+ availableThrough: number | null;
269
+ generation: number | null;
270
+ retainedBefore: number | null;
271
+ state: "complete" | "partial" | "retained" | "unavailable";
272
+ };
273
+ range: {
274
+ boundary: "inclusive";
275
+ from: number;
276
+ timezone: "UTC";
277
+ to: number;
278
+ };
279
+ uniqueVisitors: {
280
+ basis: "estimatedUnique" | "exactLegacy";
281
+ exact: boolean;
282
+ };
283
+ };
247
284
  truncated: boolean;
248
285
  },
249
286
  Name
250
287
  >;
288
+ dataCoverage: FunctionReference<
289
+ "query",
290
+ "internal",
291
+ { from: number; siteId: string; to: number },
292
+ {
293
+ datasets: Array<{
294
+ coverage: {
295
+ availableFrom: number | null;
296
+ availableThrough: number | null;
297
+ generation: number | null;
298
+ retainedBefore: number | null;
299
+ state: "complete" | "partial" | "retained" | "unavailable";
300
+ };
301
+ dataset:
302
+ | "overviewHour"
303
+ | "overviewDay"
304
+ | "events"
305
+ | "sessions"
306
+ | "conversions"
307
+ | "goals"
308
+ | "funnels"
309
+ | "affiliates"
310
+ | "vitals"
311
+ | "siteMap";
312
+ }>;
313
+ from: number;
314
+ siteId: string;
315
+ to: number;
316
+ },
317
+ Name
318
+ >;
251
319
  funnelsReport: FunctionReference<
252
320
  "query",
253
321
  "internal",
@@ -259,6 +327,22 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
259
327
  conversionWindowMs: number;
260
328
  createdAt: number;
261
329
  key: string;
330
+ metadata: {
331
+ basis: "dailyRollup" | "boundedLegacy";
332
+ coverage: {
333
+ availableFrom: number | null;
334
+ availableThrough: number | null;
335
+ generation: number | null;
336
+ retainedBefore: number | null;
337
+ state: "complete" | "partial" | "retained" | "unavailable";
338
+ };
339
+ range: {
340
+ boundary: "inclusive";
341
+ from: number;
342
+ timezone: "UTC";
343
+ to: number;
344
+ };
345
+ };
262
346
  name: string;
263
347
  siteId: string;
264
348
  steps: Array<{
@@ -296,6 +380,22 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
296
380
  updatedAt: number;
297
381
  valueCents: number;
298
382
  }>;
383
+ metadata: {
384
+ basis: "dailyRollup" | "boundedLegacy";
385
+ coverage: {
386
+ availableFrom: number | null;
387
+ availableThrough: number | null;
388
+ generation: number | null;
389
+ retainedBefore: number | null;
390
+ state: "complete" | "partial" | "retained" | "unavailable";
391
+ };
392
+ range: {
393
+ boundary: "inclusive";
394
+ from: number;
395
+ timezone: "UTC";
396
+ to: number;
397
+ };
398
+ };
299
399
  truncated: boolean;
300
400
  },
301
401
  Name
@@ -394,6 +494,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
394
494
  utmMedium?: string;
395
495
  utmSource?: string;
396
496
  visitorId: string;
497
+ visitorKey?: string;
397
498
  }>;
398
499
  pageStatus?: "SplitRecommended" | "SplitRequired" | null;
399
500
  splitCursor?: string | null;
@@ -416,9 +517,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
416
517
  lastSeenAt: number;
417
518
  latitude?: number;
418
519
  longitude?: number;
520
+ movedAt?: number;
419
521
  os: string;
420
522
  pageviewCount: number;
421
523
  path: string;
524
+ previousPath?: string;
422
525
  sessionId: string;
423
526
  siteId: string;
424
527
  source: string;
@@ -440,6 +543,44 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
440
543
  currency: string;
441
544
  from: number;
442
545
  interval: "hour" | "day";
546
+ metadata: {
547
+ breakdowns: {
548
+ topBrowsers: "eventVolume";
549
+ topCampaigns: "eventVolume";
550
+ topCountries: "eventVolume";
551
+ topDevices: "eventVolume";
552
+ topEvents: "eventVolume";
553
+ topGoals: "goalCompletionVolume";
554
+ topPages: "eventVolume";
555
+ topSources: "eventVolume";
556
+ };
557
+ coverage: {
558
+ availableFrom: number | null;
559
+ availableThrough: number | null;
560
+ generation: number | null;
561
+ retainedBefore: number | null;
562
+ state: "complete" | "partial" | "retained" | "unavailable";
563
+ };
564
+ features: {
565
+ botDetection: "userAgent";
566
+ errorInsights: "unsupported";
567
+ outboundLinks: "eventVolume";
568
+ sessionReplay: "unsupported";
569
+ };
570
+ range: {
571
+ boundary: "inclusive";
572
+ from: number;
573
+ timezone: "UTC";
574
+ to: number;
575
+ };
576
+ visitors: {
577
+ basis: "estimatedUnique" | "bucketOccurrences";
578
+ exact: boolean;
579
+ legacyIncomplete: boolean;
580
+ precision: number | null;
581
+ sketchVersion: number | null;
582
+ };
583
+ };
443
584
  timeSeries: Array<{
444
585
  clicks: number;
445
586
  conversions: number;
@@ -458,6 +599,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
458
599
  revenueCents: number;
459
600
  value: string;
460
601
  }>;
602
+ topCampaigns: Array<{
603
+ count: number;
604
+ revenueCents: number;
605
+ value: string;
606
+ }>;
461
607
  topCountries: Array<{
462
608
  count: number;
463
609
  revenueCents: number;
@@ -552,7 +698,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
552
698
  | "custom"
553
699
  | "conversion"
554
700
  | "heartbeat"
555
- | "outbound";
701
+ | "leave"
702
+ | "outbound"
703
+ | "vital";
556
704
  visitorId: string;
557
705
  }>;
558
706
  pageStatus?: "SplitRecommended" | "SplitRequired" | null;
@@ -560,6 +708,54 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
560
708
  },
561
709
  Name
562
710
  >;
711
+ siteMap: FunctionReference<
712
+ "query",
713
+ "internal",
714
+ { from: number; siteIds: Array<string>; to: number },
715
+ {
716
+ from: number;
717
+ metadata: {
718
+ basis: "dailyRollup" | "boundedLegacy";
719
+ coverage: {
720
+ availableFrom: number | null;
721
+ availableThrough: number | null;
722
+ generation: number | null;
723
+ retainedBefore: number | null;
724
+ state: "complete" | "partial" | "retained" | "unavailable";
725
+ };
726
+ limits: {
727
+ routesPerDay: number;
728
+ rows: number;
729
+ transitionSlots: number;
730
+ };
731
+ range: {
732
+ boundary: "inclusive";
733
+ from: number;
734
+ timezone: "UTC";
735
+ to: number;
736
+ };
737
+ visitors: { basis: "estimatedUnique"; exact: boolean };
738
+ };
739
+ routes: Array<{
740
+ bounces: number;
741
+ daily: Array<{ bucketStart: number; pageviews: number }>;
742
+ dwellMs: number;
743
+ dwellSamples: number;
744
+ entries: number;
745
+ exits: number;
746
+ pageviews: number;
747
+ route: string;
748
+ transitionsIn: number;
749
+ transitionsOut: number;
750
+ visitors: number;
751
+ }>;
752
+ to: number;
753
+ transitions: Array<{ count: number; from: string; to: string }>;
754
+ truncated: { routes: boolean; transitions: boolean };
755
+ window: { from: number; to: number } | null;
756
+ },
757
+ Name
758
+ >;
563
759
  visitorJourney: FunctionReference<
564
760
  "query",
565
761
  "internal",
@@ -600,11 +796,70 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
600
796
  | "custom"
601
797
  | "conversion"
602
798
  | "heartbeat"
603
- | "outbound";
799
+ | "leave"
800
+ | "outbound"
801
+ | "vital";
604
802
  visitorId: string;
605
803
  }>,
606
804
  Name
607
805
  >;
806
+ vitalsReport: FunctionReference<
807
+ "query",
808
+ "internal",
809
+ { from: number; siteId: string; to: number },
810
+ {
811
+ metadata: {
812
+ basis: "dailyRollup" | "boundedLegacy";
813
+ coverage: {
814
+ availableFrom: number | null;
815
+ availableThrough: number | null;
816
+ generation: number | null;
817
+ retainedBefore: number | null;
818
+ state: "complete" | "partial" | "retained" | "unavailable";
819
+ };
820
+ range: {
821
+ boundary: "inclusive";
822
+ from: number;
823
+ timezone: "UTC";
824
+ to: number;
825
+ };
826
+ };
827
+ metrics: Array<{
828
+ average: number;
829
+ count: number;
830
+ devices: Array<{
831
+ average: number;
832
+ count: number;
833
+ device: string;
834
+ good: number;
835
+ needsImprovement: number;
836
+ p75: number;
837
+ poor: number;
838
+ }>;
839
+ good: number;
840
+ metric: "LCP" | "CLS" | "INP" | "FCP" | "TTFB";
841
+ needsImprovement: number;
842
+ p75: number;
843
+ pages: Array<{
844
+ average: number;
845
+ count: number;
846
+ good: number;
847
+ needsImprovement: number;
848
+ p75: number;
849
+ page: string;
850
+ poor: number;
851
+ }>;
852
+ poor: number;
853
+ timeSeries: Array<{
854
+ count: number;
855
+ p75: number;
856
+ timestamp: number;
857
+ }>;
858
+ }>;
859
+ truncated: boolean;
860
+ },
861
+ Name
862
+ >;
608
863
  };
609
864
  retention: {
610
865
  cleanup: FunctionReference<
@@ -623,13 +878,111 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
623
878
  | "funnelProgress"
624
879
  | "funnelSteps"
625
880
  | "affiliateStats"
881
+ | "goalRollups"
882
+ | "funnelRollups"
883
+ | "affiliateRollups"
884
+ | "vitalRollups"
885
+ | "siteMapRollups"
886
+ | "attributions"
887
+ | "liveSessions"
888
+ | "ingestWindows"
889
+ | "siteIngestWindows";
890
+ siteId: string;
891
+ },
892
+ { before: number; deleted: number; hasMore: boolean },
893
+ Name
894
+ >;
895
+ disablePolicy: FunctionReference<
896
+ "mutation",
897
+ "internal",
898
+ { siteId: string },
899
+ null,
900
+ Name
901
+ >;
902
+ runPolicy: FunctionReference<
903
+ "mutation",
904
+ "internal",
905
+ {
906
+ batchSize?: number;
907
+ kind:
908
+ | "events"
909
+ | "sessions"
910
+ | "conversions"
911
+ | "goalCompletions"
912
+ | "aggregateHour"
913
+ | "aggregateDay"
914
+ | "funnelProgress"
915
+ | "funnelSteps"
916
+ | "affiliateStats"
917
+ | "goalRollups"
918
+ | "funnelRollups"
919
+ | "affiliateRollups"
920
+ | "vitalRollups"
921
+ | "siteMapRollups"
626
922
  | "attributions"
627
923
  | "liveSessions"
628
924
  | "ingestWindows"
629
925
  | "siteIngestWindows";
630
926
  siteId: string;
631
927
  },
632
- { deleted: number; hasMore: boolean },
928
+ { before: number; deleted: number; hasMore: boolean },
929
+ Name
930
+ >;
931
+ setPolicy: FunctionReference<
932
+ "mutation",
933
+ "internal",
934
+ {
935
+ aggregatesDays: number;
936
+ conversionsDays: number;
937
+ eventsDays: number;
938
+ reportsDays: number;
939
+ sessionsDays: number;
940
+ siteId: string;
941
+ },
942
+ null,
943
+ Name
944
+ >;
945
+ status: FunctionReference<
946
+ "query",
947
+ "internal",
948
+ { siteId: string },
949
+ {
950
+ backlog: number;
951
+ jobs: Array<{
952
+ before: number;
953
+ deleted: number;
954
+ kind:
955
+ | "events"
956
+ | "sessions"
957
+ | "conversions"
958
+ | "goalCompletions"
959
+ | "aggregateHour"
960
+ | "aggregateDay"
961
+ | "funnelProgress"
962
+ | "funnelSteps"
963
+ | "affiliateStats"
964
+ | "goalRollups"
965
+ | "funnelRollups"
966
+ | "affiliateRollups"
967
+ | "vitalRollups"
968
+ | "siteMapRollups"
969
+ | "attributions"
970
+ | "liveSessions"
971
+ | "ingestWindows"
972
+ | "siteIngestWindows";
973
+ scheduledAt: number | null;
974
+ status: "pending" | "running" | "complete";
975
+ updatedAt: number;
976
+ }>;
977
+ policy: {
978
+ aggregatesDays: number;
979
+ conversionsDays: number;
980
+ eventsDays: number;
981
+ reportsDays: number;
982
+ sessionsDays: number;
983
+ updatedAt: number;
984
+ } | null;
985
+ },
633
986
  Name
634
987
  >;
635
988
  };
@@ -703,4 +1056,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
703
1056
  Name
704
1057
  >;
705
1058
  };
1059
+ visitors: {
1060
+ link: FunctionReference<
1061
+ "mutation",
1062
+ "internal",
1063
+ { previousVisitorId: string; siteId: string; visitorId: string },
1064
+ { aliasCount: number; linked: boolean },
1065
+ Name
1066
+ >;
1067
+ };
706
1068
  };
@@ -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
  /**
@@ -22,16 +22,27 @@ export const upsert = mutation({
22
22
  },
23
23
  returns: v.id("affiliates"),
24
24
  handler: async (ctx, args) => {
25
- if (!(await ctx.db.get("sites", args.siteId))) fail("NOT_FOUND", "site not found");
26
- if (!Number.isSafeInteger(args.commissionBps) || args.commissionBps < 0 || args.commissionBps > 10_000) {
27
- fail("INVALID_ARGUMENT", "commissionBps must be an integer between 0 and 10000");
25
+ if (!(await ctx.db.get("sites", args.siteId)))
26
+ fail("NOT_FOUND", "site not found");
27
+ if (
28
+ !Number.isSafeInteger(args.commissionBps) ||
29
+ args.commissionBps < 0 ||
30
+ args.commissionBps > 10_000
31
+ ) {
32
+ fail(
33
+ "INVALID_ARGUMENT",
34
+ "commissionBps must be an integer between 0 and 10000",
35
+ );
28
36
  }
29
37
  if (
30
38
  !Number.isSafeInteger(args.attributionWindowDays) ||
31
39
  args.attributionWindowDays < 1 ||
32
40
  args.attributionWindowDays > 365
33
41
  ) {
34
- fail("INVALID_ARGUMENT", "attributionWindowDays must be an integer between 1 and 365");
42
+ fail(
43
+ "INVALID_ARGUMENT",
44
+ "attributionWindowDays must be an integer between 1 and 365",
45
+ );
35
46
  }
36
47
  let slug: string;
37
48
  try {
@@ -52,7 +63,11 @@ export const upsert = mutation({
52
63
  )
53
64
  .unique();
54
65
  const affiliateId = args.affiliateId ?? existingBySlug?._id;
55
- if (args.affiliateId && existingBySlug && existingBySlug._id !== args.affiliateId) {
66
+ if (
67
+ args.affiliateId &&
68
+ existingBySlug &&
69
+ existingBySlug._id !== args.affiliateId
70
+ ) {
56
71
  fail("CONFLICT", "another affiliate already uses this slug");
57
72
  }
58
73
  const now = Date.now();
@@ -0,0 +1,117 @@
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 =
64
+ count === 16
65
+ ? 0.673
66
+ : count === 32
67
+ ? 0.697
68
+ : count === 64
69
+ ? 0.709
70
+ : 0.7213 / (1 + 1.079 / count);
71
+ let denominator = 0;
72
+ let empty = 0;
73
+ for (const register of registers) {
74
+ denominator += 2 ** -register;
75
+ if (register === 0) empty += 1;
76
+ }
77
+ let estimate = (alpha * count * count) / denominator;
78
+ if (estimate <= 2.5 * count && empty > 0) {
79
+ estimate = count * Math.log(count / empty);
80
+ }
81
+ return Math.max(0, Math.round(estimate));
82
+ }
83
+
84
+ function checkedRegisters(sketch: VisitorSketch): Uint8Array<ArrayBuffer> {
85
+ if (sketch.version !== SKETCH_VERSION) {
86
+ throw new Error("unsupported visitor sketch version");
87
+ }
88
+ const registers = new Uint8Array(sketch.registers);
89
+ if (registers.length !== 1 << sketch.precision) {
90
+ throw new Error("visitor sketch has invalid register storage");
91
+ }
92
+ return registers;
93
+ }
94
+
95
+ function leadingZeroRank(value: bigint, bits: number): number {
96
+ if (value === 0n) return bits + 1;
97
+ let rank = 1;
98
+ for (let bit = bits - 1; bit >= 0; bit -= 1) {
99
+ if ((value & (1n << BigInt(bit))) !== 0n) break;
100
+ rank += 1;
101
+ }
102
+ return rank;
103
+ }
104
+
105
+ function fnv1a64(value: string): bigint {
106
+ let hash = 0xcbf29ce484222325n;
107
+ for (const byte of new TextEncoder().encode(value)) {
108
+ hash ^= BigInt(byte);
109
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
110
+ }
111
+ hash ^= hash >> 33n;
112
+ hash = BigInt.asUintN(64, hash * 0xff51afd7ed558ccdn);
113
+ hash ^= hash >> 33n;
114
+ hash = BigInt.asUintN(64, hash * 0xc4ceb9fe1a85ec53n);
115
+ hash ^= hash >> 33n;
116
+ return hash;
117
+ }