@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.
- package/README.md +85 -18
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/constants.d.ts +9 -6
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +9 -6
- package/dist/component/constants.js.map +1 -1
- package/dist/component/eventStore.d.ts +94 -0
- package/dist/component/eventStore.d.ts.map +1 -0
- package/dist/component/eventStore.js +304 -0
- package/dist/component/eventStore.js.map +1 -0
- package/dist/component/goals.d.ts.map +1 -1
- package/dist/component/goals.js +17 -4
- package/dist/component/goals.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +39 -7
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +520 -207
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts +8 -0
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +72 -8
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +16 -16
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +250 -66
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +15 -10
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +11 -3
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +101 -10
- package/dist/component/schema.js +30 -8
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +27 -18
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +49 -0
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +11 -0
- 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 +33 -22
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/benchmarks/2026-08-20-realistic.md +171 -0
- package/package.json +10 -4
- package/scripts/benchmark-ingest.mjs +499 -0
- package/src/component/_generated/api.ts +2 -0
- package/src/component/constants.ts +9 -6
- package/src/component/eventStore.ts +465 -0
- package/src/component/goals.ts +25 -4
- package/src/component/http.ts +49 -6
- package/src/component/ingest.ts +671 -215
- package/src/component/live.ts +83 -8
- package/src/component/reports.ts +320 -82
- package/src/component/retention.ts +15 -12
- package/src/component/sanitize.ts +10 -3
- package/src/component/schema.ts +33 -8
- package/src/component/sites.ts +31 -18
- 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
|
|
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,
|
|
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
|
|
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:
|
|
281
|
-
-
|
|
282
|
-
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
340
|
-
writes, goals, funnels, trusted payments, currency safety,
|
|
341
|
-
cross-site journeys,
|
|
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;
|
|
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 =
|
|
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 =
|
|
7
|
-
export declare const MAX_SITE_EVENTS_PER_SHARD_WINDOW =
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
7
|
-
export const MAX_SITE_EVENTS_PER_SHARD_WINDOW =
|
|
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
|
|
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 =
|
|
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,
|
|
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":"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"}
|
|
@@ -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
|