@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.
- package/README.md +325 -63
- package/agent/integration.md +651 -0
- package/agent/manifest.json +183 -0
- package/agent/manifest.schema.json +405 -0
- package/dist/client/federation.d.ts +205 -0
- package/dist/client/federation.d.ts.map +1 -0
- package/dist/client/federation.js +179 -0
- package/dist/client/federation.js.map +1 -0
- package/dist/client/index.d.ts +1600 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +210 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +9 -5
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +161 -1
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +4 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/cardinality.d.ts +12 -0
- package/dist/component/cardinality.d.ts.map +1 -0
- package/dist/component/cardinality.js +94 -0
- package/dist/component/cardinality.js.map +1 -0
- package/dist/component/constants.d.ts +10 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +10 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +7 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +9 -4
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/coverage.d.ts +18 -0
- package/dist/component/coverage.d.ts.map +1 -0
- package/dist/component/coverage.js +19 -0
- package/dist/component/coverage.js.map +1 -0
- package/dist/component/diagnostics.d.ts +9 -0
- package/dist/component/diagnostics.d.ts.map +1 -0
- package/dist/component/diagnostics.js +47 -0
- package/dist/component/diagnostics.js.map +1 -0
- package/dist/component/errors.d.ts +1 -1
- package/dist/component/errors.d.ts.map +1 -1
- package/dist/component/errors.js.map +1 -1
- package/dist/component/eventStore.d.ts +11 -9
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/eventStore.js +55 -227
- package/dist/component/eventStore.js.map +1 -1
- package/dist/component/geo.d.ts +71 -0
- package/dist/component/geo.d.ts.map +1 -0
- package/dist/component/geo.js +610 -0
- package/dist/component/geo.js.map +1 -0
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +168 -17
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +272 -32
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +6 -3
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +117 -8
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +641 -79
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts +75 -1
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +517 -54
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts +4 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +9 -4
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +202 -53
- package/dist/component/schema.js +113 -16
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +5 -1
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +29 -51
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +11 -12
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +4 -4
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +4 -4
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.js +7 -6
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/benchmarks/2026-08-20-realistic.md +5 -5
- package/docs/benchmarks/2026-08-21-formal-certification.md +333 -0
- package/docs/federation-setup.md +395 -0
- package/docs/federation.md +258 -0
- package/docs/upgrading.md +130 -0
- package/llms.txt +65 -0
- package/package.json +29 -7
- package/scripts/benchmark-ingest.mjs +81 -32
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +9 -5
- package/src/component/_generated/component.ts +224 -1
- package/src/component/_generated/server.ts +4 -0
- package/src/component/cardinality.ts +116 -0
- package/src/component/constants.ts +10 -0
- package/src/component/convex.config.ts +11 -5
- package/src/component/coverage.ts +25 -0
- package/src/component/diagnostics.ts +64 -0
- package/src/component/errors.ts +2 -1
- package/src/component/eventStore.ts +78 -280
- package/src/component/geo.ts +779 -0
- package/src/component/http.ts +259 -22
- package/src/component/ingest.ts +436 -27
- package/src/component/live.ts +8 -3
- package/src/component/reports.ts +895 -87
- package/src/component/retention.ts +624 -88
- package/src/component/sanitize.ts +15 -4
- package/src/component/schema.ts +125 -16
- package/src/component/sites.ts +5 -1
- package/src/component/validators.ts +12 -13
- package/src/test.ts +0 -2
- package/dist/component/migrations.d.ts +0 -17
- package/dist/component/migrations.d.ts.map +0 -1
- package/dist/component/migrations.js +0 -44
- package/dist/component/migrations.js.map +0 -1
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
});
|