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

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 (68) hide show
  1. package/README.md +85 -18
  2. package/dist/component/_generated/api.d.ts +2 -0
  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 +9 -6
  6. package/dist/component/constants.d.ts.map +1 -1
  7. package/dist/component/constants.js +9 -6
  8. package/dist/component/constants.js.map +1 -1
  9. package/dist/component/eventStore.d.ts +94 -0
  10. package/dist/component/eventStore.d.ts.map +1 -0
  11. package/dist/component/eventStore.js +304 -0
  12. package/dist/component/eventStore.js.map +1 -0
  13. package/dist/component/goals.d.ts.map +1 -1
  14. package/dist/component/goals.js +17 -4
  15. package/dist/component/goals.js.map +1 -1
  16. package/dist/component/http.d.ts.map +1 -1
  17. package/dist/component/http.js +39 -7
  18. package/dist/component/http.js.map +1 -1
  19. package/dist/component/ingest.d.ts.map +1 -1
  20. package/dist/component/ingest.js +520 -207
  21. package/dist/component/ingest.js.map +1 -1
  22. package/dist/component/live.d.ts +8 -0
  23. package/dist/component/live.d.ts.map +1 -1
  24. package/dist/component/live.js +72 -8
  25. package/dist/component/live.js.map +1 -1
  26. package/dist/component/reports.d.ts +16 -16
  27. package/dist/component/reports.d.ts.map +1 -1
  28. package/dist/component/reports.js +250 -66
  29. package/dist/component/reports.js.map +1 -1
  30. package/dist/component/retention.d.ts.map +1 -1
  31. package/dist/component/retention.js +15 -10
  32. package/dist/component/retention.js.map +1 -1
  33. package/dist/component/sanitize.d.ts.map +1 -1
  34. package/dist/component/sanitize.js +11 -3
  35. package/dist/component/sanitize.js.map +1 -1
  36. package/dist/component/schema.d.ts +101 -10
  37. package/dist/component/schema.js +30 -8
  38. package/dist/component/schema.js.map +1 -1
  39. package/dist/component/sites.d.ts.map +1 -1
  40. package/dist/component/sites.js +27 -18
  41. package/dist/component/sites.js.map +1 -1
  42. package/dist/component/validators.d.ts +49 -0
  43. package/dist/component/validators.d.ts.map +1 -1
  44. package/dist/component/validators.js +11 -0
  45. package/dist/component/validators.js.map +1 -1
  46. package/dist/tracker/generated.d.ts +4 -4
  47. package/dist/tracker/generated.d.ts.map +1 -1
  48. package/dist/tracker/generated.js +4 -4
  49. package/dist/tracker/generated.js.map +1 -1
  50. package/dist/tracker/tracker.js +33 -22
  51. package/dist/tracker/tracker.js.map +1 -1
  52. package/dist/tracker.min.js +1 -1
  53. package/docs/benchmarks/2026-08-20-realistic.md +171 -0
  54. package/package.json +10 -4
  55. package/scripts/benchmark-ingest.mjs +499 -0
  56. package/src/component/_generated/api.ts +2 -0
  57. package/src/component/constants.ts +9 -6
  58. package/src/component/eventStore.ts +465 -0
  59. package/src/component/goals.ts +25 -4
  60. package/src/component/http.ts +49 -6
  61. package/src/component/ingest.ts +671 -215
  62. package/src/component/live.ts +83 -8
  63. package/src/component/reports.ts +320 -82
  64. package/src/component/retention.ts +15 -12
  65. package/src/component/sanitize.ts +10 -3
  66. package/src/component/schema.ts +33 -8
  67. package/src/component/sites.ts +31 -18
  68. package/src/component/validators.ts +12 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Privacy-first, real-time web analytics built as an isolated Convex component.
6
6
 
7
- Amage Rastro combines a 991-byte gzip browser tracker with reactive traffic
7
+ Amage Rastro combines a 1,022-byte gzip browser tracker with reactive traffic
8
8
  metrics, a live visitor atlas, journey timelines, trusted payment attribution,
9
9
  goals, funnels, affiliates, and multi-site reports. Data and functions stay
10
10
  inside your Convex deployment.
@@ -24,7 +24,7 @@ inside your Convex deployment.
24
24
  | Cross-site analytics | Reports over up to ten authorized site IDs with a preloaded pseudonymous visitor ID |
25
25
  | Auto-captured clicks | Anchors, buttons, outbound links, and explicit `data-rastro-event` controls |
26
26
  | Cookieless mode | Session-scoped pseudonymous IDs in `sessionStorage`; no cookies |
27
- | Tiny tracker | 1,890 bytes raw, 991 bytes gzip; the test suite enforces `< 1,024` bytes |
27
+ | Tiny tracker | 1,909 bytes raw, 1,022 bytes gzip; the test suite enforces `< 1,024` bytes |
28
28
 
29
29
  ## Install
30
30
 
@@ -172,6 +172,12 @@ Properties accept bounded string, number, boolean, or null values. Never send
172
172
  emails, names, form values, secrets, or other personal data as analytics
173
173
  properties.
174
174
 
175
+ Browser telemetry exact retries are idempotent within
176
+ `(siteId, sessionId, event-minute, eventId)`. Reusing an event ID in another
177
+ session or with a timestamp in another minute is allowed. The tracker preserves
178
+ timestamps when retrying. Trusted payment event IDs remain separately
179
+ payload-bound by the site's financial ledger.
180
+
175
181
  ## Attribute trusted payments
176
182
 
177
183
  Browser-created conversion events are untrusted telemetry and never affect
@@ -271,25 +277,86 @@ The `Rastro` class wraps the component boundary for host functions:
271
277
  - Retention: `cleanup`
272
278
 
273
279
  Every public function has argument and return validators. Growing reads are
274
- indexed and bounded. Component pagination uses validated opaque manual cursors
275
- because native `.paginate()` is not supported inside Convex components.
280
+ indexed and bounded. Component pagination uses scope-bound opaque keyset cursors
281
+ because native `.paginate()` is not supported inside Convex components; page
282
+ cost stays constant instead of increasing with an offset.
276
283
 
277
284
  ## Operational limits
278
285
 
279
286
  - HTTP body: 64 KiB
280
- - Events per batch: 20
281
- - Aggregate shards: 16, derived from stable session/visitor identity
282
- - Site ingress budget: eight deterministic per-minute shards with bounded event
283
- and byte caps
284
- - Live session timeout: 60 seconds with one self-rescheduling expiry chain per
285
- session
287
+ - Events per batch: 50
288
+ - Session-minute groups per batch: 4
289
+ - Active goals per exact matcher: 1
290
+ - Primary aggregate shards: 256 hourly and 128 daily, derived from stable
291
+ session identity
292
+ - Funnel and affiliate aggregate shards: 16
293
+ - Site ingress budget: 4,096 deterministic per-minute shards, each capped at
294
+ 300 events and 512 KiB (1,228,800 admitted events/minute at uniform load)
295
+ - Rate-window storage: one reusable row per active session and site shard
296
+ - Live session timeout: 60 seconds with one bounded five-second expiry sweep per
297
+ site; legacy per-session jobs hand off to that sweep during the cutover
286
298
  - Dashboard overview: up to ten sites and bounded report ranges
299
+ - Hourly overview: up to 24 hours; wider ranges use daily buckets
287
300
  - Retention cleanup: bounded batches; schedule host maintenance for the data
288
- classes your policy retains
301
+ classes your policy retains. The `events` cleanup kind covers both legacy
302
+ event rows and new event batches and processes at most 32 heavy batches per
303
+ transaction.
304
+
305
+ Capacity claims use declared workload profiles. Complete release evidence
306
+ reports ten-minute `lean`, `realistic`, `heavy`, and browser `feature` profiles
307
+ separately. The browser `feature` profile exercises configured goals, funnels,
308
+ and affiliate attribution; trusted conversions require a separate server-side
309
+ driver and certification result.
310
+
311
+ The historical development evidence and its certification caveats are recorded
312
+ in [`docs/benchmarks/2026-08-20-realistic.md`](docs/benchmarks/2026-08-20-realistic.md).
313
+
314
+ From a repository checkout, run a duration-based profile against a confirmed
315
+ non-production deployment and a dedicated idle benchmark site:
316
+
317
+ ```bash
318
+ npm run --silent benchmark:ingest -- \
319
+ --url https://example.site/rastro/events \
320
+ --convex-url https://example.convex-backend \
321
+ --site-id your_site_id \
322
+ --origin https://your-allowed-origin.example \
323
+ --profile realistic \
324
+ --duration 600 \
325
+ --concurrency 100 \
326
+ --certify
327
+ ```
289
328
 
290
- The ingestion transaction folds metric/dimension deltas and writes each touched
291
- aggregate bucket at most once per batch. Click/outbound/browser-conversion
292
- telemetry skips goal and funnel work that cannot match those event types.
329
+ Profiles are `lean`, `realistic`, `heavy`, and `feature`. The realistic profile
330
+ reuses sessions and sends geographic and user-agent headers so the same load is
331
+ visible in Live Atlas. Only `feature` intentionally matches the demo goal,
332
+ funnel, and affiliate definitions; it does not call the trusted conversion API.
333
+ Use `--dry-run` to inspect generated request size without sending traffic. When
334
+ `--convex-url` is supplied, it must identify the same deployment as `--url` and
335
+ expose this repository's `example:overview` query. The final reconciliation is
336
+ a site-wide before/after delta, so unrelated traffic on that site invalidates
337
+ the result. Reports distinguish accepted ingress, heartbeats, and stored events.
338
+ Shorter completed runs report extrapolated daily estimates only. `--certify`
339
+ requires at least ten minutes and successful reconciliation before emitting
340
+ certified daily capacity fields or exiting successfully as a certification.
341
+
342
+ The ingestion transaction folds metric/dimension deltas, session rate counters,
343
+ site-shard counters, session/live state, visitor attribution, and exact funnel
344
+ progress before writing. Accepted non-heartbeat telemetry from one request is
345
+ grouped into one immutable raw document per session and event-minute, without
346
+ duplicating session geo or user-agent context. A 50-event same-session,
347
+ same-minute baseline stays within 10 document writes. Each touched aggregate
348
+ bucket is written at most once per batch, and dimension overflow folds into an
349
+ explicit `(other)` bucket instead of relabeling old counts.
350
+ Click/outbound/browser-conversion telemetry skips goal and funnel work that
351
+ cannot match those event types.
352
+
353
+ This is an additive storage migration: existing `events` rows are not deleted or
354
+ backfilled. Journey and trusted-conversion reads merge legacy rows with new
355
+ `eventBatches` rows. Journey `_id` values are stable strings for both formats,
356
+ and session-journey cursors use timestamp, creation time, and ID so pagination
357
+ matches both storage indexes. The compatibility logic is isolated in
358
+ `src/component/eventStore.ts`; ingestion, reports, and retention do not branch
359
+ on physical telemetry storage.
293
360
 
294
361
  ## Privacy contract
295
362
 
@@ -336,10 +403,10 @@ cd example && npx vite build
336
403
  ```
337
404
 
338
405
  The test suite covers origin enforcement, event/privacy sanitization,
339
- idempotency, session/site rate limits, live expiry scheduling, folded aggregate
340
- writes, goals, funnels, trusted payments, currency safety, affiliate commission,
341
- cross-site journeys, manual pagination, retention, HTTP routes, React
342
- integration, and tracker size/behavior.
406
+ idempotency, reusable session/site rate limits, bounded live expiry chains,
407
+ folded aggregate writes, goals, funnels, trusted payments, currency safety,
408
+ affiliate commission, cross-site journeys, keyset pagination beyond 1,000 rows,
409
+ retention, HTTP routes, React integration, and tracker size/behavior.
343
410
 
344
411
  ## Synapse demo
345
412
 
@@ -9,6 +9,7 @@
9
9
  import type * as affiliates from "../affiliates.js";
10
10
  import type * as constants from "../constants.js";
11
11
  import type * as errors from "../errors.js";
12
+ import type * as eventStore from "../eventStore.js";
12
13
  import type * as funnels from "../funnels.js";
13
14
  import type * as goals from "../goals.js";
14
15
  import type * as guards from "../guards.js";
@@ -25,6 +26,7 @@ declare const fullApi: ApiFromModules<{
25
26
  affiliates: typeof affiliates;
26
27
  constants: typeof constants;
27
28
  errors: typeof errors;
29
+ eventStore: typeof eventStore;
28
30
  funnels: typeof funnels;
29
31
  goals: typeof goals;
30
32
  guards: typeof guards;
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/component/_generated/api.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,KAAK,UAAU,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,KAAK,SAAS,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,KAAK,MAAM,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,KAAK,OAAO,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,KAAK,MAAM,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,KAAK,IAAI,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,KAAK,IAAI,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,KAAK,OAAO,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,KAAK,SAAS,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,KAAK,UAAU,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EACV,cAAc,EACd,SAAS,EACT,iBAAiB,EAClB,MAAM,eAAe,CAAC;AAGvB,QAAA,MAAM,OAAO,EAAE,cAAc,CAAC;IAC5B,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxB,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxB,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,UAAU,EAAE,OAAO,UAAU,CAAC;CAC/B,CAAiB,CAAC;AAEnB;;;;;;;GAOG;AACH,eAAO,MAAM,GAAG,EAAE,SAAS,CACzB,OAAO,OAAO,EACd,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CACjB,CAAC;AAElB;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAC9B,OAAO,OAAO,EACd,iBAAiB,CAAC,GAAG,EAAE,UAAU,CAAC,CACnB,CAAC;AAElB,eAAO,MAAM,UAAU,EAAqC,EAAE,CAAC"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/component/_generated/api.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,KAAK,UAAU,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,KAAK,SAAS,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,KAAK,MAAM,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,KAAK,UAAU,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,KAAK,OAAO,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,KAAK,MAAM,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,KAAK,IAAI,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,KAAK,IAAI,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,KAAK,OAAO,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,KAAK,SAAS,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,KAAK,UAAU,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EACV,cAAc,EACd,SAAS,EACT,iBAAiB,EAClB,MAAM,eAAe,CAAC;AAGvB,QAAA,MAAM,OAAO,EAAE,cAAc,CAAC;IAC5B,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,OAAO,EAAE,OAAO,OAAO,CAAC;IACxB,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxB,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,UAAU,EAAE,OAAO,UAAU,CAAC;CAC/B,CAAiB,CAAC;AAEnB;;;;;;;GAOG;AACH,eAAO,MAAM,GAAG,EAAE,SAAS,CACzB,OAAO,OAAO,EACd,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CACjB,CAAC;AAElB;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAC9B,OAAO,OAAO,EACd,iBAAiB,CAAC,GAAG,EAAE,UAAU,CAAC,CACnB,CAAC;AAElB,eAAO,MAAM,UAAU,EAAqC,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/component/_generated/api.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;;;;;GAOG;AAsBH,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE1D,MAAM,OAAO,GAeR,MAAa,CAAC;AAEnB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,GAAG,GAGZ,MAAa,CAAC;AAElB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAGjB,MAAa,CAAC;AAElB,MAAM,CAAC,MAAM,UAAU,GAAG,iBAAiB,EAAmB,CAAC"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/component/_generated/api.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;;;;;GAOG;AAuBH,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE1D,MAAM,OAAO,GAgBR,MAAa,CAAC;AAEnB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,GAAG,GAGZ,MAAa,CAAC;AAElB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAGjB,MAAa,CAAC;AAElB,MAAM,CAAC,MAAM,UAAU,GAAG,iBAAiB,EAAmB,CAAC"}
@@ -1,13 +1,17 @@
1
- export declare const MAX_BATCH_EVENTS = 20;
1
+ export declare const MAX_BATCH_EVENTS = 50;
2
+ export declare const MAX_BATCH_EVENT_GROUPS = 4;
2
3
  export declare const MAX_BATCH_BYTES: number;
3
4
  export declare const MAX_EVENTS_PER_SESSION_WINDOW = 120;
4
5
  export declare const RATE_LIMIT_WINDOW_MS = 60000;
5
6
  export declare const MAX_BYTES_PER_SESSION_WINDOW: number;
6
- export declare const SITE_INGEST_SHARDS = 8;
7
- export declare const MAX_SITE_EVENTS_PER_SHARD_WINDOW = 120;
7
+ export declare const SITE_INGEST_SHARDS = 4096;
8
+ export declare const MAX_SITE_EVENTS_PER_SHARD_WINDOW = 300;
8
9
  export declare const MAX_SITE_BYTES_PER_SHARD_WINDOW: number;
9
10
  export declare const LIVE_SESSION_TTL_MS = 60000;
10
- export declare const AGGREGATE_SHARDS = 16;
11
+ export declare const LIVE_SWEEP_INTERVAL_MS = 5000;
12
+ export declare const AGGREGATE_SHARDS = 128;
13
+ export declare const HOURLY_AGGREGATE_SHARDS = 256;
14
+ export declare const SECONDARY_AGGREGATE_SHARDS = 16;
11
15
  export declare const DIMENSION_SLOTS = 16;
12
16
  export declare const MAX_SITE_DOMAINS = 32;
13
17
  export declare const MAX_SITES_PER_OWNER = 100;
@@ -21,8 +25,7 @@ export declare const MAX_REPORT_RANGE_DAYS = 366;
21
25
  export declare const MAX_LIVE_VISITORS = 500;
22
26
  export declare const MAX_JOURNEY_EVENTS = 500;
23
27
  export declare const MAX_RETENTION_BATCH = 500;
24
- export declare const MAX_OVERVIEW_AGGREGATE_ROWS = 12000;
28
+ export declare const MAX_OVERVIEW_AGGREGATE_ROWS = 13000;
25
29
  export declare const DAY_MS = 86400000;
26
30
  export declare const HOUR_MS = 3600000;
27
- export declare const MAX_MANUAL_PAGINATION_OFFSET = 1000;
28
31
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/component/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,eAAe,QAAY,CAAC;AACzC,eAAO,MAAM,6BAA6B,MAAM,CAAC;AACjD,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAC3C,eAAO,MAAM,4BAA4B,QAAa,CAAC;AACvD,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,gCAAgC,MAAM,CAAC;AACpD,eAAO,MAAM,+BAA+B,QAAa,CAAC;AAC1D,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,eAAO,MAAM,MAAM,WAAa,CAAC;AACjC,eAAO,MAAM,OAAO,UAAY,CAAC;AACjC,eAAO,MAAM,4BAA4B,OAAQ,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/component/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC,eAAO,MAAM,eAAe,QAAY,CAAC;AACzC,eAAO,MAAM,6BAA6B,MAAM,CAAC;AACjD,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAC3C,eAAO,MAAM,4BAA4B,QAAa,CAAC;AACvD,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AACxC,eAAO,MAAM,gCAAgC,MAAM,CAAC;AACpD,eAAO,MAAM,+BAA+B,QAAa,CAAC;AAC1D,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,sBAAsB,OAAQ,CAAC;AAC5C,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAC7C,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,eAAO,MAAM,MAAM,WAAa,CAAC;AACjC,eAAO,MAAM,OAAO,UAAY,CAAC"}
@@ -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;
@@ -21,8 +25,7 @@ export const MAX_REPORT_RANGE_DAYS = 366;
21
25
  export const MAX_LIVE_VISITORS = 500;
22
26
  export const MAX_JOURNEY_EVENTS = 500;
23
27
  export const MAX_RETENTION_BATCH = 500;
24
- export const MAX_OVERVIEW_AGGREGATE_ROWS = 12_000;
28
+ export const MAX_OVERVIEW_AGGREGATE_ROWS = 13_000;
25
29
  export const DAY_MS = 86_400_000;
26
30
  export const HOUR_MS = 3_600_000;
27
- export const MAX_MANUAL_PAGINATION_OFFSET = 1_000;
28
31
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/component/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,GAAG,IAAI,CAAC;AACzC,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,CAAC;AACjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAC3C,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,GAAG,IAAI,CAAC;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AACpC,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,CAAC;AACpD,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,GAAG,IAAI,CAAC;AAC1D,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAClC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AACrC,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AACvC,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AACjC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AACzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AACrC,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AACtC,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAElD,MAAM,CAAC,MAAM,MAAM,GAAG,UAAU,CAAC;AACjC,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC;AACjC,MAAM,CAAC,MAAM,4BAA4B,GAAG,KAAK,CAAC"}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/component/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AACxC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,GAAG,IAAI,CAAC;AACzC,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,CAAC;AACjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAC3C,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,GAAG,IAAI,CAAC;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AACxC,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,CAAC;AACpD,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,GAAG,IAAI,CAAC;AAC1D,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAC;AAC5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AACpC,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,CAAC;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAClC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AACrC,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AACvC,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AACjC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AACzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AACrC,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AACtC,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAElD,MAAM,CAAC,MAAM,MAAM,GAAG,UAAU,CAAC;AACjC,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC"}
@@ -0,0 +1,94 @@
1
+ import type { Doc, Id } from "./_generated/dataModel.js";
2
+ import type { MutationCtx, QueryCtx } from "./_generated/server.js";
3
+ import type { TrackerEvent } from "./validators.js";
4
+ export type TelemetryMatch = {
5
+ sessionId: string;
6
+ visitorId: string;
7
+ country?: string;
8
+ city?: string;
9
+ latitude?: number;
10
+ longitude?: number;
11
+ browser?: string;
12
+ os?: string;
13
+ device?: string;
14
+ };
15
+ type BufferedEventBatch = {
16
+ siteId: Id<"sites">;
17
+ sessionId: string;
18
+ visitorId: string;
19
+ bucketStart: number;
20
+ events: TrackerEvent[];
21
+ };
22
+ export type EventBatchBuffer = Map<string, BufferedEventBatch>;
23
+ export type JourneyCursor = {
24
+ timestamp: number;
25
+ creationTime: number;
26
+ id: string;
27
+ };
28
+ export type RawJourneyEvent = Omit<Doc<"events">, "_id"> & {
29
+ _id: string;
30
+ };
31
+ export declare function eventBucketStart(timestamp: number): number;
32
+ export declare function eventBatchKey(sessionId: string, bucketStart: number): string;
33
+ export declare function inspectTelemetryMinute(ctx: MutationCtx, input: {
34
+ siteId: Id<"sites">;
35
+ sessionId: string;
36
+ bucketStart: number;
37
+ candidateEventIds: Set<string>;
38
+ }): Promise<{
39
+ eventIds: Set<string>;
40
+ persistedCount: number;
41
+ }>;
42
+ export declare function findTelemetry(ctx: MutationCtx, siteId: Id<"sites">, event: TrackerEvent): Promise<{
43
+ match: TelemetryMatch | null;
44
+ saturated: boolean;
45
+ }>;
46
+ export declare function bufferEventBatch(siteId: Id<"sites">, event: TrackerEvent, buffer: EventBatchBuffer): void;
47
+ export declare function insertEventBatch(ctx: MutationCtx, siteId: Id<"sites">, event: TrackerEvent): Promise<void>;
48
+ export declare function flushEventBatches(ctx: MutationCtx, buffer: EventBatchBuffer): Promise<void>;
49
+ export declare function loadSessionJourneyEvents(ctx: QueryCtx, input: {
50
+ siteId: Id<"sites">;
51
+ sessionId: string;
52
+ from: number;
53
+ to: number;
54
+ cursor: JourneyCursor | null;
55
+ resultLimit: number;
56
+ }): Promise<RawJourneyEvent[]>;
57
+ export declare function compareJourneyEvents(left: Pick<RawJourneyEvent, "timestamp" | "_creationTime" | "_id">, right: Pick<RawJourneyEvent, "timestamp" | "_creationTime" | "_id">): number;
58
+ export declare function hydrateEventContexts(ctx: QueryCtx, rows: RawJourneyEvent[]): Promise<{
59
+ _creationTime: number;
60
+ _id: string;
61
+ country?: string | undefined;
62
+ city?: string | undefined;
63
+ latitude?: number | undefined;
64
+ longitude?: number | undefined;
65
+ browser: string;
66
+ os: string;
67
+ device: string;
68
+ affiliateSlug?: string | undefined;
69
+ currency?: string | undefined;
70
+ eventId: string;
71
+ href?: string | undefined;
72
+ name?: string | undefined;
73
+ path: string;
74
+ properties?: Record<string, string | number | boolean | null> | undefined;
75
+ referrer?: string | undefined;
76
+ revenueCents?: number | undefined;
77
+ sequence: number;
78
+ sessionId: string;
79
+ siteId: import("convex/values").GenericId<"sites">;
80
+ target?: string | undefined;
81
+ timestamp: number;
82
+ type: "click" | "conversion" | "custom" | "heartbeat" | "outbound" | "pageview";
83
+ visitorId: string;
84
+ }[]>;
85
+ export declare function deleteTelemetryBefore(ctx: MutationCtx, input: {
86
+ siteId: Id<"sites">;
87
+ before: number;
88
+ batchSize: number;
89
+ }): Promise<{
90
+ deleted: number;
91
+ hasMore: boolean;
92
+ }>;
93
+ export {};
94
+ //# sourceMappingURL=eventStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventStore.d.ts","sourceRoot":"","sources":["../../src/component/eventStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAKpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;AAE/D,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3E,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE;IACL,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAChC;;;GA0BF;AAED,wBAAsB,aAAa,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,EACnB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC;IAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CAiC/D;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,EACnB,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,gBAAgB,QAmBzB;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,EACnB,KAAK,EAAE,YAAY,iBASpB;AAED,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,gBAAgB,iBAKzB;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE;IACL,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,eAAe,EAAE,CAAC,CAqG5B;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,eAAe,GAAG,KAAK,CAAC,EAClE,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,eAAe,GAAG,KAAK,CAAC,GAClE,MAAM,CAIR;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,eAAe,EAAE;;SA/PyC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;KAwSvE;AAED,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE;IACL,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;;;GA+BF"}
@@ -0,0 +1,304 @@
1
+ import { MAX_EVENTS_PER_SESSION_WINDOW, RATE_LIMIT_WINDOW_MS, } from "./constants.js";
2
+ export function eventBucketStart(timestamp) {
3
+ return Math.floor(timestamp / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
4
+ }
5
+ export function eventBatchKey(sessionId, bucketStart) {
6
+ return JSON.stringify([sessionId, bucketStart]);
7
+ }
8
+ export async function inspectTelemetryMinute(ctx, input) {
9
+ const [legacyEvents, batches] = await loadTelemetryMinute(ctx, input);
10
+ const eventIds = new Set([
11
+ ...legacyEvents.map((event) => event.eventId),
12
+ ...batches.flatMap((batch) => batch.events.map((event) => event.eventId)),
13
+ ]);
14
+ if (legacyEvents.length > MAX_EVENTS_PER_SESSION_WINDOW) {
15
+ await Promise.all([...input.candidateEventIds]
16
+ .filter((eventId) => !eventIds.has(eventId))
17
+ .map(async (eventId) => {
18
+ const legacy = await findLegacyTelemetry(ctx, {
19
+ ...input,
20
+ eventId,
21
+ });
22
+ if (legacy)
23
+ eventIds.add(eventId);
24
+ }));
25
+ }
26
+ return {
27
+ eventIds,
28
+ persistedCount: telemetryMinuteCount(legacyEvents, batches),
29
+ };
30
+ }
31
+ export async function findTelemetry(ctx, siteId, event) {
32
+ const bucketStart = eventBucketStart(event.timestamp);
33
+ const [legacyEvents, batches] = await loadTelemetryMinute(ctx, {
34
+ siteId,
35
+ sessionId: event.sessionId,
36
+ bucketStart,
37
+ });
38
+ const persistedCount = telemetryMinuteCount(legacyEvents, batches);
39
+ let legacyEvent = legacyEvents.find((candidate) => candidate.eventId === event.eventId);
40
+ if (!legacyEvent &&
41
+ legacyEvents.length > MAX_EVENTS_PER_SESSION_WINDOW) {
42
+ legacyEvent = await findLegacyTelemetry(ctx, {
43
+ siteId,
44
+ sessionId: event.sessionId,
45
+ bucketStart,
46
+ eventId: event.eventId,
47
+ }) ?? undefined;
48
+ }
49
+ const saturated = persistedCount >= MAX_EVENTS_PER_SESSION_WINDOW;
50
+ if (legacyEvent)
51
+ return { match: legacyEvent, saturated };
52
+ const batch = batches.find((candidate) => candidate.events.some((nested) => nested.eventId === event.eventId));
53
+ return {
54
+ match: batch
55
+ ? { sessionId: batch.sessionId, visitorId: batch.visitorId }
56
+ : null,
57
+ saturated,
58
+ };
59
+ }
60
+ export function bufferEventBatch(siteId, event, buffer) {
61
+ const bucketStart = eventBucketStart(event.timestamp);
62
+ const key = eventBatchKey(event.sessionId, bucketStart);
63
+ const existing = buffer.get(key);
64
+ if (existing) {
65
+ if (existing.visitorId !== event.visitorId) {
66
+ throw new Error("event batch visitor does not match its session");
67
+ }
68
+ existing.events.push(eventForBatch(event));
69
+ return;
70
+ }
71
+ buffer.set(key, {
72
+ siteId,
73
+ sessionId: event.sessionId,
74
+ visitorId: event.visitorId,
75
+ bucketStart,
76
+ events: [eventForBatch(event)],
77
+ });
78
+ }
79
+ export async function insertEventBatch(ctx, siteId, event) {
80
+ await ctx.db.insert("eventBatches", {
81
+ siteId,
82
+ sessionId: event.sessionId,
83
+ visitorId: event.visitorId,
84
+ bucketStart: eventBucketStart(event.timestamp),
85
+ events: [eventForBatch(event)],
86
+ });
87
+ }
88
+ export async function flushEventBatches(ctx, buffer) {
89
+ for (const batch of buffer.values()) {
90
+ await ctx.db.insert("eventBatches", batch);
91
+ }
92
+ }
93
+ export async function loadSessionJourneyEvents(ctx, input) {
94
+ const startTimestamp = input.cursor?.timestamp ?? input.from;
95
+ const firstBucket = eventBucketStart(startTimestamp);
96
+ const lastBucket = eventBucketStart(input.to);
97
+ // Nested event tiebreakers are not index fields, so the boundary minute must
98
+ // be read completely before applying the cursor.
99
+ const loadBatches = async () => {
100
+ const batches = [];
101
+ let eventCount = 0;
102
+ const requiredEvents = input.resultLimit + MAX_EVENTS_PER_SESSION_WINDOW;
103
+ let completedBucket;
104
+ const query = ctx.db
105
+ .query("eventBatches")
106
+ .withIndex("by_siteId_and_sessionId_and_bucketStart", (range) => range
107
+ .eq("siteId", input.siteId)
108
+ .eq("sessionId", input.sessionId)
109
+ .gte("bucketStart", firstBucket)
110
+ .lte("bucketStart", lastBucket))
111
+ .order("asc");
112
+ for await (const batch of query) {
113
+ if (completedBucket !== undefined && batch.bucketStart !== completedBucket) {
114
+ break;
115
+ }
116
+ batches.push(batch);
117
+ eventCount += batch.events.length;
118
+ if (eventCount >= requiredEvents)
119
+ completedBucket = batch.bucketStart;
120
+ }
121
+ return batches;
122
+ };
123
+ const loadLegacyEvents = async () => {
124
+ const take = input.resultLimit + 1;
125
+ if (!input.cursor) {
126
+ return ctx.db
127
+ .query("events")
128
+ .withIndex("by_siteId_and_sessionId_and_timestamp", (range) => range
129
+ .eq("siteId", input.siteId)
130
+ .eq("sessionId", input.sessionId)
131
+ .gte("timestamp", input.from)
132
+ .lte("timestamp", input.to))
133
+ .order("asc")
134
+ .take(take);
135
+ }
136
+ const rows = await ctx.db
137
+ .query("events")
138
+ .withIndex("by_siteId_and_sessionId_and_timestamp", (range) => range
139
+ .eq("siteId", input.siteId)
140
+ .eq("sessionId", input.sessionId)
141
+ .eq("timestamp", input.cursor.timestamp)
142
+ .gt("_creationTime", input.cursor.creationTime))
143
+ .order("asc")
144
+ .take(take);
145
+ if (rows.length < take) {
146
+ rows.push(...await ctx.db
147
+ .query("events")
148
+ .withIndex("by_siteId_and_sessionId_and_timestamp", (range) => range
149
+ .eq("siteId", input.siteId)
150
+ .eq("sessionId", input.sessionId)
151
+ .gt("timestamp", input.cursor.timestamp)
152
+ .lte("timestamp", input.to))
153
+ .order("asc")
154
+ .take(take - rows.length));
155
+ }
156
+ return rows;
157
+ };
158
+ const [legacyEvents, batches] = await Promise.all([
159
+ loadLegacyEvents(),
160
+ loadBatches(),
161
+ ]);
162
+ const rows = legacyEvents.map((event) => ({
163
+ ...event,
164
+ _id: String(event._id),
165
+ }));
166
+ for (const batch of batches) {
167
+ for (let index = 0; index < batch.events.length; index += 1) {
168
+ const event = batch.events[index];
169
+ rows.push({
170
+ ...event,
171
+ _id: `eventBatch:${batch._id}:${index.toString().padStart(3, "0")}`,
172
+ _creationTime: batch._creationTime,
173
+ siteId: batch.siteId,
174
+ sessionId: batch.sessionId,
175
+ visitorId: batch.visitorId,
176
+ });
177
+ }
178
+ }
179
+ return rows
180
+ .filter((event) => event.timestamp >= input.from &&
181
+ event.timestamp <= input.to &&
182
+ (!input.cursor || compareJourneyEventToCursor(event, input.cursor) > 0))
183
+ .sort(compareJourneyEvents)
184
+ .slice(0, input.resultLimit);
185
+ }
186
+ export function compareJourneyEvents(left, right) {
187
+ return left.timestamp - right.timestamp ||
188
+ left._creationTime - right._creationTime ||
189
+ left._id.localeCompare(right._id);
190
+ }
191
+ export async function hydrateEventContexts(ctx, rows) {
192
+ const sessionPromises = new Map();
193
+ for (const row of rows) {
194
+ const key = `${row.siteId}:${row.sessionId}`;
195
+ if (!sessionPromises.has(key)) {
196
+ sessionPromises.set(key, ctx.db
197
+ .query("sessions")
198
+ .withIndex("by_siteId_and_sessionId", (range) => range.eq("siteId", row.siteId).eq("sessionId", row.sessionId))
199
+ .unique());
200
+ }
201
+ }
202
+ const sessions = new Map(await Promise.all([...sessionPromises].map(async ([key, session]) => [key, await session])));
203
+ return rows.map((row) => {
204
+ const session = sessions.get(`${row.siteId}:${row.sessionId}`);
205
+ const country = row.country ?? session?.country;
206
+ const city = row.city ?? session?.city;
207
+ const latitude = row.latitude ?? session?.latitude;
208
+ const longitude = row.longitude ?? session?.longitude;
209
+ return {
210
+ ...row,
211
+ ...(country ? { country } : {}),
212
+ ...(city ? { city } : {}),
213
+ ...(latitude !== undefined ? { latitude } : {}),
214
+ ...(longitude !== undefined ? { longitude } : {}),
215
+ browser: row.browser ?? session?.browser ?? "Other",
216
+ os: row.os ?? session?.os ?? "Other",
217
+ device: row.device ?? session?.device ?? "unknown",
218
+ };
219
+ });
220
+ }
221
+ export async function deleteTelemetryBefore(ctx, input) {
222
+ const batchCutoff = eventBucketStart(input.before);
223
+ const limit = input.batchSize + 1;
224
+ const [legacyEvents, batches] = await Promise.all([
225
+ ctx.db
226
+ .query("events")
227
+ .withIndex("by_siteId_and_timestamp", (range) => range.eq("siteId", input.siteId).lt("timestamp", input.before))
228
+ .take(limit),
229
+ ctx.db
230
+ .query("eventBatches")
231
+ .withIndex("by_siteId_and_bucketStart", (range) => range.eq("siteId", input.siteId).lt("bucketStart", batchCutoff))
232
+ .take(limit),
233
+ ]);
234
+ const rows = [...legacyEvents, ...batches].slice(0, input.batchSize);
235
+ for (const row of rows) {
236
+ if ("bucketStart" in row) {
237
+ await ctx.db.delete("eventBatches", row._id);
238
+ }
239
+ else {
240
+ await ctx.db.delete("events", row._id);
241
+ }
242
+ }
243
+ return {
244
+ deleted: rows.length,
245
+ hasMore: legacyEvents.length + batches.length > input.batchSize,
246
+ };
247
+ }
248
+ async function loadTelemetryMinute(ctx, input) {
249
+ return Promise.all([
250
+ ctx.db
251
+ .query("events")
252
+ .withIndex("by_siteId_and_sessionId_and_timestamp", (range) => range
253
+ .eq("siteId", input.siteId)
254
+ .eq("sessionId", input.sessionId)
255
+ .gte("timestamp", input.bucketStart)
256
+ .lt("timestamp", input.bucketStart + RATE_LIMIT_WINDOW_MS))
257
+ .take(MAX_EVENTS_PER_SESSION_WINDOW + 1),
258
+ ctx.db
259
+ .query("eventBatches")
260
+ .withIndex("by_siteId_and_sessionId_and_bucketStart", (range) => range
261
+ .eq("siteId", input.siteId)
262
+ .eq("sessionId", input.sessionId)
263
+ .eq("bucketStart", input.bucketStart))
264
+ .take(MAX_EVENTS_PER_SESSION_WINDOW + 1),
265
+ ]);
266
+ }
267
+ async function findLegacyTelemetry(ctx, input) {
268
+ const candidates = await ctx.db
269
+ .query("events")
270
+ .withIndex("by_siteId_and_eventId", (range) => range.eq("siteId", input.siteId).eq("eventId", input.eventId))
271
+ .take(2);
272
+ return candidates.find((candidate) => candidate.sessionId === input.sessionId &&
273
+ eventBucketStart(candidate.timestamp) === input.bucketStart) ?? null;
274
+ }
275
+ function telemetryMinuteCount(legacyEvents, batches) {
276
+ return legacyEvents.length + batches.reduce((total, batch) => total + batch.events.length, 0);
277
+ }
278
+ function eventForBatch(event) {
279
+ return {
280
+ eventId: event.eventId,
281
+ sessionId: event.sessionId,
282
+ visitorId: event.visitorId,
283
+ type: event.type,
284
+ ...(event.name ? { name: event.name } : {}),
285
+ path: event.path,
286
+ ...(event.referrer ? { referrer: event.referrer } : {}),
287
+ timestamp: event.timestamp,
288
+ sequence: event.sequence,
289
+ ...(event.target ? { target: event.target } : {}),
290
+ ...(event.href ? { href: event.href } : {}),
291
+ ...(event.properties ? { properties: event.properties } : {}),
292
+ ...(event.revenueCents !== undefined
293
+ ? { revenueCents: event.revenueCents }
294
+ : {}),
295
+ ...(event.currency ? { currency: event.currency } : {}),
296
+ ...(event.affiliateSlug ? { affiliateSlug: event.affiliateSlug } : {}),
297
+ };
298
+ }
299
+ function compareJourneyEventToCursor(event, cursor) {
300
+ return event.timestamp - cursor.timestamp ||
301
+ event._creationTime - cursor.creationTime ||
302
+ event._id.localeCompare(cursor.id);
303
+ }
304
+ //# sourceMappingURL=eventStore.js.map