pyxis-analytics 0.0.0 → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Samuel Santana
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,7 +1,227 @@
1
- # pyxis-analytics
1
+ <div align="center">
2
2
 
3
- Privacy-first product analytics for the browser: no cookies, no personal data, no third party.
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/samuelcsantana/pyxis-sdk/main/.github/assets/cover-dark.png">
5
+ <img alt="Pyxis — privacy-first product analytics" src="https://raw.githubusercontent.com/samuelcsantana/pyxis-sdk/main/.github/assets/cover-light.png" width="100%">
6
+ </picture>
4
7
 
5
- This `0.0.0` release only reserves the package name. The tracker is under active development at
6
- [github.com/samuelcsantana/pyxis-sdk](https://github.com/samuelcsantana/pyxis-sdk); the first
7
- usable version will be `0.1.0`, published from GitHub Actions with npm provenance.
8
+ **The browser tracker of Pyxis: page views, named events and request results, batched and sent
9
+ without cookies, without personal data and without a single runtime dependency.**
10
+
11
+ [![CI](https://github.com/samuelcsantana/pyxis-sdk/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/samuelcsantana/pyxis-sdk/actions/workflows/ci.yml)
12
+ [![Coverage](https://codecov.io/gh/samuelcsantana/pyxis-sdk/graph/badge.svg)](https://codecov.io/gh/samuelcsantana/pyxis-sdk)
13
+ [![CodeQL](https://github.com/samuelcsantana/pyxis-sdk/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/samuelcsantana/pyxis-sdk/actions/workflows/codeql.yml)
14
+ <br>
15
+ [![License: MIT](https://img.shields.io/github/license/samuelcsantana/pyxis-sdk)](LICENSE)
16
+ [![TypeScript strict](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](tsconfig.json)
17
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-FE5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org)
18
+ [![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](package.json)
19
+
20
+ **[Playground](https://samuelcsantana.github.io/pyxis-sdk/)** — the real tracker in dry-run mode,
21
+ showing every batch it would send, with no request leaving the page.
22
+
23
+ **[Dashboard Storybook](https://samuelcsantana.github.io/pyxis-web/)** ·
24
+ **[API reference](https://samuelcsantana.github.io/pyxis-api/)**
25
+
26
+ </div>
27
+
28
+ > **Status:** early development. The package name `pyxis-analytics` is reserved on npm with a
29
+ > `0.0.0` placeholder; the first usable version is `0.1.0`, published from GitHub Actions with npm
30
+ > provenance. On `main` every function below works, validated against the API's contract; until
31
+ > 0.1.0 is released, the published `0.0.0` is only the placeholder (see [Roadmap](#roadmap)).
32
+
33
+ ## Ecosystem
34
+
35
+ | Repository | Role |
36
+ | -------------------------------------------------------- | ------------------------------------------------------------ |
37
+ | [pyxis-api](https://github.com/samuelcsantana/pyxis-api) | Ingestion, dashboard sign-in and queries, erasure, retention |
38
+ | **pyxis-sdk** (this one) | Browser tracker published to npm as `pyxis-analytics` |
39
+ | [pyxis-web](https://github.com/samuelcsantana/pyxis-web) | The dashboard: overview, funnels, features, requests |
40
+
41
+ ## Why it exists
42
+
43
+ A tracker runs on every page of the site it measures, so it decides what leaves each visitor's
44
+ browser. This one is built so that nothing personal can: no cookie, no identifier that survives the
45
+ tab, URLs reduced to path templates, query strings dropped except campaign tags, and "do not
46
+ track" signals honored before anything is queued. It is small enough to forget about: the budget
47
+ is 5 KB gzipped, enforced in CI.
48
+
49
+ ## API
50
+
51
+ ```ts
52
+ import { init, track, identify, reset } from 'pyxis-analytics';
53
+
54
+ init({
55
+ key: 'pyxis_pk_…',
56
+ endpoint: 'https://api.pyxis.example.com',
57
+ pathRules: ['/orders/:id'],
58
+ });
59
+
60
+ track('calculator_result_shown', { calculator: 'ifood', used_plan_preset: true });
61
+ identify('user_8f2c');
62
+ reset();
63
+ ```
64
+
65
+ | Function | Behavior |
66
+ | ------------------------- | -------------------------------------------------------------------------------- |
67
+ | `init(options)` | Starts the tracker and records page views on navigation; no key means no-op |
68
+ | `track(name, properties)` | A named event with up to 10 short properties |
69
+ | `identify(userId)` | Attaches the site's internal user id to later events and links the current visit |
70
+ | `reset()` | Sends what is queued, then starts a new anonymous visit (for sign-out) |
71
+ | `optOut()` / `optIn()` | Stops or resumes tracking in this browser |
72
+ | `trackRequest(request)` | The method, route template, status and duration of an HTTP call |
73
+
74
+ | Option | Default | Meaning |
75
+ | --------------- | -------- | ------------------------------------------------------------------------------------------------ |
76
+ | `key` | required | The project's public key; empty or missing disables the SDK without an error |
77
+ | `endpoint` | required | The API's base URL, `http` or `https` |
78
+ | `pathRules` | `[]` | Templates tried before the default rules: `'/blog/:slug'` turns `/blog/hello` into `/blog/:slug` |
79
+ | `autoPageViews` | `true` | Record the landing page and every History API route change as a `page_view` |
80
+ | `debug` | unset | `{ dryRun, onBatch }`: inspect batches, optionally without sending them |
81
+
82
+ A route change that keeps the same templated path records nothing, so routers that call
83
+ `replaceState` on their own do not inflate page views. Only the first page view of a visit carries
84
+ its attribution (referrer host, campaign tags, ad click flag).
85
+
86
+ Call `identify(userId)` on every load of the signed-in part of the site, not only right after
87
+ sign-in: the id lives in `sessionStorage`, so a new tab starts without it, and calling it again
88
+ for the same user sends nothing. Pass the site's internal id (`[A-Za-z0-9_-]{1,64}`), never an
89
+ email. `track` ignores reserved or malformed names and drops invalid properties one by one; set
90
+ `debug: {}` to see why in the console.
91
+
92
+ `trackRequest` records calls the site makes to its own API. Wrap the HTTP client once; leave
93
+ aborted requests out, since a user who navigated away is not an error:
94
+
95
+ ```ts
96
+ import { trackRequest, type RequestMethod } from 'pyxis-analytics';
97
+
98
+ export async function apiFetch(url: string, method: RequestMethod, init?: RequestInit) {
99
+ const startedAt = performance.now();
100
+ const durationMs = () => Math.round(performance.now() - startedAt);
101
+ try {
102
+ const response = await fetch(url, { ...init, method });
103
+ trackRequest({ method, url, status: response.status, durationMs: durationMs() });
104
+ return response;
105
+ } catch (error) {
106
+ if (!(error instanceof DOMException && error.name === 'AbortError')) {
107
+ trackRequest({ method, url, status: 0, durationMs: durationMs() });
108
+ }
109
+ throw error;
110
+ }
111
+ }
112
+ ```
113
+
114
+ The URL is reduced to a templated route (`/api/orders/42?coupon=X` becomes `/api/orders/:id`);
115
+ status `0` means no response arrived. An optional `errorCode` carries your API's machine error key
116
+ (`[a-z0-9_.]{1,64}`), never a message.
117
+
118
+ ## Privacy by design
119
+
120
+ - No cookies. The visit id lives in `sessionStorage` and ends with the tab.
121
+ - Paths are reduced to templates (`/orders/9f1c…` becomes `/orders/:id`); emails and long digit
122
+ runs in a path never leave the browser.
123
+ - Query strings are dropped, except `utm_source`, `utm_medium` and `utm_campaign`; ad click ids
124
+ become a single `from_ad_click: true`.
125
+ - Global Privacy Control and Do Not Track switch the tracker off.
126
+ - `optOut()` writes a single `pyxis:opt-out` marker to `localStorage`, the only thing the SDK
127
+ keeps beyond the tab, so the choice holds on later visits.
128
+ - Batches are sent with `credentials: "omit"`: no cookie of any domain travels with them.
129
+ - No key configured means nothing runs, which keeps tests and local development clean.
130
+
131
+ ## Architecture
132
+
133
+ ```mermaid
134
+ flowchart LR
135
+ subgraph Shell["Thin shell"]
136
+ Navigation["History API"] --> Tracker
137
+ Tracker --> Transport["fetch keepalive<br>sendBeacon"]
138
+ Tracker --> Storage["sessionStorage<br>localStorage (opt-out)"]
139
+ end
140
+ subgraph Core["Functional core (pure)"]
141
+ Paths["path templates"]
142
+ Query["query sanitizer"]
143
+ Batching["batch policy"]
144
+ Retry["retry policy"]
145
+ end
146
+ Tracker --> Core
147
+ Transport -- "POST /v1/batch" --> API["pyxis-api"]
148
+ ```
149
+
150
+ Every rule is a pure function without access to `window`; browser APIs sit behind small adapters
151
+ that tests replace with fakes ([ADR 0002](docs/adr/0002-functional-core.md)).
152
+
153
+ ## Tech stack
154
+
155
+ TypeScript 6 (strict) · esbuild (one minified ES module) · Vitest 5 with jsdom · GitHub Actions
156
+ with CodeQL, Dependabot, Codecov and release-please · npm trusted publishing with provenance.
157
+
158
+ ## Getting started
159
+
160
+ ```bash
161
+ git clone git@github.com:samuelcsantana/pyxis-sdk.git
162
+ cd pyxis-sdk
163
+ npm ci
164
+ npm run build && npm run size
165
+ ```
166
+
167
+ ## Testing
168
+
169
+ ```bash
170
+ npm run test:cov # unit tests in jsdom, 100% coverage required
171
+ npm run test:tooling # the lint rule and the scripts
172
+ npx vitest run src/contract.test.ts # the contract test alone
173
+ npm run test:e2e # the playground in Chromium: batches valid, nothing sent
174
+ ```
175
+
176
+ Coverage must stay at **100% of statements, branches, functions and lines** of `src/`; CI fails
177
+ below it. Nothing in `src/` is excluded. The tooling under `eslint-rules/` and `scripts/` sits
178
+ outside `src/` and is tested on Node's test runner instead.
179
+
180
+ ## Project structure
181
+
182
+ ```text
183
+ src/
184
+ ├── core/ pure rules: batching, retries, session, privacy, paths, attribution, input checks
185
+ ├── adapters/ browser edges: transport, storage, navigation, page lifecycle, ids
186
+ ├── tracker.ts the queue, the session and the timers
187
+ ├── page-views.ts automatic page views
188
+ └── index.ts the public API
189
+ contract/ literal copy of pyxis-api's OpenAPI document (npm run contract:sync)
190
+ eslint-rules/ the local no-comments ESLint rule
191
+ scripts/ comment check, size budget, contract sync, playground build and server
192
+ playground/ the dry-run playground published on GitHub Pages
193
+ e2e/ Playwright test of the playground
194
+ docs/adr/ architecture decision records
195
+ ```
196
+
197
+ ## Contract
198
+
199
+ The batch format belongs to [pyxis-api](https://github.com/samuelcsantana/pyxis-api), which
200
+ publishes it as OpenAPI 3.1. `contract/openapi.json` is a literal copy. The contract test builds
201
+ batches through the real tracker (an attributed page view, an event with properties, an identify)
202
+ and validates them against `components.schemas.BatchRequest` with Ajv; a daily workflow runs it
203
+ against the upstream document, so a breaking change in the API shows up here the next morning.
204
+
205
+ ## Architecture decisions
206
+
207
+ | ADR | Decision |
208
+ | ------------------------------------------------------ | ------------------------------------------------ |
209
+ | [0001](docs/adr/0001-record-architecture-decisions.md) | Record architecture decisions |
210
+ | [0002](docs/adr/0002-functional-core.md) | Functional core, thin shell |
211
+ | [0003](docs/adr/0003-zero-dependencies.md) | Zero runtime dependencies and a 3 KB size budget |
212
+ | [0004](docs/adr/0004-five-kilobyte-budget.md) | Raise the size budget to 5 KB |
213
+
214
+ ## Roadmap
215
+
216
+ - [x] Package skeleton, quality gates, release and publish pipeline
217
+ - [x] Core: queue, batching, transport, retry, session, privacy signals, opt-out
218
+ - [x] Page views: History API, path templates, query sanitizing, attribution
219
+ - [x] `track`, `identify`, `reset`, contract test against the API
220
+ - [ ] Release 0.1.0 to npm with provenance
221
+ - [x] `trackRequest`
222
+ - [x] Playground on GitHub Pages
223
+
224
+ ## Contributing and license
225
+
226
+ See [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). Released
227
+ under the [MIT License](LICENSE).
@@ -0,0 +1,2 @@
1
+ import type { TrackerDependencies } from '../tracker.js';
2
+ export declare function createBrowserDependencies(): TrackerDependencies;
@@ -0,0 +1,3 @@
1
+ export type UuidSource = Pick<Crypto, 'getRandomValues'> & Partial<Pick<Crypto, 'randomUUID'>>;
2
+ export declare function uuidFromBytes(bytes: Uint8Array): string;
3
+ export declare function createUuid(source: UuidSource): string;
@@ -0,0 +1,6 @@
1
+ export interface NavigationTarget {
2
+ readonly history: History;
3
+ addEventListener(type: 'popstate', listener: () => void): void;
4
+ removeEventListener(type: 'popstate', listener: () => void): void;
5
+ }
6
+ export declare function watchNavigation(onNavigate: () => void, target: NavigationTarget): () => void;
@@ -0,0 +1 @@
1
+ export declare function onPageHide(listener: () => void, doc?: Document, win?: Window): () => void;
@@ -0,0 +1,3 @@
1
+ import { type PrivacySignals } from '../core/privacy.js';
2
+ import type { KeyValueStore } from './storage.js';
3
+ export declare function readPrivacySignals(nav: object, win: object, store: KeyValueStore): PrivacySignals;
@@ -0,0 +1,7 @@
1
+ export interface KeyValueStore {
2
+ read(key: string): string | null;
3
+ write(key: string, value: string): void;
4
+ remove(key: string): void;
5
+ }
6
+ export declare function memoryStore(): KeyValueStore;
7
+ export declare function webStore(getStorage: () => Storage): KeyValueStore;
@@ -0,0 +1,8 @@
1
+ import { type SendOutcome } from '../core/retry-policy.js';
2
+ export interface Transport {
3
+ send(url: string, body: string, pageHidden: boolean): Promise<SendOutcome>;
4
+ }
5
+ export declare const BODY_CONTENT_TYPE = "text/plain;charset=UTF-8";
6
+ export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
7
+ export type BeaconLike = (url: string, data: Blob) => boolean;
8
+ export declare function browserTransport(fetchImpl?: FetchLike, beacon?: BeaconLike): Transport;
@@ -0,0 +1,11 @@
1
+ import type { BatchAttribution } from './batch.js';
2
+ export declare const UTM_PARAMETERS: readonly [readonly ["utm_source", "source"], readonly ["utm_medium", "medium"], readonly ["utm_campaign", "campaign"]];
3
+ export declare const AD_CLICK_PARAMETERS: readonly ["gclid", "gbraid", "wbraid", "fbclid", "msclkid", "ttclid", "li_fat_id", "twclid", "dclid"];
4
+ export declare const MAX_UTM_VALUE_LENGTH = 64;
5
+ export declare const MAX_REFERRER_HOST_LENGTH = 128;
6
+ export interface AttributionSource {
7
+ readonly search: string;
8
+ readonly referrer: string | undefined;
9
+ readonly ownHost: string;
10
+ }
11
+ export declare function buildAttribution(source: AttributionSource): BatchAttribution;
@@ -0,0 +1,17 @@
1
+ import type { BatchEvent } from './batch.js';
2
+ export declare const FLUSH_EVENT_COUNT = 10;
3
+ export declare const FLUSH_DELAY_MS = 5000;
4
+ export declare const MAX_EVENTS_PER_BATCH = 50;
5
+ export declare const MAX_BATCH_BYTES: number;
6
+ export declare const MAX_QUEUE_SIZE = 500;
7
+ export declare function byteLength(text: string): number;
8
+ export declare function shouldFlushNow(queueLength: number): boolean;
9
+ export declare function enqueue(queue: readonly BatchEvent[], event: BatchEvent, capacity?: number): {
10
+ readonly queue: readonly BatchEvent[];
11
+ readonly accepted: boolean;
12
+ };
13
+ export interface ChunkResult {
14
+ readonly chunks: readonly (readonly BatchEvent[])[];
15
+ readonly oversized: readonly BatchEvent[];
16
+ }
17
+ export declare function chunkEvents(events: readonly BatchEvent[], serialize: (events: readonly BatchEvent[]) => string, maxEvents?: number, maxBytes?: number): ChunkResult;
@@ -0,0 +1,27 @@
1
+ export type PropertyValue = string | number | boolean;
2
+ export interface BatchAttribution {
3
+ readonly referrer_host?: string;
4
+ readonly utm?: {
5
+ readonly source?: string;
6
+ readonly medium?: string;
7
+ readonly campaign?: string;
8
+ };
9
+ readonly from_ad_click: boolean;
10
+ }
11
+ export interface BatchEvent {
12
+ readonly id: string;
13
+ readonly name: string;
14
+ readonly occurred_at: string;
15
+ readonly session_id: string;
16
+ readonly user_id?: string;
17
+ readonly path: string;
18
+ readonly attribution?: BatchAttribution;
19
+ readonly properties?: Readonly<Record<string, PropertyValue>>;
20
+ }
21
+ export interface Batch {
22
+ readonly key: string;
23
+ readonly sent_at: string;
24
+ readonly events: readonly BatchEvent[];
25
+ }
26
+ export declare function buildBatch(key: string, sentAt: number, events: readonly BatchEvent[]): Batch;
27
+ export declare function serializeBatch(batch: Batch): string;
@@ -0,0 +1,5 @@
1
+ export interface GlobalScope {
2
+ readonly window?: unknown;
3
+ readonly document?: unknown;
4
+ }
5
+ export declare function isBrowserLike(scope: GlobalScope): boolean;
@@ -0,0 +1,18 @@
1
+ import type { PropertyValue } from './batch.js';
2
+ export declare const IDENTIFY_EVENT = "identify";
3
+ export declare const RESERVED_EVENT_NAMES: readonly string[];
4
+ export declare const MAX_PROPERTIES = 10;
5
+ export declare const MAX_PROPERTY_STRING_LENGTH = 100;
6
+ export type NameCheck = 'ok' | 'invalid' | 'reserved';
7
+ export type DropReason = 'invalid_key' | 'invalid_value' | 'too_many';
8
+ export interface DroppedProperty {
9
+ readonly key: string;
10
+ readonly reason: DropReason;
11
+ }
12
+ export interface SanitizedProperties {
13
+ readonly properties: Readonly<Record<string, PropertyValue>>;
14
+ readonly dropped: readonly DroppedProperty[];
15
+ }
16
+ export declare function checkEventName(name: unknown): NameCheck;
17
+ export declare function isValidUserId(userId: unknown): userId is string;
18
+ export declare function sanitizeProperties(input: unknown): SanitizedProperties;
@@ -0,0 +1,30 @@
1
+ import type { Batch } from './batch.js';
2
+ export interface PyxisDebugOptions {
3
+ readonly dryRun?: boolean;
4
+ readonly onBatch?: (batch: Batch) => void;
5
+ }
6
+ export interface PyxisOptions {
7
+ readonly key: string | undefined | null;
8
+ readonly endpoint: string;
9
+ readonly pathRules?: readonly string[];
10
+ readonly autoPageViews?: boolean;
11
+ readonly debug?: PyxisDebugOptions;
12
+ }
13
+ export interface ResolvedOptions {
14
+ readonly key: string;
15
+ readonly batchUrl: string;
16
+ readonly pathRules: readonly string[];
17
+ readonly autoPageViews: boolean;
18
+ readonly dryRun: boolean;
19
+ readonly onBatch?: (batch: Batch) => void;
20
+ }
21
+ export type OptionsResolution = {
22
+ readonly ok: true;
23
+ readonly options: ResolvedOptions;
24
+ } | {
25
+ readonly ok: false;
26
+ readonly reason: 'no-key' | 'invalid-endpoint';
27
+ };
28
+ export declare const BATCH_PATH = "/v1/batch";
29
+ export declare function batchUrlFor(endpoint: string): string | undefined;
30
+ export declare function resolveOptions(options: PyxisOptions): OptionsResolution;
@@ -0,0 +1,3 @@
1
+ export declare const MAX_PATH_LENGTH = 256;
2
+ export declare const ID_PLACEHOLDER = ":id";
3
+ export declare function templatePath(path: string, rules: readonly string[]): string;
@@ -0,0 +1,9 @@
1
+ export declare const OPT_OUT_KEY = "pyxis:opt-out";
2
+ export declare const OPT_OUT_VALUE = "1";
3
+ export interface PrivacySignals {
4
+ readonly globalPrivacyControl: boolean;
5
+ readonly doNotTrack: boolean;
6
+ readonly optedOut: boolean;
7
+ }
8
+ export declare function isDoNotTrack(values: readonly (string | null | undefined)[]): boolean;
9
+ export declare function isTrackingAllowed(signals: PrivacySignals): boolean;
@@ -0,0 +1,15 @@
1
+ export declare const RETRY_DELAYS_MS: readonly number[];
2
+ export declare const RETRY_JITTER = 0.2;
3
+ export type SendOutcome = {
4
+ readonly kind: 'response';
5
+ readonly status: number;
6
+ readonly retryAfterSeconds?: number;
7
+ } | {
8
+ readonly kind: 'network-error';
9
+ } | {
10
+ readonly kind: 'handed-off';
11
+ };
12
+ export type SendVerdict = 'done' | 'retry' | 'drop';
13
+ export declare function judgeOutcome(outcome: SendOutcome): SendVerdict;
14
+ export declare function retryDelay(attempt: number, random: () => number, retryAfterSeconds?: number): number | undefined;
15
+ export declare function parseRetryAfter(header: string | null): number | undefined;
@@ -0,0 +1,15 @@
1
+ export declare const SESSION_IDLE_TIMEOUT_MS: number;
2
+ export interface SessionState {
3
+ readonly id: string;
4
+ readonly lastActivityAt: number;
5
+ readonly userId?: string;
6
+ readonly entryRecorded?: true;
7
+ }
8
+ export interface SessionTouch {
9
+ readonly session: SessionState;
10
+ readonly isNew: boolean;
11
+ }
12
+ export declare function touchSession(current: SessionState | undefined, now: number, newId: () => string): SessionTouch;
13
+ export declare function parseSession(raw: string | null): SessionState | undefined;
14
+ export declare function recordEntry(session: SessionState): SessionState;
15
+ export declare function serializeSession(session: SessionState): string;
@@ -0,0 +1,24 @@
1
+ import type { PropertyValue } from './batch.js';
2
+ export declare const API_REQUEST_EVENT = "api_request";
3
+ export declare const REQUEST_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
4
+ export declare const MAX_ROUTE_LENGTH = 100;
5
+ export declare const MAX_REQUEST_DURATION_MS = 600000;
6
+ export declare const MAX_HTTP_STATUS = 599;
7
+ export type RequestMethod = (typeof REQUEST_METHODS)[number];
8
+ export interface TrackedRequest {
9
+ readonly method: RequestMethod;
10
+ readonly url: string;
11
+ readonly status: number;
12
+ readonly durationMs: number;
13
+ readonly errorCode?: string;
14
+ }
15
+ export type RequestCheck = {
16
+ readonly ok: true;
17
+ readonly properties: Readonly<Record<string, PropertyValue>>;
18
+ readonly droppedErrorCode: boolean;
19
+ } | {
20
+ readonly ok: false;
21
+ readonly reason: 'method' | 'url' | 'status' | 'duration';
22
+ };
23
+ export declare function routeOf(url: unknown, rules: readonly string[]): string | undefined;
24
+ export declare function checkTrackedRequest(request: TrackedRequest, rules: readonly string[]): RequestCheck;
@@ -0,0 +1,14 @@
1
+ import type { PropertyValue } from './core/batch.js';
2
+ import { type PyxisOptions } from './core/options.js';
3
+ import { type TrackedRequest } from './core/tracked-request.js';
4
+ export type { Batch, BatchAttribution, BatchEvent, PropertyValue } from './core/batch.js';
5
+ export type { PyxisDebugOptions, PyxisOptions } from './core/options.js';
6
+ export type { RequestMethod, TrackedRequest } from './core/tracked-request.js';
7
+ export type Properties = Readonly<Record<string, PropertyValue>>;
8
+ export declare function init(options: PyxisOptions): void;
9
+ export declare function track(name: string, properties?: Properties): void;
10
+ export declare function identify(userId: string): void;
11
+ export declare function reset(): void;
12
+ export declare function trackRequest(request: TrackedRequest): void;
13
+ export declare function optOut(): void;
14
+ export declare function optIn(): void;
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ function he(e){return Array.from(e,t=>t.toString(16).padStart(2,"0")).join("")}function me(e){let t=he(e),r=(parseInt(t.slice(16,18),16)&63|128).toString(16);return[t.slice(0,8),t.slice(8,12),`4${t.slice(13,16)}`,`${r}${t.slice(18,20)}`,t.slice(20,32)].join("-")}function H(e){return typeof e.randomUUID=="function"?e.randomUUID():me(e.getRandomValues(new Uint8Array(16)))}function ge(){let e=new Map;return{read:t=>e.get(t)??null,write:(t,r)=>{e.set(t,r)},remove:t=>{e.delete(t)}}}function b(e){let t=ge();return{read:r=>{try{return e().getItem(r)}catch{return t.read(r)}},write:(r,n)=>{try{e().setItem(r,n)}catch{t.write(r,n)}},remove:r=>{try{e().removeItem(r)}catch{t.remove(r)}}}}var Ee=[1e3,2e3,4e3,8e3,16e3],C=.2,Te=429;function j(e){if(e.kind==="network-error")return"retry";if(e.kind==="handed-off")return"done";let{status:t}=e;return t>=200&&t<300?"done":t===Te||t>=500?"retry":"drop"}function q(e,t,r){let n=Ee[e];if(n===void 0)return;let o=Math.round(n*(1-C+t()*2*C));return r===void 0?o:Math.max(o,r*1e3)}function $(e){if(!(e===null||!/^\d+$/.test(e.trim())))return Number(e.trim())}var Y="text/plain;charset=UTF-8";function z(e,t){let r=(o,i)=>t?.(o,new Blob([i],{type:Y}))===!0?{kind:"handed-off"}:{kind:"network-error"},n=async(o,i)=>{if(e===void 0)throw new TypeError("fetch is not available");let a=await e(o,{method:"POST",body:i,keepalive:!0,credentials:"omit",headers:{"Content-Type":Y}}),f=$(a.headers.get("Retry-After"));return f===void 0?{kind:"response",status:a.status}:{kind:"response",status:a.status,retryAfterSeconds:f}};return{send:async(o,i,a)=>{try{return await n(o,i)}catch{return a||e===void 0?r(o,i):{kind:"network-error"}}}}}function F(){return{now:()=>Date.now(),createId:()=>H(globalThis.crypto),random:Math.random,sessionStore:b(()=>window.sessionStorage),transport:z(typeof fetch=="function"?fetch.bind(globalThis):void 0,typeof navigator.sendBeacon=="function"?navigator.sendBeacon.bind(navigator):void 0)}}function G(e,t){let{history:r}=t,n=r.pushState.bind(r),o=r.replaceState.bind(r);return r.pushState=(...i)=>{n(...i),e()},r.replaceState=(...i)=>{o(...i),e()},t.addEventListener("popstate",e),()=>{r.pushState=n,r.replaceState=o,t.removeEventListener("popstate",e)}}function K(e,t=document,r=window){let n=()=>{t.visibilityState==="hidden"&&e()};return t.addEventListener("visibilitychange",n),r.addEventListener("pagehide",e),()=>{t.removeEventListener("visibilitychange",n),r.removeEventListener("pagehide",e)}}var T="pyxis:opt-out";var Se=new Set(["1","yes"]);function X(e){return e.some(t=>t!=null&&Se.has(t))}function Q(e){return!e.globalPrivacyControl&&!e.doNotTrack&&!e.optedOut}function W(e){return typeof e=="string"?e:null}function J(e,t,r){return{globalPrivacyControl:Reflect.get(e,"globalPrivacyControl")===!0,doNotTrack:X([W(Reflect.get(e,"doNotTrack")),W(Reflect.get(t,"doNotTrack"))]),optedOut:r.read(T)==="1"}}function v(e){return e.window!==void 0&&e.document!==void 0}var x="identify",_e=["page_view",x,"api_request"],be=10,ve=100,ke=/^[a-z][a-z0-9_]{0,63}$/,Re=/^[a-z0-9_]{1,40}$/,xe=/^[A-Za-z0-9_-]{1,64}$/;function Z(e){return typeof e!="string"||!ke.test(e)?"invalid":_e.includes(e)?"reserved":"ok"}function ee(e){return typeof e=="string"&&xe.test(e)}function Ae(e){return typeof e=="boolean"||typeof e=="number"&&Number.isFinite(e)||typeof e=="string"&&e.length<=ve}function te(e){let t={},r=[],n=typeof e=="object"&&e!==null?Object.entries(e):[];for(let[o,i]of n)Re.test(o)?Ae(i)?Object.keys(t).length>=be?r.push({key:o,reason:"too_many"}):t[o]=i:r.push({key:o,reason:"invalid_value"}):r.push({key:o,reason:"invalid_key"});return{properties:t,dropped:r}}var we="/v1/batch";function Pe(e){let t;try{t=new URL(e)}catch{return}if(!(t.protocol!=="https:"&&t.protocol!=="http:"))return`${t.origin}${t.pathname.replace(/\/+$/,"")}${we}`}function re(e){let t=e.key?.trim();if(!t)return{ok:!1,reason:"no-key"};let r=Pe(e.endpoint);if(r===void 0)return{ok:!1,reason:"invalid-endpoint"};let n={key:t,batchUrl:r,pathRules:e.pathRules??[],autoPageViews:e.autoPageViews??!0,dryRun:e.debug?.dryRun??!1},o=e.debug?.onBatch;return{ok:!0,options:o===void 0?n:{...n,onBatch:o}}}var Oe=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,Ne=/^\d+$/,Ie=/\d/g,Ue=/%40/i;function Be(e){return Oe.test(e)||Ne.test(e)||(e.match(Ie)?.length??0)>=10||e.includes("@")||Ue.test(e)}function A(e){let t=e.length;for(;t>0&&e.charAt(t-1)==="/";)t-=1;return t===0?"/":e.slice(0,t)}function Ve(e,t){let r=A(t).split("/");return r.length===e.length&&r.every((n,o)=>n.startsWith(":")||n===e[o])}function E(e,t){let r=A(e).split("/"),n=t.find(i=>Ve(r,i));return(n===void 0?r.map(i=>Be(i)?":id":i).join("/"):A(n)).slice(0,256)}var oe="api_request",De=["GET","POST","PUT","PATCH","DELETE"],Le=100,Me=6e5,He=599,Ce=/^[a-z0-9_.]{1,64}$/,je="http://relative.invalid";function qe(e){return typeof e=="string"&&De.includes(e)}function ne(e,t,r){return typeof e=="number"&&Number.isInteger(e)&&e>=t&&e<=r}function $e(e,t){if(typeof e=="string")try{return E(new URL(e,je).pathname,t).slice(0,Le)}catch{return}}function ie(e,t){if(!qe(e.method))return{ok:!1,reason:"method"};let r=$e(e.url,t);if(r===void 0)return{ok:!1,reason:"url"};if(!ne(e.status,0,He))return{ok:!1,reason:"status"};if(!ne(e.durationMs,0,Me))return{ok:!1,reason:"duration"};let{errorCode:n}=e,o=typeof n=="string"&&Ce.test(n),i={method:e.method,route:r,status:e.status,duration_ms:e.durationMs};return{ok:!0,properties:o?{...i,error_code:n}:i,droppedErrorCode:n!==void 0&&!o}}var Ye=[["utm_source","source"],["utm_medium","medium"],["utm_campaign","campaign"]],ze=["gclid","gbraid","wbraid","fbclid","msclkid","ttclid","li_fat_id","twclid","dclid"];var Fe=/^[a-z0-9-]+(\.[a-z0-9-]+)*$/,se=/^www\./;function Ge(e){let t=Ye.flatMap(([r,n])=>{let o=e.get(r)?.trim().slice(0,64);return o?[[n,o]]:[]});return t.length===0?void 0:Object.fromEntries(t)}function Ke(e){try{return new URL(e).hostname.toLowerCase().replace(se,"")}catch{return}}function Xe(e,t){let r=e?Ke(e):void 0;return r!==void 0&&r.length<=128&&Fe.test(r)&&r!==t.toLowerCase().replace(se,"")?r:void 0}function ae(e){let t=new URLSearchParams(e.search),r=Ge(t),n=Xe(e.referrer,e.ownHost);return{...n===void 0?{}:{referrer_host:n},...r===void 0?{}:{utm:r},from_ad_click:ze.some(o=>t.has(o))}}var Qe="page_view";function de(e,t,r){let n,o=r.referrer,i=()=>{let{pathname:a,search:f,hostname:u}=r.location(),y=E(a,t);if(y===n)return;n=y;let g=ae({search:f,referrer:o,ownHost:u});o=void 0,e.enqueue({name:Qe,path:y,entryAttribution:g})};return r.guard(i),r.watch(()=>{r.guard(i)})}function w(e,t,r){return{key:e,sent_at:new Date(t).toISOString(),events:r}}function P(e){return JSON.stringify(e)}var We=new TextEncoder;function Je(e){return We.encode(e).length}function ce(e){return e>=10}function ue(e,t,r=500){return e.length>=r?{queue:e,accepted:!1}:{queue:[...e,t],accepted:!0}}function le(e,t,r=50,n=32768){let o=[],i=[],a=[],f=u=>u.length<=r&&Je(t(u))<=n;for(let u of e){if(!f([u])){i.push(u);continue}let y=[...a,u];if(f(y)){a=y;continue}o.push(a),a=[u]}return a.length>0&&o.push(a),{chunks:o,oversized:i}}function O(e,t,r){return e!==void 0&&t-e.lastActivityAt<=18e5?{session:{...e,lastActivityAt:t},isNew:!1}:{session:e?.userId===void 0?{id:r(),lastActivityAt:t}:{id:r(),lastActivityAt:t,userId:e.userId},isNew:!0}}function Ze(e){return typeof e=="object"&&e!==null}function N(e){if(e===null)return;let t;try{t=JSON.parse(e)}catch{return}if(!Ze(t)||typeof t.id!="string"||typeof t.lastActivityAt!="number"||!Number.isFinite(t.lastActivityAt))return;let{id:r,lastActivityAt:n,userId:o,entryRecorded:i}=t;return{id:r,lastActivityAt:n,...typeof o=="string"?{userId:o}:{},...i===!0?{entryRecorded:i}:{}}}function pe(e){return{...e,entryRecorded:!0}}function I(e){return JSON.stringify(e)}var S="pyxis:session",tt={kind:"network-error"};function fe(e,t){let r=[],n,o=new Set,i=!1,a=s=>{let c=N(t.sessionStore.read(S)),{session:d}=O(c,t.now(),t.createId),l=s&&d.entryRecorded!==!0,m=l?pe(d):d;return t.sessionStore.write(S,I(m)),{session:m,isEntry:l}},f=s=>{try{e.onBatch?.(s)}catch(c){console.warn("[pyxis] debug.onBatch threw; the batch is sent anyway",c)}},u=(s,c,d)=>{if(i||j(d)!=="retry")return;let l=d.kind==="response"?d.retryAfterSeconds:void 0,m=q(c,t.random,l);if(m===void 0)return;let M=setTimeout(()=>{o.delete(M),y(s,c+1,!1)},m);o.add(M)},y=(s,c,d)=>{let l=w(e.key,t.now(),s);f(l),!e.dryRun&&t.transport.send(e.batchUrl,P(l),d).then(m=>{u(s,c,m)},()=>{u(s,c,tt)})},g=(s=!1)=>{if(n!==void 0&&(clearTimeout(n),n=void 0),i||r.length===0)return;let{chunks:c}=le(r,d=>P(w(e.key,t.now(),d)));r=[];for(let d of c)y(d,0,s)},L=s=>{if(i)return;let{session:c,isEntry:d}=a(s.entryAttribution!==void 0),l={id:t.createId(),name:s.name,occurred_at:new Date(t.now()).toISOString(),session_id:c.id,path:s.path,...c.userId===void 0?{}:{user_id:c.userId},...d?{attribution:s.entryAttribution}:{},...s.properties===void 0?{}:{properties:s.properties}};if(r=ue(r,l).queue,ce(r.length)){g();return}n??(n=setTimeout(()=>{n=void 0,g()},5e3))};return{enqueue:L,identify:(s,c)=>{if(i)return;let{session:d}=O(N(t.sessionStore.read(S)),t.now(),t.createId),l=d.userId===s;t.sessionStore.write(S,I({...d,userId:s})),l||L({name:x,path:c})},reset:()=>{i||(g(),t.sessionStore.remove(S))},flush:g,dispose:()=>{i=!0,r=[],n!==void 0&&clearTimeout(n),o.forEach(s=>{clearTimeout(s)}),o.clear()}}}var ye=!1,B=!1,p,U=[],k=[];function h(e){try{e()}catch(t){B&&console.warn("[pyxis] ignored an internal error",t)}}function _(e){B&&console.warn(`[pyxis] ${e}`)}function V(){return E(window.location.pathname,k)}function D(){return b(()=>window.localStorage)}function rt(){U.forEach(e=>{e()}),p?.dispose(),U=[],p=void 0}function nt(e,t){return de(e,t,{location:()=>window.location,referrer:document.referrer,watch:r=>G(r,window),guard:h})}function Jt(e){h(()=>{if(ye||!v(globalThis))return;ye=!0,B=e.debug!==void 0;let t=re(e);if(!t.ok||!Q(J(navigator,window,D())))return;let r=fe(t.options,F());k=t.options.pathRules,p=r;let n=K(()=>{r.flush(!0)});U=t.options.autoPageViews?[n,nt(r,k)]:[n]})}function Zt(e,t){h(()=>{if(p===void 0)return;let r=Z(e);if(r!=="ok"){_(`track() ignored "${e}": the name ${r==="reserved"?"is reserved for the SDK":"is not a valid event name"}`);return}let n=te(t);for(let{key:i,reason:a}of n.dropped)_(`track("${e}") dropped the property "${i}" (${a})`);let o=Object.keys(n.properties).length>0;p.enqueue({name:e,path:V(),...o?{properties:n.properties}:{}})})}function er(e){h(()=>{if(p!==void 0){if(!ee(e)){_("identify() ignored a user id outside [A-Za-z0-9_-]{1,64}; never pass an email");return}p.identify(e,V())}})}function tr(){h(()=>{p?.reset()})}function rr(e){h(()=>{if(p===void 0)return;let t=ie(e,k);if(!t.ok){_(`trackRequest() ignored a request with an invalid ${t.reason}`);return}t.droppedErrorCode&&_("trackRequest() dropped an errorCode outside [a-z0-9_.]{1,64}"),p.enqueue({name:oe,path:V(),properties:t.properties})})}function nr(){h(()=>{v(globalThis)&&(D().write(T,"1"),rt())})}function or(){h(()=>{v(globalThis)&&D().remove(T)})}export{er as identify,Jt as init,or as optIn,nr as optOut,tr as reset,Zt as track,rr as trackRequest};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/adapters/ids.ts", "../src/adapters/storage.ts", "../src/core/retry-policy.ts", "../src/adapters/transport.ts", "../src/adapters/browser-dependencies.ts", "../src/adapters/navigation.ts", "../src/adapters/page-lifecycle.ts", "../src/core/privacy.ts", "../src/adapters/privacy-signals.ts", "../src/core/environment.ts", "../src/core/event-input.ts", "../src/core/options.ts", "../src/core/path-template.ts", "../src/core/tracked-request.ts", "../src/core/attribution.ts", "../src/page-views.ts", "../src/core/batch.ts", "../src/core/batch-policy.ts", "../src/core/session-policy.ts", "../src/tracker.ts", "../src/index.ts"],
4
+ "sourcesContent": ["const UUID_BYTES = 16;\nconst VARIANT_MASK = 0x3f;\nconst VARIANT_BITS = 0x80;\n\nexport type UuidSource = Pick<Crypto, 'getRandomValues'> & Partial<Pick<Crypto, 'randomUUID'>>;\n\nfunction toHex(bytes: Uint8Array): string {\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nexport function uuidFromBytes(bytes: Uint8Array): string {\n const hex = toHex(bytes);\n const variant = ((parseInt(hex.slice(16, 18), 16) & VARIANT_MASK) | VARIANT_BITS).toString(16);\n return [\n hex.slice(0, 8),\n hex.slice(8, 12),\n `4${hex.slice(13, 16)}`,\n `${variant}${hex.slice(18, 20)}`,\n hex.slice(20, 32),\n ].join('-');\n}\n\nexport function createUuid(source: UuidSource): string {\n if (typeof source.randomUUID === 'function') {\n return source.randomUUID();\n }\n return uuidFromBytes(source.getRandomValues(new Uint8Array(UUID_BYTES)));\n}\n", "export interface KeyValueStore {\n read(key: string): string | null;\n write(key: string, value: string): void;\n remove(key: string): void;\n}\n\nexport function memoryStore(): KeyValueStore {\n const values = new Map<string, string>();\n return {\n read: (key) => values.get(key) ?? null,\n write: (key, value) => {\n values.set(key, value);\n },\n remove: (key) => {\n values.delete(key);\n },\n };\n}\n\nexport function webStore(getStorage: () => Storage): KeyValueStore {\n const fallback = memoryStore();\n return {\n read: (key) => {\n try {\n return getStorage().getItem(key);\n } catch {\n return fallback.read(key);\n }\n },\n write: (key, value) => {\n try {\n getStorage().setItem(key, value);\n } catch {\n fallback.write(key, value);\n }\n },\n remove: (key) => {\n try {\n getStorage().removeItem(key);\n } catch {\n fallback.remove(key);\n }\n },\n };\n}\n", "export const RETRY_DELAYS_MS: readonly number[] = [1000, 2000, 4000, 8000, 16000];\nexport const RETRY_JITTER = 0.2;\n\nexport type SendOutcome =\n | { readonly kind: 'response'; readonly status: number; readonly retryAfterSeconds?: number }\n | { readonly kind: 'network-error' }\n | { readonly kind: 'handed-off' };\n\nexport type SendVerdict = 'done' | 'retry' | 'drop';\n\nconst TOO_MANY_REQUESTS = 429;\n\nexport function judgeOutcome(outcome: SendOutcome): SendVerdict {\n if (outcome.kind === 'network-error') {\n return 'retry';\n }\n if (outcome.kind === 'handed-off') {\n return 'done';\n }\n const { status } = outcome;\n if (status >= 200 && status < 300) {\n return 'done';\n }\n if (status === TOO_MANY_REQUESTS || status >= 500) {\n return 'retry';\n }\n return 'drop';\n}\n\nexport function retryDelay(\n attempt: number,\n random: () => number,\n retryAfterSeconds?: number,\n): number | undefined {\n const base = RETRY_DELAYS_MS[attempt];\n if (base === undefined) {\n return undefined;\n }\n const jittered = Math.round(base * (1 - RETRY_JITTER + random() * 2 * RETRY_JITTER));\n return retryAfterSeconds === undefined ? jittered : Math.max(jittered, retryAfterSeconds * 1000);\n}\n\nexport function parseRetryAfter(header: string | null): number | undefined {\n if (header === null || !/^\\d+$/.test(header.trim())) {\n return undefined;\n }\n return Number(header.trim());\n}\n", "import { parseRetryAfter, type SendOutcome } from '../core/retry-policy.js';\n\nexport interface Transport {\n send(url: string, body: string, pageHidden: boolean): Promise<SendOutcome>;\n}\n\nexport const BODY_CONTENT_TYPE = 'text/plain;charset=UTF-8';\n\nexport type FetchLike = (input: string, init: RequestInit) => Promise<Response>;\nexport type BeaconLike = (url: string, data: Blob) => boolean;\n\nexport function browserTransport(fetchImpl?: FetchLike, beacon?: BeaconLike): Transport {\n const viaBeacon = (url: string, body: string): SendOutcome =>\n beacon?.(url, new Blob([body], { type: BODY_CONTENT_TYPE })) === true\n ? { kind: 'handed-off' }\n : { kind: 'network-error' };\n\n const viaFetch = async (url: string, body: string): Promise<SendOutcome> => {\n if (fetchImpl === undefined) {\n throw new TypeError('fetch is not available');\n }\n const response = await fetchImpl(url, {\n method: 'POST',\n body,\n keepalive: true,\n credentials: 'omit',\n headers: { 'Content-Type': BODY_CONTENT_TYPE },\n });\n const retryAfterSeconds = parseRetryAfter(response.headers.get('Retry-After'));\n return retryAfterSeconds === undefined\n ? { kind: 'response', status: response.status }\n : { kind: 'response', status: response.status, retryAfterSeconds };\n };\n\n return {\n send: async (url, body, pageHidden) => {\n try {\n return await viaFetch(url, body);\n } catch {\n return pageHidden || fetchImpl === undefined\n ? viaBeacon(url, body)\n : { kind: 'network-error' };\n }\n },\n };\n}\n", "import type { TrackerDependencies } from '../tracker.js';\nimport { createUuid } from './ids.js';\nimport { webStore } from './storage.js';\nimport { browserTransport } from './transport.js';\n\nexport function createBrowserDependencies(): TrackerDependencies {\n return {\n now: () => Date.now(),\n createId: () => createUuid(globalThis.crypto),\n random: Math.random,\n sessionStore: webStore(() => window.sessionStorage),\n transport: browserTransport(\n typeof fetch === 'function' ? fetch.bind(globalThis) : undefined,\n typeof navigator.sendBeacon === 'function' ? navigator.sendBeacon.bind(navigator) : undefined,\n ),\n };\n}\n", "export interface NavigationTarget {\n readonly history: History;\n addEventListener(type: 'popstate', listener: () => void): void;\n removeEventListener(type: 'popstate', listener: () => void): void;\n}\n\nexport function watchNavigation(onNavigate: () => void, target: NavigationTarget): () => void {\n const { history } = target;\n const pushState = history.pushState.bind(history);\n const replaceState = history.replaceState.bind(history);\n history.pushState = (...args: Parameters<History['pushState']>) => {\n pushState(...args);\n onNavigate();\n };\n history.replaceState = (...args: Parameters<History['replaceState']>) => {\n replaceState(...args);\n onNavigate();\n };\n target.addEventListener('popstate', onNavigate);\n return () => {\n history.pushState = pushState;\n history.replaceState = replaceState;\n target.removeEventListener('popstate', onNavigate);\n };\n}\n", "export function onPageHide(\n listener: () => void,\n doc: Document = document,\n win: Window = window,\n): () => void {\n const onVisibilityChange = () => {\n if (doc.visibilityState === 'hidden') {\n listener();\n }\n };\n doc.addEventListener('visibilitychange', onVisibilityChange);\n win.addEventListener('pagehide', listener);\n return () => {\n doc.removeEventListener('visibilitychange', onVisibilityChange);\n win.removeEventListener('pagehide', listener);\n };\n}\n", "export const OPT_OUT_KEY = 'pyxis:opt-out';\nexport const OPT_OUT_VALUE = '1';\n\nconst DO_NOT_TRACK_VALUES = new Set(['1', 'yes']);\n\nexport interface PrivacySignals {\n readonly globalPrivacyControl: boolean;\n readonly doNotTrack: boolean;\n readonly optedOut: boolean;\n}\n\nexport function isDoNotTrack(values: readonly (string | null | undefined)[]): boolean {\n return values.some(\n (value) => value !== null && value !== undefined && DO_NOT_TRACK_VALUES.has(value),\n );\n}\n\nexport function isTrackingAllowed(signals: PrivacySignals): boolean {\n return !signals.globalPrivacyControl && !signals.doNotTrack && !signals.optedOut;\n}\n", "import { isDoNotTrack, OPT_OUT_KEY, OPT_OUT_VALUE, type PrivacySignals } from '../core/privacy.js';\nimport type { KeyValueStore } from './storage.js';\n\nfunction textOrNull(value: unknown): string | null {\n return typeof value === 'string' ? value : null;\n}\n\nexport function readPrivacySignals(nav: object, win: object, store: KeyValueStore): PrivacySignals {\n return {\n globalPrivacyControl: Reflect.get(nav, 'globalPrivacyControl') === true,\n doNotTrack: isDoNotTrack([\n textOrNull(Reflect.get(nav, 'doNotTrack')),\n textOrNull(Reflect.get(win, 'doNotTrack')),\n ]),\n optedOut: store.read(OPT_OUT_KEY) === OPT_OUT_VALUE,\n };\n}\n", "export interface GlobalScope {\n readonly window?: unknown;\n readonly document?: unknown;\n}\n\nexport function isBrowserLike(scope: GlobalScope): boolean {\n return scope.window !== undefined && scope.document !== undefined;\n}\n", "import type { PropertyValue } from './batch.js';\n\nexport const IDENTIFY_EVENT = 'identify';\nexport const RESERVED_EVENT_NAMES: readonly string[] = ['page_view', IDENTIFY_EVENT, 'api_request'];\nexport const MAX_PROPERTIES = 10;\nexport const MAX_PROPERTY_STRING_LENGTH = 100;\n\nconst EVENT_NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;\nconst PROPERTY_KEY_PATTERN = /^[a-z0-9_]{1,40}$/;\nconst USER_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\n\nexport type NameCheck = 'ok' | 'invalid' | 'reserved';\n\nexport type DropReason = 'invalid_key' | 'invalid_value' | 'too_many';\n\nexport interface DroppedProperty {\n readonly key: string;\n readonly reason: DropReason;\n}\n\nexport interface SanitizedProperties {\n readonly properties: Readonly<Record<string, PropertyValue>>;\n readonly dropped: readonly DroppedProperty[];\n}\n\nexport function checkEventName(name: unknown): NameCheck {\n if (typeof name !== 'string' || !EVENT_NAME_PATTERN.test(name)) {\n return 'invalid';\n }\n return RESERVED_EVENT_NAMES.includes(name) ? 'reserved' : 'ok';\n}\n\nexport function isValidUserId(userId: unknown): userId is string {\n return typeof userId === 'string' && USER_ID_PATTERN.test(userId);\n}\n\nfunction isPropertyValue(value: unknown): value is PropertyValue {\n return (\n typeof value === 'boolean' ||\n (typeof value === 'number' && Number.isFinite(value)) ||\n (typeof value === 'string' && value.length <= MAX_PROPERTY_STRING_LENGTH)\n );\n}\n\nexport function sanitizeProperties(input: unknown): SanitizedProperties {\n const kept: Record<string, PropertyValue> = {};\n const dropped: DroppedProperty[] = [];\n const entries = typeof input === 'object' && input !== null ? Object.entries(input) : [];\n for (const [key, value] of entries) {\n if (!PROPERTY_KEY_PATTERN.test(key)) {\n dropped.push({ key, reason: 'invalid_key' });\n } else if (!isPropertyValue(value)) {\n dropped.push({ key, reason: 'invalid_value' });\n } else if (Object.keys(kept).length >= MAX_PROPERTIES) {\n dropped.push({ key, reason: 'too_many' });\n } else {\n kept[key] = value;\n }\n }\n return { properties: kept, dropped };\n}\n", "import type { Batch } from './batch.js';\n\nexport interface PyxisDebugOptions {\n readonly dryRun?: boolean;\n readonly onBatch?: (batch: Batch) => void;\n}\n\nexport interface PyxisOptions {\n readonly key: string | undefined | null;\n readonly endpoint: string;\n readonly pathRules?: readonly string[];\n readonly autoPageViews?: boolean;\n readonly debug?: PyxisDebugOptions;\n}\n\nexport interface ResolvedOptions {\n readonly key: string;\n readonly batchUrl: string;\n readonly pathRules: readonly string[];\n readonly autoPageViews: boolean;\n readonly dryRun: boolean;\n readonly onBatch?: (batch: Batch) => void;\n}\n\nexport type OptionsResolution =\n | { readonly ok: true; readonly options: ResolvedOptions }\n | { readonly ok: false; readonly reason: 'no-key' | 'invalid-endpoint' };\n\nexport const BATCH_PATH = '/v1/batch';\n\nexport function batchUrlFor(endpoint: string): string | undefined {\n let url: URL;\n try {\n url = new URL(endpoint);\n } catch {\n return undefined;\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n return undefined;\n }\n return `${url.origin}${url.pathname.replace(/\\/+$/, '')}${BATCH_PATH}`;\n}\n\nexport function resolveOptions(options: PyxisOptions): OptionsResolution {\n const key = options.key?.trim();\n if (!key) {\n return { ok: false, reason: 'no-key' };\n }\n const batchUrl = batchUrlFor(options.endpoint);\n if (batchUrl === undefined) {\n return { ok: false, reason: 'invalid-endpoint' };\n }\n const base = {\n key,\n batchUrl,\n pathRules: options.pathRules ?? [],\n autoPageViews: options.autoPageViews ?? true,\n dryRun: options.debug?.dryRun ?? false,\n };\n const onBatch = options.debug?.onBatch;\n return { ok: true, options: onBatch === undefined ? base : { ...base, onBatch } };\n}\n", "export const MAX_PATH_LENGTH = 256;\nexport const ID_PLACEHOLDER = ':id';\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst ALL_DIGITS = /^\\d+$/;\nconst DIGIT = /\\d/g;\nconst ENCODED_AT_SIGN = /%40/i;\nconst MIN_ID_DIGITS = 10;\n\nfunction isIdentifier(segment: string): boolean {\n return (\n UUID_PATTERN.test(segment) ||\n ALL_DIGITS.test(segment) ||\n (segment.match(DIGIT)?.length ?? 0) >= MIN_ID_DIGITS ||\n segment.includes('@') ||\n ENCODED_AT_SIGN.test(segment)\n );\n}\n\nfunction withoutTrailingSlashes(path: string): string {\n let end = path.length;\n while (end > 0 && path.charAt(end - 1) === '/') {\n end -= 1;\n }\n return end === 0 ? '/' : path.slice(0, end);\n}\n\nfunction matchesRule(segments: readonly string[], rule: string): boolean {\n const ruleSegments = withoutTrailingSlashes(rule).split('/');\n return (\n ruleSegments.length === segments.length &&\n ruleSegments.every(\n (ruleSegment, index) => ruleSegment.startsWith(':') || ruleSegment === segments[index],\n )\n );\n}\n\nexport function templatePath(path: string, rules: readonly string[]): string {\n const segments = withoutTrailingSlashes(path).split('/');\n const rule = rules.find((candidate) => matchesRule(segments, candidate));\n const templated =\n rule === undefined\n ? segments.map((segment) => (isIdentifier(segment) ? ID_PLACEHOLDER : segment)).join('/')\n : withoutTrailingSlashes(rule);\n return templated.slice(0, MAX_PATH_LENGTH);\n}\n", "import type { PropertyValue } from './batch.js';\nimport { templatePath } from './path-template.js';\n\nexport const API_REQUEST_EVENT = 'api_request';\nexport const REQUEST_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;\nexport const MAX_ROUTE_LENGTH = 100;\nexport const MAX_REQUEST_DURATION_MS = 600_000;\nexport const MAX_HTTP_STATUS = 599;\n\nconst ERROR_CODE_PATTERN = /^[a-z0-9_.]{1,64}$/;\nconst URL_BASE = 'http://relative.invalid';\n\nexport type RequestMethod = (typeof REQUEST_METHODS)[number];\n\nexport interface TrackedRequest {\n readonly method: RequestMethod;\n readonly url: string;\n readonly status: number;\n readonly durationMs: number;\n readonly errorCode?: string;\n}\n\nexport type RequestCheck =\n | {\n readonly ok: true;\n readonly properties: Readonly<Record<string, PropertyValue>>;\n readonly droppedErrorCode: boolean;\n }\n | { readonly ok: false; readonly reason: 'method' | 'url' | 'status' | 'duration' };\n\nfunction isMethod(value: unknown): value is RequestMethod {\n return typeof value === 'string' && (REQUEST_METHODS as readonly string[]).includes(value);\n}\n\nfunction isIntegerBetween(value: unknown, min: number, max: number): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max;\n}\n\nexport function routeOf(url: unknown, rules: readonly string[]): string | undefined {\n if (typeof url !== 'string') {\n return undefined;\n }\n try {\n return templatePath(new URL(url, URL_BASE).pathname, rules).slice(0, MAX_ROUTE_LENGTH);\n } catch {\n return undefined;\n }\n}\n\nexport function checkTrackedRequest(\n request: TrackedRequest,\n rules: readonly string[],\n): RequestCheck {\n if (!isMethod(request.method)) {\n return { ok: false, reason: 'method' };\n }\n const route = routeOf(request.url, rules);\n if (route === undefined) {\n return { ok: false, reason: 'url' };\n }\n if (!isIntegerBetween(request.status, 0, MAX_HTTP_STATUS)) {\n return { ok: false, reason: 'status' };\n }\n if (!isIntegerBetween(request.durationMs, 0, MAX_REQUEST_DURATION_MS)) {\n return { ok: false, reason: 'duration' };\n }\n const { errorCode } = request;\n const keepsErrorCode = typeof errorCode === 'string' && ERROR_CODE_PATTERN.test(errorCode);\n const base = {\n method: request.method,\n route,\n status: request.status,\n duration_ms: request.durationMs,\n };\n return {\n ok: true,\n properties: keepsErrorCode ? { ...base, error_code: errorCode } : base,\n droppedErrorCode: errorCode !== undefined && !keepsErrorCode,\n };\n}\n", "import type { BatchAttribution } from './batch.js';\n\nexport const UTM_PARAMETERS = [\n ['utm_source', 'source'],\n ['utm_medium', 'medium'],\n ['utm_campaign', 'campaign'],\n] as const;\nexport const AD_CLICK_PARAMETERS = [\n 'gclid',\n 'gbraid',\n 'wbraid',\n 'fbclid',\n 'msclkid',\n 'ttclid',\n 'li_fat_id',\n 'twclid',\n 'dclid',\n] as const;\nexport const MAX_UTM_VALUE_LENGTH = 64;\nexport const MAX_REFERRER_HOST_LENGTH = 128;\n\nconst HOSTNAME_PATTERN = /^[a-z0-9-]+(\\.[a-z0-9-]+)*$/;\nconst WWW_PREFIX = /^www\\./;\n\nexport interface AttributionSource {\n readonly search: string;\n readonly referrer: string | undefined;\n readonly ownHost: string;\n}\n\ntype Utm = NonNullable<BatchAttribution['utm']>;\n\nfunction readUtm(parameters: URLSearchParams): Utm | undefined {\n const entries = UTM_PARAMETERS.flatMap(([parameter, field]) => {\n const value = parameters.get(parameter)?.trim().slice(0, MAX_UTM_VALUE_LENGTH);\n return value ? [[field, value] as const] : [];\n });\n return entries.length === 0 ? undefined : Object.fromEntries(entries);\n}\n\nfunction hostOf(url: string): string | undefined {\n try {\n return new URL(url).hostname.toLowerCase().replace(WWW_PREFIX, '');\n } catch {\n return undefined;\n }\n}\n\nfunction referrerHost(referrer: string | undefined, ownHost: string): string | undefined {\n const host = referrer ? hostOf(referrer) : undefined;\n const isUsable =\n host !== undefined &&\n host.length <= MAX_REFERRER_HOST_LENGTH &&\n HOSTNAME_PATTERN.test(host) &&\n host !== ownHost.toLowerCase().replace(WWW_PREFIX, '');\n return isUsable ? host : undefined;\n}\n\nexport function buildAttribution(source: AttributionSource): BatchAttribution {\n const parameters = new URLSearchParams(source.search);\n const utm = readUtm(parameters);\n const host = referrerHost(source.referrer, source.ownHost);\n return {\n ...(host === undefined ? {} : { referrer_host: host }),\n ...(utm === undefined ? {} : { utm }),\n from_ad_click: AD_CLICK_PARAMETERS.some((parameter) => parameters.has(parameter)),\n };\n}\n", "import { buildAttribution } from './core/attribution.js';\nimport { templatePath } from './core/path-template.js';\nimport type { Tracker } from './tracker.js';\n\nexport const PAGE_VIEW = 'page_view';\n\nexport interface PageLocation {\n readonly pathname: string;\n readonly search: string;\n readonly hostname: string;\n}\n\nexport interface PageViewDependencies {\n readonly location: () => PageLocation;\n readonly referrer: string;\n readonly watch: (onNavigate: () => void) => () => void;\n readonly guard: (action: () => void) => void;\n}\n\nexport function startPageViews(\n tracker: Tracker,\n pathRules: readonly string[],\n deps: PageViewDependencies,\n): () => void {\n let lastPath: string | undefined;\n let referrer: string | undefined = deps.referrer;\n\n const recordPageView = (): void => {\n const { pathname, search, hostname } = deps.location();\n const path = templatePath(pathname, pathRules);\n if (path === lastPath) {\n return;\n }\n lastPath = path;\n const entryAttribution = buildAttribution({ search, referrer, ownHost: hostname });\n referrer = undefined;\n tracker.enqueue({ name: PAGE_VIEW, path, entryAttribution });\n };\n\n deps.guard(recordPageView);\n return deps.watch(() => {\n deps.guard(recordPageView);\n });\n}\n", "export type PropertyValue = string | number | boolean;\n\nexport interface BatchAttribution {\n readonly referrer_host?: string;\n readonly utm?: {\n readonly source?: string;\n readonly medium?: string;\n readonly campaign?: string;\n };\n readonly from_ad_click: boolean;\n}\n\nexport interface BatchEvent {\n readonly id: string;\n readonly name: string;\n readonly occurred_at: string;\n readonly session_id: string;\n readonly user_id?: string;\n readonly path: string;\n readonly attribution?: BatchAttribution;\n readonly properties?: Readonly<Record<string, PropertyValue>>;\n}\n\nexport interface Batch {\n readonly key: string;\n readonly sent_at: string;\n readonly events: readonly BatchEvent[];\n}\n\nexport function buildBatch(key: string, sentAt: number, events: readonly BatchEvent[]): Batch {\n return { key, sent_at: new Date(sentAt).toISOString(), events };\n}\n\nexport function serializeBatch(batch: Batch): string {\n return JSON.stringify(batch);\n}\n", "import type { BatchEvent } from './batch.js';\n\nexport const FLUSH_EVENT_COUNT = 10;\nexport const FLUSH_DELAY_MS = 5000;\nexport const MAX_EVENTS_PER_BATCH = 50;\nexport const MAX_BATCH_BYTES = 32 * 1024;\nexport const MAX_QUEUE_SIZE = 500;\n\nconst encoder = new TextEncoder();\n\nexport function byteLength(text: string): number {\n return encoder.encode(text).length;\n}\n\nexport function shouldFlushNow(queueLength: number): boolean {\n return queueLength >= FLUSH_EVENT_COUNT;\n}\n\nexport function enqueue(\n queue: readonly BatchEvent[],\n event: BatchEvent,\n capacity: number = MAX_QUEUE_SIZE,\n): { readonly queue: readonly BatchEvent[]; readonly accepted: boolean } {\n return queue.length >= capacity\n ? { queue, accepted: false }\n : { queue: [...queue, event], accepted: true };\n}\n\nexport interface ChunkResult {\n readonly chunks: readonly (readonly BatchEvent[])[];\n readonly oversized: readonly BatchEvent[];\n}\n\nexport function chunkEvents(\n events: readonly BatchEvent[],\n serialize: (events: readonly BatchEvent[]) => string,\n maxEvents: number = MAX_EVENTS_PER_BATCH,\n maxBytes: number = MAX_BATCH_BYTES,\n): ChunkResult {\n const chunks: BatchEvent[][] = [];\n const oversized: BatchEvent[] = [];\n let current: BatchEvent[] = [];\n const fits = (candidate: readonly BatchEvent[]) =>\n candidate.length <= maxEvents && byteLength(serialize(candidate)) <= maxBytes;\n\n for (const event of events) {\n if (!fits([event])) {\n oversized.push(event);\n continue;\n }\n const candidate = [...current, event];\n if (fits(candidate)) {\n current = candidate;\n continue;\n }\n chunks.push(current);\n current = [event];\n }\n if (current.length > 0) {\n chunks.push(current);\n }\n return { chunks, oversized };\n}\n", "export const SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;\n\nexport interface SessionState {\n readonly id: string;\n readonly lastActivityAt: number;\n readonly userId?: string;\n readonly entryRecorded?: true;\n}\n\nexport interface SessionTouch {\n readonly session: SessionState;\n readonly isNew: boolean;\n}\n\nexport function touchSession(\n current: SessionState | undefined,\n now: number,\n newId: () => string,\n): SessionTouch {\n if (current !== undefined && now - current.lastActivityAt <= SESSION_IDLE_TIMEOUT_MS) {\n return { session: { ...current, lastActivityAt: now }, isNew: false };\n }\n const session: SessionState =\n current?.userId === undefined\n ? { id: newId(), lastActivityAt: now }\n : { id: newId(), lastActivityAt: now, userId: current.userId };\n return { session, isNew: true };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport function parseSession(raw: string | null): SessionState | undefined {\n if (raw === null) {\n return undefined;\n }\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (\n !isRecord(value) ||\n typeof value.id !== 'string' ||\n typeof value.lastActivityAt !== 'number' ||\n !Number.isFinite(value.lastActivityAt)\n ) {\n return undefined;\n }\n const { id, lastActivityAt, userId, entryRecorded } = value;\n return {\n id,\n lastActivityAt,\n ...(typeof userId === 'string' ? { userId } : {}),\n ...(entryRecorded === true ? { entryRecorded } : {}),\n };\n}\n\nexport function recordEntry(session: SessionState): SessionState {\n return { ...session, entryRecorded: true };\n}\n\nexport function serializeSession(session: SessionState): string {\n return JSON.stringify(session);\n}\n", "import type { KeyValueStore } from './adapters/storage.js';\nimport type { Transport } from './adapters/transport.js';\nimport {\n type Batch,\n type BatchAttribution,\n type BatchEvent,\n buildBatch,\n type PropertyValue,\n serializeBatch,\n} from './core/batch.js';\nimport { chunkEvents, enqueue, FLUSH_DELAY_MS, shouldFlushNow } from './core/batch-policy.js';\nimport { IDENTIFY_EVENT } from './core/event-input.js';\nimport type { ResolvedOptions } from './core/options.js';\nimport { judgeOutcome, retryDelay, type SendOutcome } from './core/retry-policy.js';\nimport {\n parseSession,\n recordEntry,\n type SessionState,\n serializeSession,\n touchSession,\n} from './core/session-policy.js';\n\nexport const SESSION_KEY = 'pyxis:session';\n\nexport interface TrackerDependencies {\n readonly now: () => number;\n readonly createId: () => string;\n readonly random: () => number;\n readonly sessionStore: KeyValueStore;\n readonly transport: Transport;\n}\n\nexport interface EventInput {\n readonly name: string;\n readonly path: string;\n readonly entryAttribution?: BatchAttribution;\n readonly properties?: Readonly<Record<string, PropertyValue>>;\n}\n\nexport interface Tracker {\n enqueue(input: EventInput): void;\n identify(userId: string, path: string): void;\n reset(): void;\n flush(pageHidden?: boolean): void;\n dispose(): void;\n}\n\nconst NETWORK_ERROR: SendOutcome = { kind: 'network-error' };\n\nexport function createTracker(options: ResolvedOptions, deps: TrackerDependencies): Tracker {\n let queue: readonly BatchEvent[] = [];\n let flushTimer: ReturnType<typeof setTimeout> | undefined;\n const retryTimers = new Set<ReturnType<typeof setTimeout>>();\n let disposed = false;\n\n const touchCurrentSession = (\n claimsEntry: boolean,\n ): { session: SessionState; isEntry: boolean } => {\n const stored = parseSession(deps.sessionStore.read(SESSION_KEY));\n const { session } = touchSession(stored, deps.now(), deps.createId);\n const isEntry = claimsEntry && session.entryRecorded !== true;\n const updated = isEntry ? recordEntry(session) : session;\n deps.sessionStore.write(SESSION_KEY, serializeSession(updated));\n return { session: updated, isEntry };\n };\n\n const tryNotifyBatch = (batch: Batch): void => {\n try {\n options.onBatch?.(batch);\n } catch (error) {\n console.warn('[pyxis] debug.onBatch threw; the batch is sent anyway', error);\n }\n };\n\n const scheduleRetry = (\n events: readonly BatchEvent[],\n attempt: number,\n outcome: SendOutcome,\n ): void => {\n if (disposed || judgeOutcome(outcome) !== 'retry') {\n return;\n }\n const retryAfter = outcome.kind === 'response' ? outcome.retryAfterSeconds : undefined;\n const delay = retryDelay(attempt, deps.random, retryAfter);\n if (delay === undefined) {\n return;\n }\n const timer = setTimeout(() => {\n retryTimers.delete(timer);\n send(events, attempt + 1, false);\n }, delay);\n retryTimers.add(timer);\n };\n\n const send = (events: readonly BatchEvent[], attempt: number, pageHidden: boolean): void => {\n const batch = buildBatch(options.key, deps.now(), events);\n tryNotifyBatch(batch);\n if (options.dryRun) {\n return;\n }\n void deps.transport.send(options.batchUrl, serializeBatch(batch), pageHidden).then(\n (outcome) => {\n scheduleRetry(events, attempt, outcome);\n },\n () => {\n scheduleRetry(events, attempt, NETWORK_ERROR);\n },\n );\n };\n\n const flush = (pageHidden = false): void => {\n if (flushTimer !== undefined) {\n clearTimeout(flushTimer);\n flushTimer = undefined;\n }\n if (disposed || queue.length === 0) {\n return;\n }\n const { chunks } = chunkEvents(queue, (events) =>\n serializeBatch(buildBatch(options.key, deps.now(), events)),\n );\n queue = [];\n for (const chunk of chunks) {\n send(chunk, 0, pageHidden);\n }\n };\n\n const enqueueEvent = (input: EventInput): void => {\n if (disposed) {\n return;\n }\n const { session, isEntry } = touchCurrentSession(input.entryAttribution !== undefined);\n const event: BatchEvent = {\n id: deps.createId(),\n name: input.name,\n occurred_at: new Date(deps.now()).toISOString(),\n session_id: session.id,\n path: input.path,\n ...(session.userId === undefined ? {} : { user_id: session.userId }),\n ...(isEntry ? { attribution: input.entryAttribution } : {}),\n ...(input.properties === undefined ? {} : { properties: input.properties }),\n };\n queue = enqueue(queue, event).queue;\n if (shouldFlushNow(queue.length)) {\n flush();\n return;\n }\n flushTimer ??= setTimeout(() => {\n flushTimer = undefined;\n flush();\n }, FLUSH_DELAY_MS);\n };\n\n const identify = (userId: string, path: string): void => {\n if (disposed) {\n return;\n }\n const { session } = touchSession(\n parseSession(deps.sessionStore.read(SESSION_KEY)),\n deps.now(),\n deps.createId,\n );\n const alreadyIdentified = session.userId === userId;\n deps.sessionStore.write(SESSION_KEY, serializeSession({ ...session, userId }));\n if (!alreadyIdentified) {\n enqueueEvent({ name: IDENTIFY_EVENT, path });\n }\n };\n\n const reset = (): void => {\n if (disposed) {\n return;\n }\n flush();\n deps.sessionStore.remove(SESSION_KEY);\n };\n\n return {\n enqueue: enqueueEvent,\n identify,\n reset,\n flush,\n dispose: () => {\n disposed = true;\n queue = [];\n if (flushTimer !== undefined) {\n clearTimeout(flushTimer);\n }\n retryTimers.forEach((timer) => {\n clearTimeout(timer);\n });\n retryTimers.clear();\n },\n };\n}\n", "import { createBrowserDependencies } from './adapters/browser-dependencies.js';\nimport { watchNavigation } from './adapters/navigation.js';\nimport { onPageHide } from './adapters/page-lifecycle.js';\nimport { readPrivacySignals } from './adapters/privacy-signals.js';\nimport { webStore } from './adapters/storage.js';\nimport type { PropertyValue } from './core/batch.js';\nimport { isBrowserLike } from './core/environment.js';\nimport { checkEventName, isValidUserId, sanitizeProperties } from './core/event-input.js';\nimport { type PyxisOptions, resolveOptions } from './core/options.js';\nimport { templatePath } from './core/path-template.js';\nimport {\n API_REQUEST_EVENT,\n checkTrackedRequest,\n type TrackedRequest,\n} from './core/tracked-request.js';\nimport { isTrackingAllowed, OPT_OUT_KEY, OPT_OUT_VALUE } from './core/privacy.js';\nimport { startPageViews } from './page-views.js';\nimport { createTracker, type Tracker } from './tracker.js';\n\nexport type { Batch, BatchAttribution, BatchEvent, PropertyValue } from './core/batch.js';\nexport type { PyxisDebugOptions, PyxisOptions } from './core/options.js';\nexport type { RequestMethod, TrackedRequest } from './core/tracked-request.js';\n\nexport type Properties = Readonly<Record<string, PropertyValue>>;\n\nlet started = false;\nlet debugEnabled = false;\nlet tracker: Tracker | undefined;\nlet stoppers: readonly (() => void)[] = [];\nlet pathRules: readonly string[] = [];\n\nfunction bestEffort(action: () => void): void {\n try {\n action();\n } catch (error) {\n if (debugEnabled) {\n console.warn('[pyxis] ignored an internal error', error);\n }\n }\n}\n\nfunction debugWarn(message: string): void {\n if (debugEnabled) {\n console.warn(`[pyxis] ${message}`);\n }\n}\n\nfunction currentPath(): string {\n return templatePath(window.location.pathname, pathRules);\n}\n\nfunction localStore() {\n return webStore(() => window.localStorage);\n}\n\nfunction stop(): void {\n stoppers.forEach((stopOne) => {\n stopOne();\n });\n tracker?.dispose();\n stoppers = [];\n tracker = undefined;\n}\n\nfunction startAutoPageViews(current: Tracker, pathRules: readonly string[]): () => void {\n return startPageViews(current, pathRules, {\n location: () => window.location,\n referrer: document.referrer,\n watch: (onNavigate) => watchNavigation(onNavigate, window),\n guard: bestEffort,\n });\n}\n\nexport function init(options: PyxisOptions): void {\n bestEffort(() => {\n if (started || !isBrowserLike(globalThis)) {\n return;\n }\n started = true;\n debugEnabled = options.debug !== undefined;\n const resolution = resolveOptions(options);\n if (!resolution.ok) {\n return;\n }\n if (!isTrackingAllowed(readPrivacySignals(navigator, window, localStore()))) {\n return;\n }\n const current = createTracker(resolution.options, createBrowserDependencies());\n pathRules = resolution.options.pathRules;\n tracker = current;\n const stopFlushingOnHide = onPageHide(() => {\n current.flush(true);\n });\n stoppers = resolution.options.autoPageViews\n ? [stopFlushingOnHide, startAutoPageViews(current, pathRules)]\n : [stopFlushingOnHide];\n });\n}\n\nexport function track(name: string, properties?: Properties): void {\n bestEffort(() => {\n if (tracker === undefined) {\n return;\n }\n const nameCheck = checkEventName(name);\n if (nameCheck !== 'ok') {\n const why =\n nameCheck === 'reserved' ? 'is reserved for the SDK' : 'is not a valid event name';\n debugWarn(`track() ignored \"${name}\": the name ${why}`);\n return;\n }\n const sanitized = sanitizeProperties(properties);\n for (const { key, reason } of sanitized.dropped) {\n debugWarn(`track(\"${name}\") dropped the property \"${key}\" (${reason})`);\n }\n const hasProperties = Object.keys(sanitized.properties).length > 0;\n tracker.enqueue({\n name,\n path: currentPath(),\n ...(hasProperties ? { properties: sanitized.properties } : {}),\n });\n });\n}\n\nexport function identify(userId: string): void {\n bestEffort(() => {\n if (tracker === undefined) {\n return;\n }\n if (!isValidUserId(userId)) {\n debugWarn('identify() ignored a user id outside [A-Za-z0-9_-]{1,64}; never pass an email');\n return;\n }\n tracker.identify(userId, currentPath());\n });\n}\n\nexport function reset(): void {\n bestEffort(() => {\n tracker?.reset();\n });\n}\n\nexport function trackRequest(request: TrackedRequest): void {\n bestEffort(() => {\n if (tracker === undefined) {\n return;\n }\n const check = checkTrackedRequest(request, pathRules);\n if (!check.ok) {\n debugWarn(`trackRequest() ignored a request with an invalid ${check.reason}`);\n return;\n }\n if (check.droppedErrorCode) {\n debugWarn('trackRequest() dropped an errorCode outside [a-z0-9_.]{1,64}');\n }\n tracker.enqueue({ name: API_REQUEST_EVENT, path: currentPath(), properties: check.properties });\n });\n}\n\nexport function optOut(): void {\n bestEffort(() => {\n if (!isBrowserLike(globalThis)) {\n return;\n }\n localStore().write(OPT_OUT_KEY, OPT_OUT_VALUE);\n stop();\n });\n}\n\nexport function optIn(): void {\n bestEffort(() => {\n if (isBrowserLike(globalThis)) {\n localStore().remove(OPT_OUT_KEY);\n }\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["toHex", "bytes", "byte", "uuidFromBytes", "hex", "variant", "createUuid", "source", "memoryStore", "values", "key", "value", "webStore", "getStorage", "fallback", "RETRY_DELAYS_MS", "RETRY_JITTER", "TOO_MANY_REQUESTS", "judgeOutcome", "outcome", "status", "retryDelay", "attempt", "random", "retryAfterSeconds", "base", "jittered", "parseRetryAfter", "header", "BODY_CONTENT_TYPE", "browserTransport", "fetchImpl", "beacon", "viaBeacon", "url", "body", "viaFetch", "response", "retryAfterSeconds", "parseRetryAfter", "pageHidden", "createBrowserDependencies", "createUuid", "webStore", "browserTransport", "watchNavigation", "onNavigate", "target", "history", "pushState", "replaceState", "args", "onPageHide", "listener", "doc", "win", "onVisibilityChange", "OPT_OUT_KEY", "DO_NOT_TRACK_VALUES", "isDoNotTrack", "values", "value", "isTrackingAllowed", "signals", "textOrNull", "value", "readPrivacySignals", "nav", "win", "store", "isDoNotTrack", "OPT_OUT_KEY", "isBrowserLike", "scope", "IDENTIFY_EVENT", "RESERVED_EVENT_NAMES", "MAX_PROPERTIES", "MAX_PROPERTY_STRING_LENGTH", "EVENT_NAME_PATTERN", "PROPERTY_KEY_PATTERN", "USER_ID_PATTERN", "checkEventName", "name", "isValidUserId", "userId", "isPropertyValue", "value", "sanitizeProperties", "input", "kept", "dropped", "entries", "key", "BATCH_PATH", "batchUrlFor", "endpoint", "url", "resolveOptions", "options", "key", "batchUrl", "base", "onBatch", "UUID_PATTERN", "ALL_DIGITS", "DIGIT", "ENCODED_AT_SIGN", "isIdentifier", "segment", "UUID_PATTERN", "ALL_DIGITS", "DIGIT", "ENCODED_AT_SIGN", "withoutTrailingSlashes", "path", "end", "matchesRule", "segments", "rule", "ruleSegments", "ruleSegment", "index", "templatePath", "rules", "candidate", "API_REQUEST_EVENT", "REQUEST_METHODS", "MAX_ROUTE_LENGTH", "MAX_REQUEST_DURATION_MS", "MAX_HTTP_STATUS", "ERROR_CODE_PATTERN", "URL_BASE", "isMethod", "value", "isIntegerBetween", "min", "max", "routeOf", "url", "rules", "templatePath", "checkTrackedRequest", "request", "route", "errorCode", "keepsErrorCode", "base", "UTM_PARAMETERS", "AD_CLICK_PARAMETERS", "HOSTNAME_PATTERN", "WWW_PREFIX", "readUtm", "parameters", "entries", "UTM_PARAMETERS", "parameter", "field", "value", "hostOf", "url", "referrerHost", "referrer", "ownHost", "host", "buildAttribution", "source", "utm", "AD_CLICK_PARAMETERS", "PAGE_VIEW", "startPageViews", "tracker", "pathRules", "deps", "lastPath", "referrer", "recordPageView", "pathname", "search", "hostname", "path", "templatePath", "entryAttribution", "buildAttribution", "buildBatch", "key", "sentAt", "events", "serializeBatch", "batch", "encoder", "byteLength", "text", "shouldFlushNow", "queueLength", "enqueue", "queue", "event", "capacity", "chunkEvents", "events", "serialize", "maxEvents", "maxBytes", "chunks", "oversized", "current", "fits", "candidate", "touchSession", "current", "now", "newId", "isRecord", "value", "parseSession", "raw", "id", "lastActivityAt", "userId", "entryRecorded", "recordEntry", "session", "serializeSession", "SESSION_KEY", "NETWORK_ERROR", "createTracker", "options", "deps", "queue", "flushTimer", "retryTimers", "disposed", "touchCurrentSession", "claimsEntry", "stored", "parseSession", "session", "touchSession", "isEntry", "updated", "recordEntry", "serializeSession", "tryNotifyBatch", "batch", "error", "scheduleRetry", "events", "attempt", "outcome", "judgeOutcome", "retryAfter", "delay", "retryDelay", "timer", "send", "pageHidden", "buildBatch", "serializeBatch", "flush", "chunks", "chunkEvents", "chunk", "enqueueEvent", "input", "event", "enqueue", "shouldFlushNow", "userId", "path", "alreadyIdentified", "IDENTIFY_EVENT", "started", "debugEnabled", "tracker", "stoppers", "pathRules", "bestEffort", "action", "error", "debugWarn", "message", "currentPath", "templatePath", "localStore", "webStore", "stop", "stopOne", "startAutoPageViews", "current", "startPageViews", "onNavigate", "watchNavigation", "init", "options", "isBrowserLike", "resolution", "resolveOptions", "isTrackingAllowed", "readPrivacySignals", "createTracker", "createBrowserDependencies", "stopFlushingOnHide", "onPageHide", "track", "name", "properties", "nameCheck", "checkEventName", "sanitized", "sanitizeProperties", "key", "reason", "hasProperties", "identify", "userId", "isValidUserId", "reset", "trackRequest", "request", "check", "checkTrackedRequest", "API_REQUEST_EVENT", "optOut", "OPT_OUT_KEY", "optIn"]
7
+ }
@@ -0,0 +1,14 @@
1
+ import type { Tracker } from './tracker.js';
2
+ export declare const PAGE_VIEW = "page_view";
3
+ export interface PageLocation {
4
+ readonly pathname: string;
5
+ readonly search: string;
6
+ readonly hostname: string;
7
+ }
8
+ export interface PageViewDependencies {
9
+ readonly location: () => PageLocation;
10
+ readonly referrer: string;
11
+ readonly watch: (onNavigate: () => void) => () => void;
12
+ readonly guard: (action: () => void) => void;
13
+ }
14
+ export declare function startPageViews(tracker: Tracker, pathRules: readonly string[], deps: PageViewDependencies): () => void;
@@ -0,0 +1,26 @@
1
+ import type { KeyValueStore } from './adapters/storage.js';
2
+ import type { Transport } from './adapters/transport.js';
3
+ import { type BatchAttribution, type PropertyValue } from './core/batch.js';
4
+ import type { ResolvedOptions } from './core/options.js';
5
+ export declare const SESSION_KEY = "pyxis:session";
6
+ export interface TrackerDependencies {
7
+ readonly now: () => number;
8
+ readonly createId: () => string;
9
+ readonly random: () => number;
10
+ readonly sessionStore: KeyValueStore;
11
+ readonly transport: Transport;
12
+ }
13
+ export interface EventInput {
14
+ readonly name: string;
15
+ readonly path: string;
16
+ readonly entryAttribution?: BatchAttribution;
17
+ readonly properties?: Readonly<Record<string, PropertyValue>>;
18
+ }
19
+ export interface Tracker {
20
+ enqueue(input: EventInput): void;
21
+ identify(userId: string, path: string): void;
22
+ reset(): void;
23
+ flush(pageHidden?: boolean): void;
24
+ dispose(): void;
25
+ }
26
+ export declare function createTracker(options: ResolvedOptions, deps: TrackerDependencies): Tracker;
package/package.json CHANGED
@@ -1,26 +1,80 @@
1
1
  {
2
2
  "name": "pyxis-analytics",
3
- "version": "0.0.0",
4
- "description": "Privacy-first product analytics tracker for the browser. Name reserved; the first usable release is 0.1.0.",
5
- "license": "MIT",
6
- "author": "Samuel Santana",
7
- "homepage": "https://github.com/samuelcsantana/pyxis-sdk#readme",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/samuelcsantana/pyxis-sdk.git"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/samuelcsantana/pyxis-sdk/issues"
14
- },
3
+ "version": "0.1.0",
4
+ "description": "Privacy-first product analytics tracker for the browser: no cookies, no personal data, zero dependencies.",
15
5
  "keywords": [
16
6
  "analytics",
17
- "privacy",
18
7
  "product-analytics",
8
+ "privacy",
19
9
  "tracker",
20
10
  "lgpd",
21
- "gdpr"
11
+ "gdpr",
12
+ "zero-dependency"
22
13
  ],
14
+ "homepage": "https://github.com/samuelcsantana/pyxis-sdk#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/samuelcsantana/pyxis-sdk/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/samuelcsantana/pyxis-sdk.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Samuel Santana",
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ }
31
+ },
23
32
  "files": [
24
- "README.md"
25
- ]
33
+ "dist"
34
+ ],
35
+ "scripts": {
36
+ "prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
37
+ "build": "esbuild src/index.ts --bundle --minify --format=esm --target=es2020 --sourcemap --outfile=dist/index.js && tsc -p tsconfig.build.json",
38
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json && tsc -p scripts/tsconfig.json && tsc -p tsconfig.e2e.json",
39
+ "test": "vitest run",
40
+ "test:cov": "vitest run --coverage",
41
+ "test:watch": "vitest",
42
+ "lint": "eslint . --max-warnings 0 && node scripts/check-no-comments.mts",
43
+ "format": "prettier --write .",
44
+ "format:check": "prettier --check .",
45
+ "test:tooling": "node --test \"eslint-rules/**/*.test.mjs\" \"scripts/**/*.test.mts\"",
46
+ "size": "node scripts/check-size.mts",
47
+ "prepare": "husky",
48
+ "contract:sync": "node scripts/contract-sync.mts",
49
+ "build:playground": "node scripts/build-playground.mts",
50
+ "test:e2e": "playwright test"
51
+ },
52
+ "devDependencies": {
53
+ "@commitlint/cli": "^21.2.3",
54
+ "@commitlint/config-conventional": "^21.2.3",
55
+ "@eslint/js": "^10.0.1",
56
+ "@playwright/test": "^1.63.0",
57
+ "@types/node": "^24.19.1",
58
+ "@vitest/coverage-v8": "^5.0.3",
59
+ "ajv": "^8.20.0",
60
+ "ajv-formats": "^3.0.1",
61
+ "esbuild": "^0.28.2",
62
+ "eslint": "^10.12.0",
63
+ "eslint-config-prettier": "^10.1.8",
64
+ "husky": "^9.1.7",
65
+ "jsdom": "^30.1.2",
66
+ "lint-staged": "^17.6.0",
67
+ "prettier": "^3.9.9",
68
+ "typescript": "~6.0.3",
69
+ "typescript-eslint": "^8.71.1",
70
+ "vitest": "^5.0.3"
71
+ },
72
+ "lint-staged": {
73
+ "*.{ts,mts,mjs}": [
74
+ "eslint --max-warnings 0",
75
+ "prettier --check"
76
+ ],
77
+ "src/**/*.ts": "vitest related --run",
78
+ "*.{json,yml,yaml,md}": "prettier --check"
79
+ }
26
80
  }