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

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 (79) hide show
  1. package/README.md +98 -18
  2. package/dist/component/_generated/api.d.ts +7 -1
  3. package/dist/component/_generated/api.d.ts.map +1 -1
  4. package/dist/component/_generated/api.js.map +1 -1
  5. package/dist/component/constants.d.ts +11 -6
  6. package/dist/component/constants.d.ts.map +1 -1
  7. package/dist/component/constants.js +11 -6
  8. package/dist/component/constants.js.map +1 -1
  9. package/dist/component/convex.config.d.ts +2 -2
  10. package/dist/component/convex.config.d.ts.map +1 -1
  11. package/dist/component/convex.config.js +4 -1
  12. package/dist/component/convex.config.js.map +1 -1
  13. package/dist/component/eventStore.d.ts +102 -0
  14. package/dist/component/eventStore.d.ts.map +1 -0
  15. package/dist/component/eventStore.js +463 -0
  16. package/dist/component/eventStore.js.map +1 -0
  17. package/dist/component/goals.d.ts.map +1 -1
  18. package/dist/component/goals.js +17 -4
  19. package/dist/component/goals.js.map +1 -1
  20. package/dist/component/http.d.ts.map +1 -1
  21. package/dist/component/http.js +39 -7
  22. package/dist/component/http.js.map +1 -1
  23. package/dist/component/ingest.d.ts.map +1 -1
  24. package/dist/component/ingest.js +520 -207
  25. package/dist/component/ingest.js.map +1 -1
  26. package/dist/component/live.d.ts +8 -0
  27. package/dist/component/live.d.ts.map +1 -1
  28. package/dist/component/live.js +72 -8
  29. package/dist/component/live.js.map +1 -1
  30. package/dist/component/migrations.d.ts +17 -0
  31. package/dist/component/migrations.d.ts.map +1 -0
  32. package/dist/component/migrations.js +44 -0
  33. package/dist/component/migrations.js.map +1 -0
  34. package/dist/component/reports.d.ts +16 -16
  35. package/dist/component/reports.d.ts.map +1 -1
  36. package/dist/component/reports.js +228 -65
  37. package/dist/component/reports.js.map +1 -1
  38. package/dist/component/retention.d.ts.map +1 -1
  39. package/dist/component/retention.js +15 -10
  40. package/dist/component/retention.js.map +1 -1
  41. package/dist/component/sanitize.d.ts.map +1 -1
  42. package/dist/component/sanitize.js +11 -3
  43. package/dist/component/sanitize.js.map +1 -1
  44. package/dist/component/schema.d.ts +132 -9
  45. package/dist/component/schema.js +32 -7
  46. package/dist/component/schema.js.map +1 -1
  47. package/dist/component/sites.d.ts.map +1 -1
  48. package/dist/component/sites.js +27 -18
  49. package/dist/component/sites.js.map +1 -1
  50. package/dist/component/validators.d.ts +96 -0
  51. package/dist/component/validators.d.ts.map +1 -1
  52. package/dist/component/validators.js +12 -0
  53. package/dist/component/validators.js.map +1 -1
  54. package/dist/tracker/generated.d.ts +4 -4
  55. package/dist/tracker/generated.d.ts.map +1 -1
  56. package/dist/tracker/generated.js +4 -4
  57. package/dist/tracker/generated.js.map +1 -1
  58. package/dist/tracker/tracker.js +33 -22
  59. package/dist/tracker/tracker.js.map +1 -1
  60. package/dist/tracker.min.js +1 -1
  61. package/docs/benchmarks/2026-08-20-realistic.md +171 -0
  62. package/package.json +15 -5
  63. package/scripts/benchmark-ingest.mjs +499 -0
  64. package/src/component/_generated/api.ts +7 -1
  65. package/src/component/constants.ts +11 -6
  66. package/src/component/convex.config.ts +5 -1
  67. package/src/component/eventStore.ts +684 -0
  68. package/src/component/goals.ts +25 -4
  69. package/src/component/http.ts +49 -6
  70. package/src/component/ingest.ts +671 -215
  71. package/src/component/live.ts +83 -8
  72. package/src/component/migrations.ts +52 -0
  73. package/src/component/reports.ts +295 -86
  74. package/src/component/retention.ts +15 -12
  75. package/src/component/sanitize.ts +10 -3
  76. package/src/component/schema.ts +35 -7
  77. package/src/component/sites.ts +31 -18
  78. package/src/component/validators.ts +16 -0
  79. package/src/test.ts +2 -0
@@ -0,0 +1,499 @@
1
+ import process from "node:process";
2
+ import { Buffer } from "node:buffer";
3
+ import { performance } from "node:perf_hooks";
4
+ import { clearInterval, setInterval } from "node:timers";
5
+ import { ConvexHttpClient } from "convex/browser";
6
+ import { makeFunctionReference } from "convex/server";
7
+
8
+ const overviewQuery = makeFunctionReference("example:overview");
9
+
10
+ const PROFILE_DEFAULTS = {
11
+ lean: { batchSize: 50, sessions: 0 },
12
+ realistic: { batchSize: 20, sessions: 2_048 },
13
+ heavy: { batchSize: 20, sessions: 2_048 },
14
+ feature: { batchSize: 15, sessions: 2_048 },
15
+ };
16
+ const OPTION_NAMES = new Set([
17
+ "url",
18
+ "convex-url",
19
+ "site-id",
20
+ "origin",
21
+ "label",
22
+ "profile",
23
+ "duration",
24
+ "requests",
25
+ "concurrency",
26
+ "batch-size",
27
+ "sessions",
28
+ "progress",
29
+ "dry-run",
30
+ "certify",
31
+ ]);
32
+
33
+ const LOCATIONS = [
34
+ ["BR", "Sao Paulo", -23.55, -46.63],
35
+ ["US", "New York", 40.71, -74.01],
36
+ ["CA", "Toronto", 43.65, -79.38],
37
+ ["GB", "London", 51.51, -0.13],
38
+ ["DE", "Berlin", 52.52, 13.41],
39
+ ["FR", "Paris", 48.86, 2.35],
40
+ ["SE", "Stockholm", 59.33, 18.07],
41
+ ["IN", "Bengaluru", 12.97, 77.59],
42
+ ["SG", "Singapore", 1.35, 103.82],
43
+ ["JP", "Tokyo", 35.68, 139.69],
44
+ ["AU", "Sydney", -33.87, 151.21],
45
+ ["ZA", "Cape Town", -33.92, 18.42],
46
+ ];
47
+
48
+ const USER_AGENTS = [
49
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/127.0 Safari/537.36",
50
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/605.1.15 Version/17.6 Safari/605.1.15",
51
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 Version/17.6 Mobile/15E148 Safari/604.1",
52
+ "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
53
+ ];
54
+
55
+ const REALISTIC_PATHS = [
56
+ "/products",
57
+ "/docs",
58
+ "/blog/scale",
59
+ "/account",
60
+ "/checkout",
61
+ ];
62
+
63
+ function parseOptions(argv) {
64
+ const values = new Map();
65
+ for (let index = 0; index < argv.length; index += 1) {
66
+ const argument = argv[index];
67
+ if (!argument.startsWith("--")) throw new Error(`unexpected argument: ${argument}`);
68
+ const [rawName, inlineValue] = argument.slice(2).split("=", 2);
69
+ if (!OPTION_NAMES.has(rawName)) throw new Error(`unknown option: --${rawName}`);
70
+ if (values.has(rawName)) throw new Error(`duplicate option: --${rawName}`);
71
+ if (rawName === "dry-run" || rawName === "certify") {
72
+ values.set(rawName, "true");
73
+ continue;
74
+ }
75
+ const value = inlineValue ?? argv[++index];
76
+ if (!value || value.startsWith("--")) throw new Error(`missing value for --${rawName}`);
77
+ values.set(rawName, value);
78
+ }
79
+ const profile = values.get("profile") ?? "realistic";
80
+ if (!(profile in PROFILE_DEFAULTS)) {
81
+ throw new Error("--profile must be lean, realistic, heavy, or feature");
82
+ }
83
+ const defaults = PROFILE_DEFAULTS[profile];
84
+ return {
85
+ url: values.get("url"),
86
+ convexUrl: values.get("convex-url"),
87
+ siteId: values.get("site-id"),
88
+ origin: values.get("origin") ?? "https://demo.rastro.local",
89
+ label: values.get("label") ?? `${profile}-${new Date().toISOString()}`,
90
+ profile,
91
+ durationSeconds: positiveNumber(values.get("duration") ?? "60", "duration"),
92
+ maximumRequests: values.has("requests")
93
+ ? positiveInteger(values.get("requests"), "requests")
94
+ : Number.POSITIVE_INFINITY,
95
+ concurrency: positiveInteger(values.get("concurrency") ?? "100", "concurrency"),
96
+ batchSize: positiveInteger(
97
+ values.get("batch-size") ?? String(defaults.batchSize),
98
+ "batch-size",
99
+ ),
100
+ sessionCount: positiveInteger(
101
+ values.get("sessions") ?? String(defaults.sessions || 1),
102
+ "sessions",
103
+ ),
104
+ progressSeconds: positiveNumber(values.get("progress") ?? "10", "progress"),
105
+ dryRun: values.get("dry-run") === "true",
106
+ certify: values.get("certify") === "true",
107
+ };
108
+ }
109
+
110
+ function positiveNumber(value, name) {
111
+ const parsed = Number(value);
112
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`--${name} must be positive`);
113
+ return parsed;
114
+ }
115
+
116
+ function positiveInteger(value, name) {
117
+ const parsed = positiveNumber(value, name);
118
+ if (!Number.isSafeInteger(parsed)) throw new Error(`--${name} must be an integer`);
119
+ return parsed;
120
+ }
121
+
122
+ function eventFor(profile, session, sequence, timestamp) {
123
+ const path = profile === "lean"
124
+ ? `/benchmark/lean/${sequence % 4}`
125
+ : REALISTIC_PATHS[sequence % REALISTIC_PATHS.length];
126
+ const base = {
127
+ eventId: `${session.id}.${sequence.toString(36)}`,
128
+ sessionId: session.id,
129
+ visitorId: session.visitorId,
130
+ path,
131
+ timestamp,
132
+ sequence,
133
+ };
134
+ if (profile === "lean") return { ...base, type: "pageview" };
135
+ if (profile === "heavy") {
136
+ return {
137
+ ...base,
138
+ type: "custom",
139
+ name: "payload-sample",
140
+ properties: Object.fromEntries(
141
+ Array.from({ length: 16 }, (_, index) => [
142
+ `property_${index}`,
143
+ `${index.toString(36)}-${"x".repeat(157)}`,
144
+ ]),
145
+ ),
146
+ };
147
+ }
148
+ if (profile === "feature") {
149
+ const step = sequence % 3;
150
+ if (step === 0) {
151
+ return { ...base, type: "pageview", path: "/", affiliateSlug: "creator-club" };
152
+ }
153
+ if (step === 1) {
154
+ return { ...base, type: "pageview", path: "/pricing", affiliateSlug: "creator-club" };
155
+ }
156
+ return {
157
+ ...base,
158
+ type: "custom",
159
+ name: "signup",
160
+ path: "/signup",
161
+ affiliateSlug: "creator-club",
162
+ properties: { plan: sequence % 2 === 0 ? "pro" : "starter" },
163
+ };
164
+ }
165
+
166
+ const kind = sequence === 0 ? 0 : sequence % 20;
167
+ if (kind === 19) return { ...base, type: "heartbeat" };
168
+ if (kind < 6) {
169
+ return {
170
+ ...base,
171
+ type: "pageview",
172
+ referrer: sequence % 2 === 0 ? "https://www.google.com/search?q=rastro" : undefined,
173
+ };
174
+ }
175
+ if (kind < 13) return { ...base, type: "click", target: "Get started" };
176
+ if (kind < 17) {
177
+ return {
178
+ ...base,
179
+ type: "custom",
180
+ name: "cta-engaged",
181
+ properties: { placement: "hero", experiment: `variant-${sequence % 3}` },
182
+ };
183
+ }
184
+ return { ...base, type: "outbound", href: "https://github.com/amageweb/amage-rastro" };
185
+ }
186
+
187
+ function requestContext(session) {
188
+ const [country, city, latitude, longitude] = LOCATIONS[session.locationIndex];
189
+ return {
190
+ "User-Agent": USER_AGENTS[session.userAgentIndex],
191
+ "x-vercel-ip-country": country,
192
+ "x-vercel-ip-city": encodeURIComponent(city),
193
+ "x-vercel-ip-latitude": String(latitude),
194
+ "x-vercel-ip-longitude": String(longitude),
195
+ };
196
+ }
197
+
198
+ function percentile(sorted, fraction) {
199
+ if (sorted.length === 0) return 0;
200
+ return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)];
201
+ }
202
+
203
+ async function main() {
204
+ const options = parseOptions(process.argv.slice(2));
205
+ if (options.batchSize > 50) throw new Error("--batch-size cannot exceed 50");
206
+ if (options.certify && options.durationSeconds < 600) {
207
+ throw new Error("--certify requires --duration of at least 600 seconds");
208
+ }
209
+ if (options.certify && !options.convexUrl) {
210
+ throw new Error("--certify requires --convex-url reconciliation");
211
+ }
212
+ if (options.certify && Number.isFinite(options.maximumRequests)) {
213
+ throw new Error("--certify cannot be combined with --requests");
214
+ }
215
+ const runId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
216
+ const sessions = Array.from({ length: options.sessionCount }, (_, index) => ({
217
+ id: `bench-${runId}-${index}`,
218
+ visitorId: `visitor-${runId}-${index}`,
219
+ sequence: 0,
220
+ locationIndex: index % LOCATIONS.length,
221
+ userAgentIndex: index % USER_AGENTS.length,
222
+ }));
223
+ let nextRequest = 0;
224
+ let stopping = false;
225
+ let terminationReason = null;
226
+ let accepted = 0;
227
+ let duplicates = 0;
228
+ let rejected = 0;
229
+ let failures = 0;
230
+ let failedEvents = 0;
231
+ let completedRequests = 0;
232
+ let sentEvents = 0;
233
+ let sentHeartbeats = 0;
234
+ let sentBytes = 0;
235
+ let acceptedHeartbeats = 0;
236
+ let reconciliationReliable = true;
237
+ const errors = new Map();
238
+ const durations = [];
239
+
240
+ const buildRequest = (requestIndex) => {
241
+ const session = options.profile === "lean"
242
+ ? {
243
+ id: `bench-${runId}-${requestIndex}`,
244
+ visitorId: `visitor-${runId}-${requestIndex}`,
245
+ sequence: 0,
246
+ locationIndex: requestIndex % LOCATIONS.length,
247
+ userAgentIndex: requestIndex % USER_AGENTS.length,
248
+ }
249
+ : sessions[requestIndex % sessions.length];
250
+ const sequenceStart = session.sequence;
251
+ session.sequence += options.batchSize;
252
+ const now = Date.now();
253
+ const events = Array.from({ length: options.batchSize }, (_, index) =>
254
+ eventFor(options.profile, session, sequenceStart + index, now + index),
255
+ );
256
+ return {
257
+ body: JSON.stringify({ siteId: options.siteId, events }),
258
+ context: requestContext(session),
259
+ heartbeats: events.filter((event) => event.type === "heartbeat").length,
260
+ };
261
+ };
262
+
263
+ const dryRequest = buildRequest(0);
264
+ if (Buffer.byteLength(dryRequest.body) > 64 * 1024) {
265
+ throw new Error("generated request exceeds the 64 KiB HTTP limit");
266
+ }
267
+ if (options.dryRun) {
268
+ process.stdout.write(`${JSON.stringify({
269
+ profile: options.profile,
270
+ batchSize: options.batchSize,
271
+ requestBytes: Buffer.byteLength(dryRequest.body),
272
+ heartbeats: dryRequest.heartbeats,
273
+ location: requestContext(sessions[0]),
274
+ }, null, 2)}\n`);
275
+ return;
276
+ }
277
+ if (options.profile !== "lean") sessions[0].sequence = 0;
278
+ if (!options.url || !options.siteId) {
279
+ throw new Error("--url and --site-id are required unless --dry-run is used");
280
+ }
281
+
282
+ const stopForSignal = (signal) => {
283
+ stopping = true;
284
+ terminationReason = signal;
285
+ };
286
+ const stopForInterrupt = () => stopForSignal("SIGINT");
287
+ const stopForTermination = () => stopForSignal("SIGTERM");
288
+ process.once("SIGINT", stopForInterrupt);
289
+ process.once("SIGTERM", stopForTermination);
290
+
291
+ const wallClockStartedAt = Date.now();
292
+ const reconciliationFrom = Math.floor(wallClockStartedAt / 3_600_000) * 3_600_000;
293
+ const reconciliationTo = Math.ceil(
294
+ (wallClockStartedAt + options.durationSeconds * 1_000 + 60_000) / 3_600_000,
295
+ ) * 3_600_000 - 1;
296
+ const convex = options.convexUrl ? new ConvexHttpClient(options.convexUrl) : null;
297
+ const aggregateBefore = convex
298
+ ? await convex.query(overviewQuery, {
299
+ siteIds: [options.siteId],
300
+ from: reconciliationFrom,
301
+ to: reconciliationTo,
302
+ interval: "hour",
303
+ })
304
+ : null;
305
+ const startedAt = performance.now();
306
+ const deadline = startedAt + options.durationSeconds * 1_000;
307
+ const recordError = (key) => errors.set(key, (errors.get(key) ?? 0) + 1);
308
+ const worker = async () => {
309
+ while (!stopping) {
310
+ if (nextRequest >= options.maximumRequests) {
311
+ terminationReason ??= "request-limit";
312
+ return;
313
+ }
314
+ if (performance.now() >= deadline) {
315
+ terminationReason ??= "duration";
316
+ return;
317
+ }
318
+ const requestIndex = nextRequest++;
319
+ const request = buildRequest(requestIndex);
320
+ const requestBytes = Buffer.byteLength(request.body);
321
+ if (requestBytes > 64 * 1024) {
322
+ throw new Error(`generated request ${requestIndex} exceeds the 64 KiB HTTP limit`);
323
+ }
324
+ sentBytes += requestBytes;
325
+ sentEvents += options.batchSize;
326
+ sentHeartbeats += request.heartbeats;
327
+ const requestStartedAt = performance.now();
328
+ try {
329
+ const response = await globalThis.fetch(options.url, {
330
+ method: "POST",
331
+ headers: {
332
+ "Content-Type": "application/json",
333
+ Origin: options.origin,
334
+ ...request.context,
335
+ },
336
+ body: request.body,
337
+ signal: globalThis.AbortSignal.timeout(30_000),
338
+ });
339
+ if (!response.ok) {
340
+ failures += 1;
341
+ failedEvents += options.batchSize;
342
+ const body = await response.text();
343
+ let code = body.slice(0, 160);
344
+ try {
345
+ const parsed = JSON.parse(body);
346
+ code = parsed?.error?.code ?? parsed?.error?.message ?? code;
347
+ } catch {
348
+ // Preserve the bounded response text.
349
+ }
350
+ recordError(`${response.status}:${code}`);
351
+ } else {
352
+ const result = await response.json();
353
+ if (
354
+ ![result.accepted, result.duplicates, result.rejected].every(
355
+ (value) => Number.isSafeInteger(value) && value >= 0,
356
+ ) ||
357
+ result.accepted + result.duplicates + result.rejected !== options.batchSize
358
+ ) {
359
+ throw new Error("response has invalid event accounting");
360
+ }
361
+ accepted += result.accepted;
362
+ duplicates += result.duplicates;
363
+ rejected += result.rejected;
364
+ if (result.accepted === options.batchSize) {
365
+ acceptedHeartbeats += request.heartbeats;
366
+ } else {
367
+ reconciliationReliable = false;
368
+ }
369
+ }
370
+ } catch (error) {
371
+ failures += 1;
372
+ failedEvents += options.batchSize;
373
+ recordError(`fetch:${error instanceof Error ? error.message : "unknown"}`);
374
+ } finally {
375
+ durations.push(performance.now() - requestStartedAt);
376
+ completedRequests += 1;
377
+ }
378
+ }
379
+ };
380
+
381
+ const progress = setInterval(() => {
382
+ const elapsedSeconds = Math.max(0.001, (performance.now() - startedAt) / 1_000);
383
+ process.stderr.write(
384
+ `[${options.label}] ${Math.round(elapsedSeconds)}s ` +
385
+ `${accepted} accepted ${Math.round(accepted / elapsedSeconds)} events/s ` +
386
+ `${failures} failures\n`,
387
+ );
388
+ }, options.progressSeconds * 1_000);
389
+ progress.unref();
390
+ await Promise.all(Array.from({ length: options.concurrency }, worker));
391
+ clearInterval(progress);
392
+
393
+ const elapsedMs = performance.now() - startedAt;
394
+ terminationReason ??= performance.now() >= deadline ? "duration" : "completed";
395
+ const aggregateAfter = convex
396
+ ? await convex.query(overviewQuery, {
397
+ siteIds: [options.siteId],
398
+ from: reconciliationFrom,
399
+ to: reconciliationTo,
400
+ interval: "hour",
401
+ })
402
+ : null;
403
+ process.removeListener("SIGINT", stopForInterrupt);
404
+ process.removeListener("SIGTERM", stopForTermination);
405
+ const durationCompleted = terminationReason === "duration";
406
+ durations.sort((left, right) => left - right);
407
+ const storedEvents = accepted - acceptedHeartbeats;
408
+ const reconciliation = aggregateBefore && aggregateAfter
409
+ ? {
410
+ scope: "site-wide-delta",
411
+ reliable: reconciliationReliable && failures === 0 && rejected === 0,
412
+ expectedStoredEvents: storedEvents,
413
+ aggregateEventsDelta:
414
+ aggregateAfter.totals.events - aggregateBefore.totals.events,
415
+ matches:
416
+ reconciliationReliable &&
417
+ failures === 0 &&
418
+ rejected === 0 &&
419
+ aggregateAfter.totals.events - aggregateBefore.totals.events === storedEvents,
420
+ }
421
+ : null;
422
+ const cleanRun =
423
+ failures === 0 && duplicates === 0 && rejected === 0 && durationCompleted;
424
+ const reconciliationPassed = Boolean(
425
+ reconciliation?.reliable && reconciliation.matches,
426
+ );
427
+ const certificationPassed =
428
+ options.certify && cleanRun && reconciliationPassed;
429
+ const result = {
430
+ label: options.label,
431
+ runId,
432
+ profile: options.profile,
433
+ targetDurationSeconds: options.durationSeconds,
434
+ elapsedMs: Math.round(elapsedMs),
435
+ terminationReason,
436
+ durationCompleted,
437
+ certification: {
438
+ requested: options.certify,
439
+ passed: certificationPassed,
440
+ minimumDurationSeconds: 600,
441
+ },
442
+ concurrency: options.concurrency,
443
+ batchSize: options.batchSize,
444
+ sessionPool: options.profile === "lean" ? "unique-per-request" : options.sessionCount,
445
+ maximumRequests: Number.isFinite(options.maximumRequests)
446
+ ? options.maximumRequests
447
+ : null,
448
+ requests: { started: nextRequest, completed: completedRequests, failures },
449
+ events: {
450
+ sent: sentEvents,
451
+ heartbeats: sentHeartbeats,
452
+ accepted,
453
+ stored: storedEvents,
454
+ duplicates,
455
+ rejected,
456
+ failed: failedEvents,
457
+ },
458
+ errors: Object.fromEntries(errors),
459
+ throughput: {
460
+ eventsPerSecond: Math.round((accepted * 1_000) / elapsedMs),
461
+ storedEventsPerSecond: reconciliationReliable
462
+ ? Math.round((storedEvents * 1_000) / elapsedMs)
463
+ : null,
464
+ estimatedEventsPerDay: durationCompleted
465
+ ? Math.round((accepted * 86_400_000) / elapsedMs)
466
+ : null,
467
+ estimatedStoredEventsPerDay: durationCompleted && reconciliationReliable
468
+ ? Math.round((storedEvents * 86_400_000) / elapsedMs)
469
+ : null,
470
+ certifiedEventsPerDay: certificationPassed
471
+ ? Math.round((accepted * 86_400_000) / elapsedMs)
472
+ : null,
473
+ certifiedStoredEventsPerDay: certificationPassed
474
+ ? Math.round((storedEvents * 86_400_000) / elapsedMs)
475
+ : null,
476
+ requestBytesPerSecond: Math.round((sentBytes * 1_000) / elapsedMs),
477
+ },
478
+ latencyMs: {
479
+ p50: Math.round(percentile(durations, 0.5)),
480
+ p95: Math.round(percentile(durations, 0.95)),
481
+ p99: Math.round(percentile(durations, 0.99)),
482
+ max: Math.round(durations.at(-1) ?? 0),
483
+ },
484
+ reconciliation,
485
+ };
486
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
487
+ if (
488
+ failures > 0 ||
489
+ duplicates > 0 ||
490
+ rejected > 0 ||
491
+ !durationCompleted ||
492
+ (options.convexUrl && !reconciliationPassed) ||
493
+ (options.certify && !certificationPassed)
494
+ ) {
495
+ process.exitCode = 1;
496
+ }
497
+ }
498
+
499
+ await main();
@@ -11,12 +11,14 @@
11
11
  import type * as affiliates from "../affiliates.js";
12
12
  import type * as constants from "../constants.js";
13
13
  import type * as errors from "../errors.js";
14
+ import type * as eventStore from "../eventStore.js";
14
15
  import type * as funnels from "../funnels.js";
15
16
  import type * as goals from "../goals.js";
16
17
  import type * as guards from "../guards.js";
17
18
  import type * as http from "../http.js";
18
19
  import type * as ingest from "../ingest.js";
19
20
  import type * as live from "../live.js";
21
+ import type * as migrations from "../migrations.js";
20
22
  import type * as reports from "../reports.js";
21
23
  import type * as retention from "../retention.js";
22
24
  import type * as sanitize from "../sanitize.js";
@@ -34,12 +36,14 @@ const fullApi: ApiFromModules<{
34
36
  affiliates: typeof affiliates;
35
37
  constants: typeof constants;
36
38
  errors: typeof errors;
39
+ eventStore: typeof eventStore;
37
40
  funnels: typeof funnels;
38
41
  goals: typeof goals;
39
42
  guards: typeof guards;
40
43
  http: typeof http;
41
44
  ingest: typeof ingest;
42
45
  live: typeof live;
46
+ migrations: typeof migrations;
43
47
  reports: typeof reports;
44
48
  retention: typeof retention;
45
49
  sanitize: typeof sanitize;
@@ -73,4 +77,6 @@ export const internal: FilterApi<
73
77
  FunctionReference<any, "internal">
74
78
  > = anyApi as any;
75
79
 
76
- export const components = componentsGeneric() as unknown as {};
80
+ export const components = componentsGeneric() as unknown as {
81
+ migrations: import("@convex-dev/migrations/_generated/component.js").ComponentApi<"migrations">;
82
+ };
@@ -1,13 +1,17 @@
1
- export const MAX_BATCH_EVENTS = 20;
1
+ export const MAX_BATCH_EVENTS = 50;
2
+ export const MAX_BATCH_EVENT_GROUPS = 4;
2
3
  export const MAX_BATCH_BYTES = 64 * 1024;
3
4
  export const MAX_EVENTS_PER_SESSION_WINDOW = 120;
4
5
  export const RATE_LIMIT_WINDOW_MS = 60_000;
5
6
  export const MAX_BYTES_PER_SESSION_WINDOW = 256 * 1024;
6
- export const SITE_INGEST_SHARDS = 8;
7
- export const MAX_SITE_EVENTS_PER_SHARD_WINDOW = 120;
7
+ export const SITE_INGEST_SHARDS = 4_096;
8
+ export const MAX_SITE_EVENTS_PER_SHARD_WINDOW = 300;
8
9
  export const MAX_SITE_BYTES_PER_SHARD_WINDOW = 512 * 1024;
9
10
  export const LIVE_SESSION_TTL_MS = 60_000;
10
- export const AGGREGATE_SHARDS = 16;
11
+ export const LIVE_SWEEP_INTERVAL_MS = 5_000;
12
+ export const AGGREGATE_SHARDS = 128;
13
+ export const HOURLY_AGGREGATE_SHARDS = 256;
14
+ export const SECONDARY_AGGREGATE_SHARDS = 16;
11
15
  export const DIMENSION_SLOTS = 16;
12
16
  export const MAX_SITE_DOMAINS = 32;
13
17
  export const MAX_SITES_PER_OWNER = 100;
@@ -20,9 +24,10 @@ export const MAX_REPORT_SITES = 10;
20
24
  export const MAX_REPORT_RANGE_DAYS = 366;
21
25
  export const MAX_LIVE_VISITORS = 500;
22
26
  export const MAX_JOURNEY_EVENTS = 500;
27
+ export const JOURNEY_READ_BYTE_RESERVE = 4 * 1024 * 1024;
28
+ export const JOURNEY_READ_DOCUMENT_RESERVE = 1_000;
23
29
  export const MAX_RETENTION_BATCH = 500;
24
- export const MAX_OVERVIEW_AGGREGATE_ROWS = 12_000;
30
+ export const MAX_OVERVIEW_AGGREGATE_ROWS = 13_000;
25
31
 
26
32
  export const DAY_MS = 86_400_000;
27
33
  export const HOUR_MS = 3_600_000;
28
- export const MAX_MANUAL_PAGINATION_OFFSET = 1_000;
@@ -1,3 +1,7 @@
1
+ import migrations from "@convex-dev/migrations/convex.config.js";
1
2
  import { defineComponent } from "convex/server";
2
3
 
3
- export default defineComponent("rastroAnalytics");
4
+ const component = defineComponent("rastroAnalytics");
5
+ component.use(migrations);
6
+
7
+ export default component;