@iann29/rastro 0.1.0-alpha.3 → 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 +264 -51
- 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 +4 -0
- 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 +151 -1
- package/dist/component/_generated/component.d.ts.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/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/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/http.d.ts.map +1 -1
- package/dist/component/http.js +46 -8
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +238 -18
- 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 +111 -2
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +639 -78
- 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 +187 -1
- package/dist/component/schema.js +100 -1
- 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 +26 -1
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +7 -0
- package/dist/component/validators.js.map +1 -1
- package/docs/benchmarks/2026-08-20-realistic.md +4 -4
- 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 +32 -6
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +211 -1
- package/src/component/cardinality.ts +116 -0
- package/src/component/constants.ts +10 -0
- package/src/component/coverage.ts +25 -0
- package/src/component/errors.ts +2 -1
- package/src/component/http.ts +76 -6
- package/src/component/ingest.ts +387 -12
- package/src/component/live.ts +8 -3
- package/src/component/reports.ts +891 -86
- package/src/component/retention.ts +624 -88
- package/src/component/sanitize.ts +15 -4
- package/src/component/schema.ts +110 -0
- package/src/component/sites.ts +5 -1
- package/src/component/validators.ts +8 -0
|
@@ -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;
|
|
@@ -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
|
+
}
|
package/src/component/errors.ts
CHANGED
package/src/component/http.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MAX_BATCH_BYTES,
|
|
8
8
|
MAX_BATCH_EVENT_GROUPS,
|
|
9
9
|
MAX_BATCH_EVENTS,
|
|
10
|
+
RATE_LIMIT_WINDOW_MS,
|
|
10
11
|
} from "./constants.js";
|
|
11
12
|
import {
|
|
12
13
|
classifyIp,
|
|
@@ -35,9 +36,16 @@ http.route({
|
|
|
35
36
|
path: "/events",
|
|
36
37
|
method: "POST",
|
|
37
38
|
handler: httpAction(async (ctx, request) => {
|
|
39
|
+
const requestId = crypto.randomUUID();
|
|
38
40
|
const origin = request.headers.get("origin");
|
|
39
|
-
if (!origin)
|
|
40
|
-
|
|
41
|
+
if (!origin) {
|
|
42
|
+
return json(
|
|
43
|
+
{ error: { code: "ORIGIN_REQUIRED" } },
|
|
44
|
+
400,
|
|
45
|
+
requestIdHeaders(requestId),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const headers = corsHeaders(origin, requestId);
|
|
41
49
|
const contentLength = Number(request.headers.get("content-length"));
|
|
42
50
|
if (Number.isFinite(contentLength) && contentLength > MAX_BATCH_BYTES) {
|
|
43
51
|
return json(
|
|
@@ -100,6 +108,7 @@ http.route({
|
|
|
100
108
|
) {
|
|
101
109
|
const events = enrichmentCandidateEvents(payload.events);
|
|
102
110
|
if (events) {
|
|
111
|
+
let stage: GeoIpFailureStage = "claim";
|
|
103
112
|
try {
|
|
104
113
|
const claim = await ctx.runMutation(
|
|
105
114
|
internal.geo.claimEnrichment,
|
|
@@ -110,22 +119,34 @@ http.route({
|
|
|
110
119
|
},
|
|
111
120
|
);
|
|
112
121
|
if (claim.sessionIds.length > 0) {
|
|
122
|
+
stage = "resolve";
|
|
113
123
|
const providerGeo = await resolveGeoIp(
|
|
114
124
|
clientIp.ip,
|
|
115
125
|
env.RASTRO_GEOIP_PROVIDER!,
|
|
116
126
|
env.RASTRO_GEOIP_TOKEN,
|
|
117
127
|
);
|
|
118
128
|
if (providerGeo) {
|
|
129
|
+
stage = "apply";
|
|
119
130
|
await ctx.runMutation(internal.geo.applyEnrichment, {
|
|
120
131
|
siteId: payload.siteId as Id<"sites">,
|
|
121
132
|
sessionIds: claim.sessionIds,
|
|
122
133
|
events: claim.events,
|
|
123
134
|
context: providerGeo,
|
|
124
135
|
});
|
|
136
|
+
} else {
|
|
137
|
+
logGeoIpFailure(
|
|
138
|
+
requestId,
|
|
139
|
+
stage,
|
|
140
|
+
env.RASTRO_GEOIP_PROVIDER!,
|
|
141
|
+
);
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
144
|
} catch {
|
|
128
|
-
|
|
145
|
+
logGeoIpFailure(
|
|
146
|
+
requestId,
|
|
147
|
+
stage,
|
|
148
|
+
env.RASTRO_GEOIP_PROVIDER!,
|
|
149
|
+
);
|
|
129
150
|
}
|
|
130
151
|
}
|
|
131
152
|
}
|
|
@@ -139,9 +160,25 @@ http.route({
|
|
|
139
160
|
: data.code === "RATE_LIMITED"
|
|
140
161
|
? 429
|
|
141
162
|
: 400;
|
|
142
|
-
return json(
|
|
163
|
+
return json(
|
|
164
|
+
{ error: data },
|
|
165
|
+
status,
|
|
166
|
+
status === 429
|
|
167
|
+
? {
|
|
168
|
+
...headers,
|
|
169
|
+
"Retry-After": String(
|
|
170
|
+
Math.ceil(RATE_LIMIT_WINDOW_MS / 1_000),
|
|
171
|
+
),
|
|
172
|
+
}
|
|
173
|
+
: headers,
|
|
174
|
+
);
|
|
143
175
|
}
|
|
144
|
-
|
|
176
|
+
logInternalHttpError(requestId);
|
|
177
|
+
return json(
|
|
178
|
+
{ error: { code: "INTERNAL_ERROR", requestId } },
|
|
179
|
+
500,
|
|
180
|
+
headers,
|
|
181
|
+
);
|
|
145
182
|
}
|
|
146
183
|
}),
|
|
147
184
|
});
|
|
@@ -491,12 +528,45 @@ function classifyUserAgent(userAgent: string): {
|
|
|
491
528
|
return { browser, os, device };
|
|
492
529
|
}
|
|
493
530
|
|
|
494
|
-
|
|
531
|
+
type GeoIpFailureStage = "claim" | "resolve" | "apply";
|
|
532
|
+
|
|
533
|
+
function logInternalHttpError(requestId: string): void {
|
|
534
|
+
console.error(JSON.stringify({
|
|
535
|
+
level: "error",
|
|
536
|
+
event: "rastro.http.internal_error",
|
|
537
|
+
requestId,
|
|
538
|
+
route: "/events",
|
|
539
|
+
method: "POST",
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function logGeoIpFailure(
|
|
544
|
+
requestId: string,
|
|
545
|
+
stage: GeoIpFailureStage,
|
|
546
|
+
provider: "ipinfo" | "ipwhois",
|
|
547
|
+
): void {
|
|
548
|
+
console.warn(JSON.stringify({
|
|
549
|
+
level: "warn",
|
|
550
|
+
event: "rastro.geoip.failure",
|
|
551
|
+
requestId,
|
|
552
|
+
route: "/events",
|
|
553
|
+
stage,
|
|
554
|
+
provider,
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function requestIdHeaders(requestId: string): Record<string, string> {
|
|
559
|
+
return { "X-Request-ID": requestId };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function corsHeaders(origin: string, requestId?: string): Record<string, string> {
|
|
495
563
|
return {
|
|
496
564
|
"Access-Control-Allow-Origin": origin,
|
|
497
565
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
498
566
|
"Access-Control-Allow-Headers": "Content-Type",
|
|
567
|
+
"Access-Control-Expose-Headers": "X-Request-ID, Retry-After",
|
|
499
568
|
"Access-Control-Max-Age": "86400",
|
|
569
|
+
...(requestId ? requestIdHeaders(requestId) : {}),
|
|
500
570
|
Vary: "Origin",
|
|
501
571
|
};
|
|
502
572
|
}
|