# mytart

**Multi-Yield Tracking & Analytics Relay Tool** — framework-agnostic analytics for any ESM JavaScript/TypeScript project.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Documentation](#documentation)
- [Providers](#providers)
  - [Google Analytics 4](#google-analytics-4)
  - [Google Ads](#google-ads)
  - [Mixpanel](#mixpanel)
  - [Segment](#segment)
  - [Amplitude](#amplitude)
  - [Plausible](#plausible)
  - [PostHog](#posthog)
  - [Meta Pixel](#meta-pixel)
  - [Microsoft Clarity](#microsoft-clarity)
  - [Hotjar](#hotjar)
  - [Heap](#heap)
  - [TikTok](#tiktok)
  - [Snapchat](#snapchat)
  - [Twitter/X](#twitterx)
  - [Reddit](#reddit)
  - [Pinterest](#pinterest)
  - [Microsoft Ads](#microsoft-ads)
- [Standardized Event Taxonomy](#standardized-event-taxonomy)
- [Bot Filtering](#bot-filtering)
- [Cross-Provider Linking](#cross-provider-linking)
- [Browser Fingerprint](#browser-fingerprint)
- [Provider Groups](#provider-groups)
- [Retry & Dead-Letter Queue](#retry--dead-letter-queue)
- [Debug Mode](#debug-mode)
- [State Management](#state-management)
- [API Reference](#api-reference)
- [TypeScript](#typescript)
- [Custom Providers](#custom-providers)
- [Framework Integration](#framework-integration)
- [License](#license)

`mytart` makes direct HTTP calls to analytics provider endpoints via [axios](https://axios-http.com/) — no bloated SDK wrappers, no global state, no lock-in.

## Features

- 🔌 **17 providers out of the box**: Google Analytics 4, Google Ads, Mixpanel, Segment, Amplitude, Plausible, PostHog, Meta Pixel, Microsoft Clarity, Hotjar, Heap, TikTok, Snapchat, Twitter/X, Reddit, Pinterest, Microsoft Ads
- 🏷️ **Standardized event taxonomy**: 36 standard events (purchase, lead, sign_up, bet_placed, first_bet, deposit, etc.) auto-mapped to each provider's native conventions
- 🎯 **Provider groups**: route events to subsets of providers (`marketing`, `product`, `infrastructure`, `physical`) — send conversion events only to ad platforms, product events only to analytics tools
- 🌐 **Universal**: works in Node.js, browsers, and any JS framework (Next.js, Remix, Astro, SvelteKit, etc.)
- 🔷 **TypeScript-first**: precise typings, object-parameter style for great DX
- 📦 **Dual ESM/CJS output**: works with `import` and `require`
- 🪶 **Lightweight**: direct HTTP via axios, no SDK overhead
- 🤖 **Bot filtering**: optionally ignore bots and crawlers via [ua-parser-js](https://github.com/nicolevanderhoeven/ua-parser-js)
- 🔗 **Cross-provider linking**: auto-captures click IDs (`gclid`, `fbclid`, `msclkid`, etc.) and cookies, then injects them into every provider call for unified attribution
- 🪪 **Browser fingerprint**: uses [ThumbmarkJS](https://thumbmarkjs.com) to generate a stable, cookieless device identifier as `anonymousId` — consistent across sessions, cache clears, and incognito mode. Enabled by default, lazy-loaded, SSR-safe
- 🔄 **Automatic retries**: configurable exponential backoff with jitter for 429/5xx/network errors — transparent to all providers via axios interceptor
- 📬 **Dead-letter queue**: failed retryable events are queued in memory for later replay, with optional callback for external persistence
- 🔍 **Debug mode**: global `debug` flag activates provider-native validation endpoints (GA4, Google Ads, Snapchat) and captures full request/response details in `TrackResult.debugInfo`
- 🛡️ **Global consent flag**: `consent: true/false` on `MytartConfig` auto-configures consent across all supporting providers (GA Consent Mode v2 + Meta Pixel grant/revoke)
- ✅ **Node.js ≥ 18**

## Installation

```bash
npm install mytart
# or
pnpm add mytart
# or
yarn add mytart
```

## Quick Start

```typescript
import { Mytart } from 'mytart';

const analytics = new Mytart({
  providers: [
    { provider: 'segment', writeKey: 'YOUR_WRITE_KEY' },
    { provider: 'posthog', apiKey: 'phc_YOUR_KEY' },
  ],
  defaultUserId: 'user_123',
  // browserFingerprint is enabled by default — a stable device fingerprint
  // is automatically generated and used as anonymousId for all providers
});

// Track an event
await analytics.track({ event: 'signup', properties: { plan: 'pro' } });

// Identify a user
await analytics.identify({ userId: 'user_123', traits: { name: 'Alice', email: 'alice@example.com' } });

// Track a page view
await analytics.page({ url: 'https://example.com/pricing', name: 'Pricing' });
```

## Documentation

For detailed documentation on specific topics, see the `docs/` folder:

### Guides
- **[Betting / iGaming Events](docs/betting-events.md)** — Complete guide to betting event properties, provider mappings, and the property mapping system

### Architecture
- **[Mytart Core](docs/architecture/mytart-core.md)** — Main class, parallel dispatch, provider groups, typed track overloads
- **[Base Provider](docs/architecture/base-provider.md)** — Abstract class, executeRequest pattern, createHttp factory
- **[Event Taxonomy](docs/architecture/event-taxonomy.md)** — Standardized event name mapping across providers
- **[Cross-Provider Linking](docs/architecture/cross-provider-linking.md)** — Click ID + cookie capture, xpl_ properties
- **[HTTP Client](docs/architecture/http-client.md)** — Axios factory, retry interceptor, DLQ, debug capture
- **[PII Hashing](docs/architecture/pii-hashing.md)** — SHA-256 utility, hashUserData, provider-specific wrappers
- **[State Management](docs/architecture/state-management.md)** — Central userId/anonymousId/sessionId, trait persistence
- **[Consent Mode](docs/architecture/consent-mode.md)** — Global toggle, GA Consent Mode v2, Meta binary, Clarity cookie
- **[Browser Fingerprinting](docs/architecture/browser-fingerprinting.md)** — ThumbmarkJS, lazy resolution, cookieless device ID
- **[Bot Filtering](docs/architecture/bot-filtering.md)** — ua-parser-js bot detection, global toggle, fail-open

### Provider Documentation
- **[Google Analytics](docs/providers/google-analytics.md)** — GA4 Measurement Protocol, gtag.js, Consent Mode v2
- **[Google Ads](docs/providers/google-ads.md)** — Conversion tracking, Enhanced Conversions API
- **[Meta Pixel](docs/providers/meta-pixel.md)** — fbq pixel, Conversions API, Advanced Matching
- **[TikTok](docs/providers/tiktok.md)** — ttq pixel, Events API v1.3
- **[Snapchat](docs/providers/snapchat.md)** — snaptr pixel, Conversions API v3
- **[Twitter/X](docs/providers/twitter.md)** — twq pixel, Conversions API
- **[Reddit](docs/providers/reddit.md)** — rdt pixel, Conversions API v2.0
- **[Pinterest](docs/providers/pinterest.md)** — pintrk tag, Conversions API v5
- **[Microsoft Ads](docs/providers/microsoft-ads.md)** — UET tag, Offline Conversions API
- **[Mixpanel](docs/providers/mixpanel.md)** — HTTP API, Title Case events
- **[Segment](docs/providers/segment.md)** — HTTP API, pass-through event names
- **[Amplitude](docs/providers/amplitude.md)** — HTTP API v2, Title Case events
- **[Plausible](docs/providers/plausible.md)** — Events API, privacy-first
- **[PostHog](docs/providers/posthog.md)** — Capture API, $session_id in properties
- **[Clarity](docs/providers/clarity.md)** — Microsoft session replay, browser-only

## Providers

### Testing Status

| Provider | Status |
|---|---|
| Google Analytics 4 | Tested and confirmed working |
| Google Ads | Not yet tested |
| Mixpanel | Not yet tested |
| Segment | Not yet tested |
| Amplitude | Not yet tested |
| Plausible | Not yet tested |
| PostHog | Not yet tested |
| Meta Pixel | Not yet tested |
| Microsoft Clarity | Tested and confirmed working |
| Hotjar | Not yet tested |
| Heap | Not yet tested |
| TikTok | Not yet tested |
| Snapchat | Not yet tested |
| Twitter/X | Not yet tested |
| Reddit | Not yet tested |
| Pinterest | Not yet tested |
| Microsoft Ads | Not yet tested |

### Google Analytics 4

GA4 supports two modes via the `appType` option:

#### Server mode (default)

Uses the [GA4 Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4) — direct HTTP calls, no browser APIs. Use this for Node.js, API routes, serverless functions, etc.

```typescript
{
  provider: 'google-analytics',
  measurementId: 'G-XXXXXXXXXX',
  apiSecret: 'YOUR_SECRET',
  enabled: true,
  // appType defaults to 'server'
}
```

#### Browser mode

Injects Google's official [gtag.js snippet](https://developers.google.com/tag-platform/gtagjs) into the page. Use this for client-side tracking in any framework (React, Vue, Svelte, plain HTML, etc.). No `apiSecret` needed.

```typescript
{
  provider: 'google-analytics',
  measurementId: 'G-XXXXXXXXXX',
  appType: 'browser',
  enabled: true,
}
```

When `appType: 'browser'` is set:

- The gtag.js script is loaded once on the first `track()`, `identify()`, or `page()` call
- All calls use the standard `gtag()` API — compatible with Google Tag Tester and Tag Assistant
- SSR-safe: silently succeeds when `window` is undefined (e.g. during server-side rendering)
- `apiSecret` is not required (and not used)

#### Google Signals (demographics)

To enable demographic data (age, gender, interests) in GA4 reports, set `signals: true`:

```typescript
{
  provider: 'google-analytics',
  measurementId: 'G-XXXXXXXXXX',
  appType: 'browser',
  enabled: true,
  signals: true,
}
```

This does two things automatically:

1. Passes `allow_google_signals: true` and `allow_ad_personalization_signals: true` to `gtag('config')`
2. Sets Consent Mode v2 defaults granting `ad_personalization`, `ad_user_data`, `ad_storage`, and `analytics_storage`

Set `signals: false` to explicitly disable Google Signals. Omit the flag entirely to use Google's default behaviour.

> **Note**: You must also enable Google Signals in the GA4 admin panel (Admin > Data Settings > Data Collection) for demographic data to appear.

#### Consent Mode v2

For GDPR/privacy compliance you can control Consent Mode v2 directly. Use `defaultConsent` to set the initial consent state (emitted before `gtag('config')`), and `updateConsent()` to change it at runtime when the user interacts with a cookie banner.

```typescript
const analytics = new Mytart({
  providers: [{
    provider: 'google-analytics',
    measurementId: 'G-XXXXXXXXXX',
    appType: 'browser',
    enabled: true,
    signals: true,
    defaultConsent: {
      ad_storage: 'denied',
      analytics_storage: 'denied',
      ad_user_data: 'denied',
      ad_personalization: 'denied',
    },
    consentWaitForUpdate: 500, // wait 500ms for consent banner
  }],
});

// After the user accepts the cookie banner:
await analytics.updateConsent({
  ad_storage: 'granted',
  analytics_storage: 'granted',
  ad_user_data: 'granted',
  ad_personalization: 'granted',
});
```

When both `signals: true` and `defaultConsent` are set, the explicit `defaultConsent` takes precedence over the auto-consent that `signals` would generate. This lets you combine `signals: true` (for the config flags) with a GDPR-safe denied-by-default consent flow.

Consent Mode is a no-op in server mode (the Measurement Protocol does not support it).

### Google Ads

Google Ads supports two modes via the `appType` option:

#### Browser mode (default)

Uses gtag.js to fire conversion events via `gtag('event', 'conversion', { send_to: ... })`. If GA4 is also configured, the same gtag.js instance is reused.

```typescript
{
  provider: 'google-ads',
  conversionActionId: 'AW-123456789/AbC1dEfG2hIjK3lM',
  appType: 'browser',
  enabled: true,
}
```

#### Server mode (Enhanced Conversions)

Uses the [Google Ads API](https://developers.google.com/google-ads/api/docs/conversions/overview) to upload conversion adjustments with hashed user identifiers for enhanced matching.

```typescript
{
  provider: 'google-ads',
  conversionActionId: 'AW-123456789/AbC1dEfG2hIjK3lM',
  customerId: '123-456-7890',
  accessToken: 'YOUR_OAUTH_TOKEN',
  developerToken: 'YOUR_DEV_TOKEN',
  appType: 'server',
  userIdentifiers: ['email', 'phone'],
  enabled: true,
}
```

#### Enhanced Conversions

Configure which user identifiers to send for conversion matching:

```typescript
{
  provider: 'google-ads',
  conversionActionId: 'AW-123456789/AbC1dEfG2hIjK3lM',
  customerId: '123-456-7890',
  accessToken: 'YOUR_OAUTH_TOKEN',
  developerToken: 'YOUR_DEV_TOKEN',
  appType: 'server',
  userIdentifiers: ['email', 'phone', 'address'],
  defaultUserIdentifier: {
    email: 'user@example.com',
    phone: '+15551234567',
    address: {
      firstName: 'Jane',
      lastName: 'Doe',
      countryCode: 'US',
      postalCode: '10001',
    },
  },
  enabled: true,
}
```

PII fields are automatically normalized and SHA-256 hashed before sending:
- **Email**: lowercased, Gmail dots/plus suffix removed, then hashed
- **Phone**: normalized to E.164 format (+CCNNNNNNNN), then hashed
- **Address**: first name, last name, street address lowercased and hashed; country code, postal code, city, state are NOT hashed

#### Conversion tracking with value

Pass `orderId`, `value`, and `currency` for conversion value reporting and deduplication:

```typescript
await analytics.track({
  event: 'purchase',
  properties: { items: ['SKU-001'] },
  orderId: 'order-abc-123',
  value: 149.99,
  currency: 'USD',
});
```

#### Identify for enhanced conversions

Call `identify()` to cache user identifiers for subsequent conversions:

```typescript
await analytics.identify({
  userId: 'user-42',
  traits: {
    email: 'user@example.com',
    phone: '+15551234567',
  },
});

// Subsequent track() calls will include the cached identifiers
await analytics.track({ event: 'purchase', value: 99.99, currency: 'USD' });
```

#### Event taxonomy

Google Ads uses the standardized event taxonomy by default. You can customize mappings or disable auto-mapping:

```typescript
{
  provider: 'google-ads',
  conversionActionId: 'AW-123456789/AbC1dEfG2hIjK3lM',
  appType: 'browser',
  eventTaxonomy: {
    customMappings: {
      purchase: 'online_sale',
      lead: 'qualified_lead',
    },
  },
  enabled: true,
}
```

### Mixpanel

```typescript
{ provider: 'mixpanel', token: 'YOUR_TOKEN', apiUrl?: string }
```

### Segment

```typescript
{ provider: 'segment', writeKey: 'YOUR_WRITE_KEY', apiUrl?: string }
```

### Amplitude

```typescript
{ provider: 'amplitude', apiKey: 'YOUR_API_KEY', apiUrl?: string }
```

### Plausible

```typescript
{ provider: 'plausible', domain: 'example.com', apiUrl?: string, userAgent?: string, xForwardedFor?: string }
```

> **Note**: Plausible does not support `identify()` — it returns an error result.

### PostHog

```typescript
{ provider: 'posthog', apiKey: 'phc_YOUR_KEY', apiUrl?: string }
```

### Meta Pixel

Meta Pixel supports two modes via the `appType` option:

#### Server mode (default)

Uses the [Meta Conversions API](https://developers.facebook.com/docs/marketing-api/conversions-api) — direct HTTP calls, no browser APIs. Use this for Node.js, API routes, serverless functions, etc.

```typescript
{
  provider: 'meta-pixel',
  pixelId: '123456789',
  accessToken: 'YOUR_ACCESS_TOKEN',
  enabled: true,
  // appType defaults to 'server'
}
```

PII fields in `user_data` (`em`, `ph`, `fn`, `ln`, `ge`, `db`, `ct`, `st`, `zp`, `country`) are automatically SHA-256 hashed before being sent to the Conversions API. Already-hashed values are not double-hashed.

#### Browser mode

Injects Meta's official [fbevents.js snippet](https://developers.facebook.com/docs/meta-pixel/get-started) into the page. Use this for client-side tracking in any framework.

```typescript
{
  provider: 'meta-pixel',
  pixelId: '123456789',
  appType: 'browser',
  advancedMatching: { em: 'user@example.com' },
  enabled: true,
}
```

When `appType: 'browser'` is set:

- The fbevents.js script is loaded once on the first `track()`, `identify()`, or `page()` call
- Standard Meta events (e.g. `Purchase`, `AddToCart`, `ViewContent`) use `fbq('track', ...)` — custom events use `fbq('trackCustom', ...)`
- SSR-safe: silently succeeds when `window` is undefined
- `accessToken` is not required

#### Event deduplication

Pass `eventId` via context to deduplicate browser + server events:

```typescript
await analytics.track({
  event: 'Purchase',
  properties: { currency: 'USD', value: 42 },
  context: { eventId: 'order-abc-123' },
});
```

In browser mode this passes `{ eventID: 'order-abc-123' }` as the 4th `fbq()` parameter. In server mode it sets the `event_id` field in the Conversions API payload.

#### Identify

In browser mode, `identify()` re-calls `fbq('init', pixelId, newTraits)` to update Advanced Matching data. In server mode, traits are cached in memory and included as `user_data` in all subsequent `track()` / `page()` calls.

#### Consent

Meta Pixel uses a simple binary consent model (`fbq('consent', 'grant')` / `fbq('consent', 'revoke')`).

The easiest way to manage Meta consent is via the **global consent flag** on `MytartConfig`:

```typescript
const analytics = new Mytart({
  consent: true,   // auto-grants consent for all supporting providers (GA + Meta)
  providers: [
    { provider: 'meta-pixel', pixelId: '123456', appType: 'browser', enabled: true },
    { provider: 'google-analytics', measurementId: 'G-XXX', appType: 'browser', enabled: true },
  ],
});
```

You can also manage consent at runtime via the standard `updateConsent()` API — `ad_storage` maps to Meta's binary model:

```typescript
// Grant consent (bridges ad_storage: 'granted' → fbq('consent', 'grant'))
await analytics.updateConsent({ ad_storage: 'granted' });

// Revoke consent (bridges ad_storage: 'denied' → fbq('consent', 'revoke'))
await analytics.updateConsent({ ad_storage: 'denied' });
```

For direct low-level access, use the provider instance:

```typescript
import { MetaPixelProvider } from 'mytart';

const provider = new MetaPixelProvider({ provider: 'meta-pixel', pixelId: '123', appType: 'browser' });
await provider.updatePixelConsent(true);   // fbq('consent', 'grant')
await provider.updatePixelConsent(false);  // fbq('consent', 'revoke')
```

### Microsoft Clarity

[Microsoft Clarity](https://clarity.microsoft.com/) is a free behavioral analytics tool that provides session recordings, heatmaps, and insights. Clarity is **browser-only** — there is no server mode.

```typescript
{
  provider: 'clarity',
  projectId: 'YOUR_PROJECT_ID',
  enabled: true,
}
```

When enabled:

- The official `https://www.clarity.ms/tag/{projectId}` script is loaded once on the first `track()`, `identify()`, or `page()` call
- `track()` fires `clarity('event', eventName)` and sets each property as a custom tag via `clarity('set', key, value)`
- `identify()` calls `clarity('identify', userId, sessionId, undefined, friendlyName)` with session ID as the native `custom-session-id` parameter, and sets remaining traits as custom tags
- `page()` fires a `PageView` event and sets `pageUrl`, `pageName`, and `referrer` as custom tags
- SSR-safe: silently succeeds when `window` is undefined

#### Cookie consent

By default Clarity operates in cookieless mode. To enable cookie-based tracking, set `cookie: true`:

```typescript
{
  provider: 'clarity',
  projectId: 'YOUR_PROJECT_ID',
  cookie: true,
  enabled: true,
}
```

This calls `clarity('consent')` during initialisation so Clarity can set cookies for more accurate session tracking.

### Hotjar

[Hotjar](https://www.hotjar.com/) provides heatmaps, session recordings, and user feedback. Hotjar is **browser-only** — there is no server mode.

```typescript
{
  provider: 'hotjar',
  siteId: 123456,
  enabled: true,
}
```

When enabled:

- The official Hotjar script is loaded once on the first `track()`, `identify()`, or `page()` call
- `track()` fires `hj('event', eventName)` and sets properties via `hj('tagRecording', ...)`
- `identify()` calls `hj('identify', userId, traits)` to attach user information
- `page()` calls `hj('stateChange', url)` for SPA navigation tracking
- SSR-safe: silently succeeds when `window` is undefined

#### Configuration

- `siteId` — Your Hotjar site ID (required)
- `version` — Hotjar SDK version, defaults to 6
- `debug` — Enable debug mode

### Heap

[Heap](https://heap.io/) provides product analytics with retroactive event capture. Heap supports both **browser** and **server** modes.

```typescript
// Browser mode
{
  provider: 'heap',
  appId: '123456789',
  appType: 'browser',
  enabled: true,
}

// Server mode (HTTP API)
{
  provider: 'heap',
  appId: '123456789',
  appType: 'server',
  enabled: true,
}
```

#### Browser mode

In browser mode, Heap uses the official `heap.js` SDK via `window.heap()` calls:

- `track()` calls `heap('track', eventName, properties)`
- `identify()` calls `heap('identify', userId)` and `heap('addUserProperties', traits)`
- `page()` calls `heap('trackPageview', pageData)`

#### Server mode

In server mode, Heap sends events via the HTTP API (`https://heapanalytics.com/api`):

- Authentication uses `app_id` in the request body (no API key required)
- You must provide either `userId` or `anonymousId` for each event
- Heap's server API does **not** support batch tracking

> **Note:** Heap's server-side API returns HTTP 200 for all requests, including malformed payloads. Ensure your `app_id` and `identity` values are valid before treating a response as success.

### TikTok

[TikTok](https://ads.tiktok.com/) supports two modes via the `appType` option:

#### Server mode (default)

Uses the [TikTok Events API v1.3](https://business-api.tiktok.com/portal/docs?id=1741601162187777) — direct HTTP calls, no browser APIs. Use this for Node.js, API routes, serverless functions, etc.

```typescript
{
  provider: 'tiktok',
  pixelCode: 'CXXXXXXXXXXXXXXXXX',
  accessToken: 'YOUR_ACCESS_TOKEN',
  enabled: true,
  // appType defaults to 'server'
}
```

PII fields (`email`, `phone_number`, `external_id`) are automatically SHA-256 hashed before being sent to the Events API. `ip` and `user_agent` are NOT hashed. Already-hashed values (64-char hex) are not double-hashed.

#### Browser mode

Injects TikTok's official [Pixel script](https://ads.tiktok.com/help/article?aid=10000357) into the page. Use this for client-side tracking in any framework.

```typescript
{
  provider: 'tiktok',
  pixelCode: 'CXXXXXXXXXXXXXXXXX',
  appType: 'browser',
  enabled: true,
}
```

When `appType: 'browser'` is set:

- The `analytics.tiktok.com/i18n/pixel/events.js` script is loaded once on the first call
- `track()` calls `ttq.track(event, properties)`, `identify()` calls `ttq.identify({ external_id, email, phone_number })`
- `page()` calls `ttq.page()`
- SSR-safe: silently succeeds when `window` is undefined

#### Event deduplication

Pass `eventId` via context to deduplicate browser + server events:

```typescript
await analytics.track({
  event: 'purchase',
  properties: { value: 42, currency: 'USD' },
  context: { eventId: 'order-abc-123' },
});
```

#### Identify

In browser mode, `identify()` calls `ttq.identify({ external_id, email, phone_number })`. In server mode, traits are cached in memory and included in all subsequent `track()` / `page()` calls — no standalone identify HTTP call.

#### Conversion enrichment

```typescript
await analytics.track({
  event: 'purchase',
  properties: { contents: [{ content_id: 'SKU-001', quantity: 1 }] },
  orderId: 'order-abc',
  value: 99.99,
  currency: 'USD',
});
```

#### Test events

Use `testEventCode` on the config to test without affecting production data:

```typescript
{
  provider: 'tiktok',
  pixelCode: 'CXXXXXXXXXXXXXXXXX',
  accessToken: 'YOUR_TOKEN',
  testEventCode: 'TEST12345',
  enabled: true,
}
```

### Snapchat

[Snapchat](https://forbusiness.snapchat.com/) supports two modes via the `appType` option:

#### Server mode (default)

Uses the [Snapchat Conversions API v3](https://marketingapi.snapchat.com/docs/conversion.html) — direct HTTP calls, no browser APIs.

```typescript
{
  provider: 'snapchat',
  pixelId: '1234567890',
  accessToken: 'YOUR_ACCESS_TOKEN',
  enabled: true,
  // appType defaults to 'server'
}
```

PII fields (`em`, `ph`, `fn`, `ln`, `ge`, `ct`, `st`, `zp`, `country`) are automatically SHA-256 hashed via the shared `hashUserData()` utility. `external_id`, `sc_click_id`, `sc_cookie1`, `client_ip_address`, and `client_user_agent` are NOT hashed.

#### Browser mode

Injects Snapchat's official [Snap Pixel](https://businesshelp.snapchat.com/s/article/snap-pixel-about) into the page.

```typescript
{
  provider: 'snapchat',
  pixelId: '1234567890',
  appType: 'browser',
  enabled: true,
}
```

When `appType: 'browser'` is set:

- The `sc-static.net/scevent.min.js` script is loaded once on the first call
- `track()` calls `snaptr('track', event, params)`, `page()` calls `snaptr('track', 'PAGE_VIEW')`
- `identify()` re-calls `snaptr('init', pixelId, userData)` for advanced matching
- SSR-safe: silently succeeds when `window` is undefined

#### Event deduplication

Pass `eventId` via context — maps to `client_dedup_id` in browser mode, `event_id` in server mode.

#### Identify

In browser mode, `identify()` re-calls `snaptr('init', pixelId, { user_email, user_phone_number })` to update advanced matching data. In server mode, traits are cached in memory and included in all subsequent `track()` / `page()` calls.

#### Debug / validation mode

Set `debug: true` on the Snapchat config (or global `debug: true`) to route server-mode requests to the validation endpoint (`/events/validate`) instead of the live endpoint:

```typescript
{
  provider: 'snapchat',
  pixelId: '1234567890',
  accessToken: 'YOUR_TOKEN',
  debug: true,
  enabled: true,
}
```

#### Test events

Use `testEventCode` on the config to test without affecting production data.

### Twitter/X

[Twitter/X](https://ads.x.com/) supports two modes via the `appType` option:

#### Server mode (default)

Uses the [X Conversions API](https://developer.x.com/en/docs/x-ads-api/measurement/conversions/guides/implementing-the-conversions-api) — direct HTTP calls with **OAuth 1.0a** authentication (HMAC-SHA1 signatures, no external dependency).

```typescript
{
  provider: 'twitter',
  pixelId: 'oka17',
  appType: 'server',
  oauthCredentials: {
    consumerKey: 'YOUR_CONSUMER_KEY',
    consumerSecret: 'YOUR_CONSUMER_SECRET',
    accessToken: 'YOUR_ACCESS_TOKEN',
    accessTokenSecret: 'YOUR_ACCESS_TOKEN_SECRET',
  },
  enabled: true,
}
```

PII fields are SHA-256 hashed: `email` → `hashed_email`, `phone` → `hashed_phone_number`. `twclid`, `ip_address`, and `user_agent` are NOT hashed. Identifiers are sent as an **array of objects** (each identifier is its own object).

#### Browser mode

Injects X's official [Pixel script](https://business.x.com/en/help/campaign-measurement-and-analytics/conversion-tracking-for-websites.html) into the page.

```typescript
{
  provider: 'twitter',
  pixelId: 'oka17',
  appType: 'browser',
  enabled: true,
}
```

When `appType: 'browser'` is set:

- The `static.ads-twitter.com/uwt.js` script is loaded once on the first call
- `track()` calls `twq('event', eventTag, params)`, `page()` calls `twq('event', 'PageView')`
- `identify()` passes PII (email, phone) via event params — the browser pixel auto-hashes PII
- SSR-safe: silently succeeds when `window` is undefined

#### Identify

In browser mode, `identify()` sends a `PageView` event with PII params for advanced matching. In server mode, traits are cached in memory and included in all subsequent `track()` / `page()` calls — no standalone identify HTTP call.

#### Conversion enrichment

`value` (sent as string), `currency` (mapped to `price_currency`), `orderId` (mapped to `conversion_id`) are forwarded in the conversion payload. Conversion time uses ISO 8601 format.

### Reddit

[Reddit](https://ads.reddit.com/) supports two modes via the `appType` option:

#### Server mode (default)

Uses the [Reddit Conversions API v2.0](https://ads-api.reddit.com/docs/v2/#tag/Conversions) — direct HTTP calls, no browser APIs.

```typescript
{
  provider: 'reddit',
  pixelId: 't2_abc123',
  accountId: 't2_abc123',
  accessToken: 'YOUR_ACCESS_TOKEN',
  enabled: true,
  // appType defaults to 'server'
}
```

`email` and `external_id` are SHA-256 hashed before sending. `ip_address`, `user_agent`, and `uuid` (Reddit `_rdt_uuid` cookie) are NOT hashed.

#### Browser mode

Injects Reddit's official [Pixel script](https://business.reddithelp.com/helpcenter/s/article/Install-the-Reddit-Pixel-on-your-website) into the page.

```typescript
{
  provider: 'reddit',
  pixelId: 't2_abc123',
  appType: 'browser',
  enabled: true,
}
```

When `appType: 'browser'` is set:

- The `redditstatic.com/ads/pixel.js` script is loaded once on the first call
- `track()` calls `rdt('track', event, params)`, `page()` calls `rdt('track', 'PageVisit')`
- `identify()` re-calls `rdt('init', pixelId, { externalId, email })` for advanced matching
- SSR-safe: silently succeeds when `window` is undefined

#### Standard events

Reddit only supports 8 standard event types natively: `PageVisit`, `ViewContent`, `Search`, `AddToCart`, `AddToWishlist`, `Purchase`, `Lead`, `SignUp`. All other events are sent as `tracking_type: 'Custom'` with a `custom_event_name` field.

#### Identify

In browser mode, `identify()` re-calls `rdt('init', pixelId, { externalId, email })`. In server mode, traits are cached and included in subsequent calls.

#### Conversion enrichment

`value` (mapped to `value_decimal`), `currency`, `orderId` (mapped to `order_id`), `itemCount`/`item_count`, and `contents` (mapped to `products`) are forwarded in the `event_metadata` object.

#### Test mode

Set `testMode: true` on the config — events are processed but not used for ad optimization:

```typescript
{
  provider: 'reddit',
  pixelId: 't2_abc123',
  accountId: 't2_abc123',
  accessToken: 'YOUR_TOKEN',
  testMode: true,
  enabled: true,
}
```

### Pinterest

[Pinterest](https://ads.pinterest.com/) supports two modes via the `appType` option:

#### Server mode (default)

Uses the [Pinterest Conversions API v5](https://developers.pinterest.com/docs/conversions/conversions/) — direct HTTP calls, no browser APIs.

```typescript
{
  provider: 'pinterest',
  tagId: '123456789012',
  adAccountId: '123456789012',
  accessToken: 'YOUR_ACCESS_TOKEN',
  enabled: true,
  // appType defaults to 'server'
}
```

PII fields (`em`, `ph`, `fn`, `ln`, `ge`, `db`, `ct`, `st`, `zp`, `country`) are SHA-256 hashed via `hashUserData()`, then **wrapped in arrays** per Pinterest's specification (e.g. `em: ['hash1']`). `external_id` is also hashed and wrapped in an array. `client_ip_address`, `client_user_agent`, and `click_id` are NOT hashed.

#### Browser mode

Injects Pinterest's official [Tag script](https://help.pinterest.com/en/business/article/install-the-pinterest-tag) into the page.

```typescript
{
  provider: 'pinterest',
  tagId: '123456789012',
  appType: 'browser',
  enabled: true,
}
```

When `appType: 'browser'` is set:

- The `s.pinimg.com/ct/core.js` script is loaded once on the first call
- Browser pixel uses concatenated-lowercase event names (e.g. `addtocart`, `viewcontent`) — the provider auto-converts from CAPI snake_case
- `identify()` calls `pintrk('set', { external_id, em })` for enhanced matching
- `page()` calls `pintrk('track', 'pagevisit')`
- SSR-safe: silently succeeds when `window` is undefined

#### Identify

In browser mode, `identify()` calls `pintrk('set', { external_id, em })`. In server mode, PII traits are normalized (lowercase, trim) before caching for subsequent calls.

#### Conversion enrichment

`value` (sent as **string** — Pinterest requirement), `currency`, `orderId` (mapped to `order_id`), `contents`, and `num_items` are forwarded in the `custom_data` object. Conversion time uses Unix timestamp in seconds.

#### Test mode

Set `testMode: true` on the config — appends `?test=true` to the CAPI endpoint:

```typescript
{
  provider: 'pinterest',
  tagId: '123456789012',
  adAccountId: '123456789012',
  accessToken: 'YOUR_TOKEN',
  testMode: true,
  enabled: true,
}
```

### Microsoft Ads

[Microsoft Advertising](https://ads.microsoft.com/) (Bing Ads) supports two modes via the `appType` option:

#### Server mode (default)

Uses the [Microsoft Advertising Offline Conversions API](https://learn.microsoft.com/en-us/advertising/campaign-management-service/applyofflineconversions) — uploads conversion data for offline/server-side matching.

```typescript
{
  provider: 'microsoft-ads',
  tagId: '12345678',
  accessToken: 'YOUR_OAUTH_TOKEN',
  developerToken: 'YOUR_DEV_TOKEN',
  customerId: '123456',
  accountId: '654321',
  enabled: true,
  // appType defaults to 'server'
}
```

Server mode requires 4 auth headers: `Authorization` (Bearer token), `DeveloperToken`, `CustomerAccountId`, `CustomerId`.

PII fields are SHA-256 hashed: `email` → `HashedEmailAddress` (lowercased, trimmed), `phone` → `HashedPhoneNumber` (trimmed). `MicrosoftClickId` (`msclkid`) is NOT hashed — it's the primary attribution key.

#### Browser mode

Injects the official [UET tag](https://help.ads.microsoft.com/#apex/ads/en/56682/2-500) script into the page.

```typescript
{
  provider: 'microsoft-ads',
  tagId: '12345678',
  appType: 'browser',
  enabled: true,
}
```

When `appType: 'browser'` is set:

- The `bat.bing.com/bat.js` script is loaded once on the first call
- `track()` pushes `uetq.push('event', eventName, params)` with revenue/currency
- `identify()` pushes `uetq.push('set', { pid: { em: hashedEmail, ph: hashedPhone } })` for Enhanced Conversions
- `page()` pushes `uetq.push('event', 'page_view', { page_path, page_title })`
- SSR-safe: silently succeeds when `window` is undefined

#### defaultConversionName

If you have a single conversion goal in Microsoft Ads, set `defaultConversionName` to use it for all events:

```typescript
{
  provider: 'microsoft-ads',
  tagId: '12345678',
  accessToken: 'YOUR_TOKEN',
  developerToken: 'YOUR_DEV_TOKEN',
  customerId: '123456',
  accountId: '654321',
  defaultConversionName: 'Online Purchase',
  enabled: true,
}
```

Without `defaultConversionName`, the (mapped) event name is used as the `ConversionName`.

#### Enhanced Conversions

Call `identify()` to cache PII for subsequent server-mode conversions:

```typescript
await analytics.identify({
  userId: 'user-42',
  traits: {
    email: 'user@example.com',
    phone: '+15551234567',
  },
});

// Subsequent track() calls include HashedEmailAddress and HashedPhoneNumber
await analytics.track({ event: 'purchase', value: 99.99, currency: 'USD' });
```

In browser mode, `identify()` hashes PII and pushes Enhanced Conversion matching data via `uetq.push('set', { pid: { em, ph } })`.

#### msclkid attribution

`msclkid` from cross-provider linking (`xpl_msclkid`) maps to `MicrosoftClickId` in the Offline Conversion payload — the primary key for matching offline conversions to ad clicks:

```typescript
const analytics = new Mytart({
  crossProviderLinking: true,
  providers: [
    { provider: 'microsoft-ads', tagId: '12345678', accessToken: 'YOUR_TOKEN', /* ... */ enabled: true },
  ],
});

// xpl_msclkid is auto-captured from the URL and included in all track() calls
await analytics.track({ event: 'purchase', value: 99.99, currency: 'USD' });
```

#### Page tracking

- **Browser mode**: `page()` pushes `uetq.push('event', 'page_view', { page_path, page_title })`.
- **Server mode**: `page()` returns `success: false` with `MICROSOFT_ADS_PAGE_NOT_SUPPORTED` error — the Offline Conversions API does not support page view tracking.

#### Conversion enrichment

`value` → `ConversionValue`, `currency` → `ConversionCurrencyCode`. `orderId` maps to `event_label` in browser mode for deduplication. Conversion time uses ISO 8601 format.

## Standardized Event Taxonomy

mytart includes a standardized set of 36 event names that automatically map to each provider's native event conventions. When you use a standard event name in `track()`, it's automatically translated to the correct format for each provider.

### Standard events

`purchase`, `lead`, `sign_up`, `login`, `search`, `add_to_cart`, `add_to_wishlist`, `begin_checkout`, `remove_from_cart`, `select_item`, `select_promotion`, `view_item`, `view_item_list`, `view_promotion`, `view_cart`, `add_payment_info`, `add_shipping_info`, `purchase_refund`, `subscribe`, `unsubscribe`, `contact`, `generate_lead`, `schedule`, `start_trial`, `complete_registration`, `donate`, `share`, `view_search_results`, `bet_placed`, `bet_settled`, `bet_cancelled`, `inplay_update`, `deposit`, `withdrawal`, `session_timeout`, `responsible_gambling_alert`, `first_bet`

### How mapping works

When you call `analytics.track({ event: 'purchase' })`, each provider receives the event in its native format:

| Standard Event | GA4 | Google Ads | Meta Pixel | Mixpanel | Segment / PostHog |
|---|---|---|---|---|---|
| `purchase` | `purchase` | `purchase` | `Purchase` | `Purchase` | `purchase` |
| `lead` | `generate_lead` | `lead` | `Lead` | `Lead` | `lead` |
| `add_to_cart` | `add_to_cart` | `add_to_cart` | `AddToCart` | `Add to Cart` | `add_to_cart` |
| `begin_checkout` | `begin_checkout` | `begin_checkout` | `InitiateCheckout` | `Begin Checkout` | `begin_checkout` |
| `sign_up` | `sign_up` | `sign_up` | `CompleteRegistration` | `Sign Up` | `sign_up` |
| `purchase_refund` | `refund` | `purchase_refund` | `Purchase` | `Purchase Refund` | `purchase_refund` |

Non-standard event names (e.g. `my_custom_event`) are always passed through unchanged to all providers.

Additional provider-specific mappings:

- **TikTok**: `purchase` → `CompletePayment`, `lead` → `SubmitForm`, `sign_up` → `CompleteRegistration`, `add_to_cart` → `AddToCart`, `begin_checkout` → `InitiateCheckout`, `view_item` → `ViewContent`
- **Snapchat**: `purchase` → `PURCHASE`, `lead` → `SIGN_UP`, `add_to_cart` → `ADD_CART`, `view_item` → `VIEW_CONTENT`, `begin_checkout` → `START_CHECKOUT`
- **Twitter/X**: `purchase` → `Purchase`, `lead` → `Lead`, `sign_up` → `SignUp`, `add_to_cart` → `AddToCart`, `begin_checkout` → `CheckoutInitiated`, `view_item` → `ContentView`
- **Reddit**: `purchase` → `Purchase`, `lead` → `Lead`, `sign_up` → `SignUp`, `add_to_cart` → `AddToCart`. Only 8 standard events supported; all others sent as `'Custom'`
- **Pinterest**: `purchase` → `checkout`, `lead` → `lead`, `sign_up` → `signup`, `add_to_cart` → `add_to_cart`, `view_item` → `view_content`. Browser mode auto-converts to concatenated-lowercase (`addtocart`, `viewcontent`)
- **Microsoft Ads**: snake_case pass-through similar to GA4/Google Ads. Aliases: `generate_lead` → `lead`, `complete_registration` → `sign_up`

### TrackOptions enrichment

`TrackOptions` supports additional fields for conversion-focused providers:

```typescript
await analytics.track({
  event: 'purchase',                    // standard event name
  properties: { items: ['SKU-001'] },
  orderId: 'order-abc-123',            // transaction ID for deduplication
  value: 149.99,                        // conversion value
  currency: 'USD',                      // ISO 4217 currency code
});
```

These fields are used by conversion-focused providers including Google Ads, Meta Pixel, TikTok, Snapchat, Twitter/X, Reddit, Pinterest, and Microsoft Ads.

### Betting / iGaming Events

mytart includes 9 standard betting events designed for sportsbook and iGaming platforms. These events follow the same auto-mapping logic as all other standard events and have fully typed property interfaces.

| Standard Event | GA4 | Google Ads | Meta Pixel | Mixpanel / Amplitude | Segment / PostHog / Plausible |
|---|---|---|---|---|---|
| `bet_placed` | `purchase` | `purchase` | `Purchase` | `Bet Placed` | `bet_placed` |
| `bet_settled` | `bet_settled` | *(custom)* | `BetSettled` | `Bet Settled` | `bet_settled` |
| `bet_cancelled` | `refund` | *(custom)* | `BetCancelled` | `Bet Cancelled` | `bet_cancelled` |
| `inplay_update` | `inplay_update` | *(custom)* | `InplayUpdate` | `Inplay Update` | `inplay_update` |
| `deposit` | `deposit` | *(custom)* | `Deposit` | `Deposit` | `deposit` |
| `withdrawal` | `withdrawal` | *(custom)* | `Withdrawal` | `Withdrawal` | `withdrawal` |
| `session_timeout` | `session_timeout` | *(custom)* | `SessionTimeout` | `Session Timeout` | `session_timeout` |
| `responsible_gambling_alert` | `responsible_gambling_alert` | *(custom)* | `ResponsibleGamblingAlert` | `Responsible Gambling Alert` | `responsible_gambling_alert` |
| `first_bet` | `purchase` | `purchase` | `Purchase` | `First Bet` | `first_bet` |

> **Note**: `bet_placed` and `first_bet` map to `purchase` in GA4 and Google Ads so bets appear in native revenue reports. `bet_cancelled` maps to `refund` in GA4. Google Ads only has a default mapping for `bet_placed` and `first_bet` — use `customMappings` to map other betting events to specific conversion actions.
>
> **Additional provider mappings**: TikTok and Twitter/X pass betting events through as-is (custom events). Reddit maps `bet_placed`/`first_bet` → `Purchase`, others as custom. Pinterest maps `bet_placed`/`first_bet` → `checkout`, others as-is. Microsoft Ads maps `bet_placed`/`first_bet` → `purchase`, others as-is.

#### Property Mapping

Different providers expect different field names for betting properties. mytart automatically maps standard property names to provider-specific conventions while **preserving the original properties** for custom analytics.

**Default property mappings:**

| Property | GA4 | Meta Pixel | TikTok |
|----------|-----|------------|--------|
| `stake_amount` | → `value` | → `value` | → `value` |
| `currency` | → `currency` | → `currency` | → `conversion_currency` |
| `value` | - | - | → `conversion_value` |

**Example: Automatic mapping preserves both original and mapped fields**

```typescript
await mytart.track({
  event: 'bet_placed',
  properties: {
    stake_amount: 25.0,
    odds: 2.5,
    sport: 'football',
    currency: 'GBP',
  },
});
```

**Sent to GA4:** `{ stake_amount: 25.0, value: 25.0, odds: 2.5, sport: 'football', currency: 'GBP' }`  
**Sent to TikTok:** `{ stake_amount: 25.0, value: 25.0, conversion_value: 25.0, odds: 2.5, sport: 'football', currency: 'GBP', conversion_currency: 'GBP' }`

#### Custom property mappings

You can override default mappings or add custom ones via `eventTaxonomy.propertyMappings`:

```typescript
{
  provider: 'google-analytics',
  measurementId: 'G-XXX',
  apiSecret: 'secret',
  eventTaxonomy: {
    propertyMappings: {
      stake_amount: 'value',
      odds: 'custom_odds_field',
      market_type: 'market',
    },
  },
  enabled: true,
}
```

> **See the [Betting / iGaming Events guide](docs/betting-events.md) for complete property interfaces, usage examples, and best practices.**

#### Usage examples

```typescript
// Track a bet placement (typed properties with IDE intellisense)
await analytics.track({
  event: 'bet_placed',
  properties: {
    stake_amount: 25.00,
    odds: 2.5,
    odds_format: 'decimal',
    market_type: 'match_winner',
    sport: 'football',
    league: 'Premier League',
    selection: 'Arsenal to win',
    bet_type: 'single',
    is_live: false,
    bet_id: 'bet-abc-123',
  },
  value: 25.00,
  currency: 'GBP',
  orderId: 'bet-abc-123',
});

// Track a bet settlement
await analytics.track({
  event: 'bet_settled',
  properties: {
    bet_id: 'bet-abc-123',
    settlement_type: 'win',
    payout_amount: 62.50,
    stake_amount: 25.00,
    odds: 2.5,
    sport: 'football',
  },
});

// Track a deposit
await analytics.track({
  event: 'deposit',
  properties: {
    amount: 100.00,
    payment_method: 'card',
    is_first_deposit: true,
  },
  value: 100.00,
  currency: 'GBP',
});

// Track a responsible gambling alert
await analytics.track({
  event: 'responsible_gambling_alert',
  properties: {
    alert_type: 'deposit_limit_reached',
    limit_amount: 500.00,
    limit_period: 'weekly',
  },
});
```

#### Custom mappings for Google Ads

Since only `bet_placed` has a default Google Ads mapping, use `customMappings` to map other betting events to your conversion actions:

```typescript
{
  provider: 'google-ads',
  conversionActionId: 'AW-123456789/AbC1dEfG2hIjK3lM',
  appType: 'browser',
  eventTaxonomy: {
    customMappings: {
      deposit: 'first_deposit',          // map deposit to a custom conversion
      bet_settled: 'bet_win',            // map settlement to a custom conversion
    },
  },
  enabled: true,
}
```

## Bot Filtering

Set `ignoreBots: true` at the top level to silently drop all `track()`, `identify()`, and `page()` calls when the visitor is a known bot or crawler. Detection is powered by [`ua-parser-js`](https://github.com/nicolevanderhoeven/ua-parser-js)'s `isBot()` function.

```typescript
const analytics = new Mytart({
  ignoreBots: true,
  providers: [
    { provider: 'google-analytics', measurementId: 'G-XXXXXXXXXX', enabled: true },
    { provider: 'segment', writeKey: 'YOUR_KEY', enabled: true },
  ],
});
```

When a bot is detected, all methods return an empty `TrackResult[]` array — no events are dispatched to any provider.

The User-Agent is read from:

1. `context.userAgent` if supplied in the `track()` call
2. `navigator.userAgent` in browser environments

If no User-Agent is available (e.g. server-side without `context.userAgent`), the call proceeds normally.

## Cross-Provider Linking

Set `crossProviderLinking: true` to automatically capture click IDs and analytics cookies, then inject them as `xpl_`-prefixed properties into every `track()`, `page()`, and `identify()` call. This lets you correlate events across providers — e.g. trace a Meta ad click through to a Clarity session recording or a Mixpanel funnel.

```typescript
const analytics = new Mytart({
  crossProviderLinking: true,
  // browserFingerprint (default: true) works alongside cross-provider linking —
  // fingerprint provides a stable anonymousId, linking captures click IDs and cookies
  providers: [
    { provider: 'google-analytics', measurementId: 'G-XXXXXXXXXX', appType: 'browser', enabled: true },
    { provider: 'meta-pixel', pixelId: '123456789', appType: 'browser', enabled: true },
    { provider: 'clarity', projectId: 'YOUR_PROJECT_ID', enabled: true },
    { provider: 'mixpanel', token: 'YOUR_TOKEN', enabled: true },
  ],
});
```

### What gets captured

| Source | Captured ID | Property key |
|---|---|---|
| `?gclid=` URL param | Google click ID | `xpl_gclid` |
| `?fbclid=` URL param | Meta click ID | `xpl_fbclid` |
| `?ttclid=` URL param | TikTok click ID | `xpl_ttclid` |
| `?msclkid=` URL param | Microsoft Ads click ID | `xpl_msclkid` |
| `?li_fat_id=` URL param | LinkedIn click ID | `xpl_li_fat_id` |
| `_fbp` cookie | Meta browser ID | `xpl_fbp` |
| `_fbc` cookie | Meta click ID cookie | `xpl_fbc` |
| `_ga` cookie | GA client ID | `xpl_ga_client_id` |

If `fbclid` is present in the URL but the `_fbc` cookie has not yet been set, mytart synthesises an `fbc` value in Meta's standard format (`fb.1.{timestamp}.{fbclid}`).

### How it flows to each provider

All captured IDs are injected as event properties (`track`/`page`) or user traits (`identify`). No provider code changes are needed — each provider already forwards properties to its API:

- **GA4** — `xpl_*` fields appear as event parameters and user properties (queryable in BigQuery exports)
- **Meta Pixel** — `xpl_*` fields become custom data parameters (`fbclid`/`_fbp`/`_fbc` are also handled natively by Meta)
- **Clarity** — `xpl_*` fields are set as custom tags, letting you filter session recordings by ad click source
- **Mixpanel / Amplitude / PostHog** — `xpl_*` fields become event and user properties, fully queryable
- **Segment** — `xpl_*` fields flow through to all downstream destinations in your Segment pipeline
- **Plausible** — `xpl_*` fields are sent as custom props (must be [registered in your Plausible dashboard](https://plausible.io/docs/custom-props/introduction) to appear in reports)
- **TikTok** — `xpl_ttclid` is extracted and included as `ttclid` in the Events API user data; other `xpl_*` fields forwarded as event properties
- **Snapchat** — `xpl_sccid` maps to `sc_click_id`, `xpl_sc_cookie1` to `sc_cookie1` in CAPI user_data; other `xpl_*` fields in custom_data
- **Twitter/X** — `xpl_twclid` maps to `twclid` identifier in the Conversions API payload
- **Reddit** — `xpl_*` fields forwarded in event_metadata for downstream analysis
- **Pinterest** — `xpl_epik` (Pinterest `_epik` cookie) maps to `click_id` in CAPI user_data for attribution matching
- **Microsoft Ads** — `xpl_msclkid` maps to `MicrosoftClickId` in the Offline Conversion payload — primary attribution key

### Inspecting captured IDs

Use `getCapturedIds()` to see what was captured at construction time:

```typescript
const ids = analytics.getCapturedIds();
// { fbclid: 'abc123', fbc: 'fb.1.1234567890.abc123', gaClientId: '1234567890.1234567890' }
```

Returns `null` when `crossProviderLinking` is disabled.

### Notes

- **Browser-only.** SSR-safe — returns empty results when `window` is undefined.
- **User properties take precedence.** If you pass a property with the same `xpl_*` key, your value wins.
- **Consent.** This feature reads existing URL parameters and cookies — it does not set new cookies or tracking identifiers. Each provider's own consent mechanism remains authoritative.

## Browser Fingerprint

By default (`browserFingerprint: true`), mytart uses [@thumbmarkjs/thumbmarkjs](https://thumbmarkjs.com) to generate a stable browser fingerprint and set it as the `anonymousId` in central state. This provides a consistent, cookieless device identifier that persists across sessions, cache clears, and incognito mode for the same browser/device.

### How it works

1. **Lazy resolution** — the fingerprint is computed on the first `track()`, `identify()`, or `page()` call, not at construction time. The first call **waits for the fingerprint to resolve** before dispatching to any provider — so all providers receive the fingerprint as their anonymous/device ID on the very first event. The result is cached for all subsequent calls.
2. **Dynamic import** — ThumbmarkJS is loaded via `import()` for tree-shaking. Server-only consumers never load the library.
3. **SSR-safe** — returns `undefined` when `window` is unavailable. No errors, no side effects.
4. **Graceful degradation** — if ThumbmarkJS fails or is not installed, tracking continues without a fingerprint (caught silently).
5. **Explicit values win** — fingerprint only sets `anonymousId` when no explicit value has been provided via `defaultAnonymousId`, `setAnonymousId()`, or per-call `anonymousId`.
6. **Full result exposed** — the complete ThumbmarkJS response (`ThumbmarkResponse`) is stored in `state.fingerprintData` and accessible via `mytart.getState().fingerprintData`. The fingerprint ID hash is also available as `state.fingerprintId` and via `mytart.getFingerprintId()` for direct access without digging into `fingerprintData`.

### How it flows to providers

The fingerprint (~64 char hash) flows to all providers as their anonymous/device ID:

| Provider | Field | Notes |
|---|---|---|
| GA4 | `client_id` | Both browser and server mode |
| Mixpanel | `distinct_id` | Falls back when no userId |
| PostHog | `distinct_id` | Falls back when no userId |
| Amplitude | `device_id` | Separate from `user_id` |
| Segment | `anonymousId` | Standard Segment field |
| Meta CAPI | `external_id` | Hashed before sending |
| TikTok | `external_id` | Hashed before sending |
| Reddit | `external_id` | Hashed before sending |
| Pinterest | `external_id` | Hashed, wrapped in array |
| Snapchat | `xpl_anonymous_id` | No dedicated external_id field |
| Twitter/X | `xpl_anonymous_id` | No dedicated external_id field |
| Microsoft Ads | `xpl_anonymous_id` | No dedicated external_id field |

### Disabling

Set `browserFingerprint: false` to disable:

```typescript
const analytics = new Mytart({
  browserFingerprint: false,
  providers: [/* ... */],
});
```

## Provider Groups

Provider groups let you route events to subsets of your providers. Assign providers to groups like `'marketing'`, `'product'`, `'infrastructure'`, or `'physical'`, then specify which groups should receive each event.

### Configuring groups

Add a `group` field to any provider config:

```typescript
const analytics = new Mytart({
  providers: [
    { provider: 'google-analytics', measurementId: 'G-XXX', appType: 'browser', enabled: true, group: 'product' },
    { provider: 'meta-pixel', pixelId: '123', appType: 'browser', enabled: true, group: 'marketing' },
    { provider: 'segment', writeKey: 'KEY', enabled: true, group: ['marketing', 'product'] },  // multiple groups
    { provider: 'clarity', projectId: 'ABC', enabled: true, group: 'product' },
    { provider: 'posthog', apiKey: 'phc_KEY', enabled: true },  // no group assigned
  ],
});
```

### Sending events to specific groups

Pass `groups` on any `track()`, `identify()`, or `page()` call:

```typescript
// Only marketing providers receive this event (Meta Pixel + Segment)
await analytics.track({
  event: 'purchase',
  properties: { value: 99.99 },
  groups: ['marketing'],
});

// Only product providers (GA4 + Segment + Clarity)
await analytics.page({
  url: 'https://example.com/dashboard',
  groups: ['product'],
});

// Multiple groups — providers in either group receive the event
await analytics.identify({
  userId: 'user-123',
  traits: { email: 'alice@example.com' },
  groups: ['marketing', 'product'],
});
```

### Behavior

- **No `groups` specified** (or empty array): all enabled providers receive the event — fully backward compatible
- **`groups` specified**: only providers whose group(s) intersect with the requested groups receive the event
- **Ungrouped providers excluded**: when `groups` is specified, providers with no `group` assigned are skipped (PostHog in the example above)
- **Multi-group providers**: a provider with `group: ['marketing', 'product']` matches either `groups: ['marketing']` or `groups: ['product']`

### Available groups

Four fixed group names: `'marketing'` | `'product'` | `'infrastructure'` | `'physical'`

| Group | Typical use |
|---|---|
| `marketing` | Ad platforms, conversion tracking (Meta, Google Ads, TikTok, Snapchat, etc.) |
| `product` | Product analytics, session recording (GA4, Mixpanel, Amplitude, PostHog, Clarity, etc.) |
| `infrastructure` | Data warehousing, ETL pipelines (Segment, custom providers) |
| `physical` | In-store / POS / offline conversions |

## Retry & Dead-Letter Queue

Server-side analytics events can fail silently due to transient errors (rate limits, network blips, server outages). mytart provides automatic retries with exponential backoff and a dead-letter queue (DLQ) for events that exhaust all retry attempts.

### Automatic retries

Pass a `retry` object on `MytartConfig` to enable retries for all server-mode providers:

```typescript
const analytics = new Mytart({
  retry: {
    maxRetries: 3,                              // default: 3
    baseDelay: 1000,                            // default: 1000ms
    maxDelay: 30000,                            // default: 30000ms
    retryableStatusCodes: [429, 500, 502, 503, 504],  // default
  },
  providers: [
    { provider: 'segment', writeKey: 'YOUR_KEY', enabled: true },
    { provider: 'posthog', apiKey: 'phc_YOUR_KEY', enabled: true },
  ],
});

// Or use defaults — just pass an empty object:
const analytics2 = new Mytart({
  retry: {},
  providers: [/* ... */],
});
```

Omit `retry` entirely to disable retries (single attempt per request).

**How it works:**

- Implemented as an axios response interceptor — transparent to all providers, zero retry code in provider implementations
- Exponential backoff with jitter: `baseDelay * 2^attempt * random(0.5–1.5)`
- Honors `Retry-After` header for 429 (Too Many Requests) responses
- Network errors (no response) are always retried
- Non-retryable status codes (e.g. 400, 401, 403) fail immediately

### Dead-letter queue

Events that fail after exhausting all retry attempts are automatically added to an in-memory dead-letter queue. This gives you a second chance to process them.

```typescript
const analytics = new Mytart({
  retry: {},
  deadLetterMaxSize: 500,  // default: 1000 — oldest entries evicted when full
  onDeadLetter: (event) => {
    // Optional callback — persist to database, send to Sentry, etc.
    console.error('Dead letter:', event.provider, event.error);
  },
  providers: [/* ... */],
});
```

Only events with `retryable: true` in their `TrackResult` are added to the DLQ. Client errors (400, 401, 403) are not queued since retrying them would produce the same result.

#### Inspecting the queue

```typescript
const queue = analytics.getDeadLetterQueue();
// Returns a shallow copy of DeadLetterEvent[]
// Each entry has: { id, provider, method, args, error, timestamp, attempts }
```

#### Replaying the queue

```typescript
const result = analytics.replayDeadLetterQueue();
// Returns: { replayed: number, failed: DeadLetterEvent[] }
// Successfully replayed events are removed; failures remain in the queue
```

#### Clearing the queue

```typescript
analytics.clearDeadLetterQueue();
```

### TrackResult enrichment

When retries are enabled, `TrackResult` includes additional metadata:

```typescript
const results = await analytics.track({ event: 'purchase', value: 99.99 });

for (const result of results) {
  console.log(result.attempts);   // total HTTP attempts (1 = no retries, 4 = 3 retries)
  console.log(result.retryable);  // true if the failure can be retried
  console.log(result.duration);   // wall-clock time in ms (including retries)
}
```

These fields are optional and non-breaking — they are `undefined` when retries are disabled or for browser-mode providers.

## Debug Mode

Set `debug: true` on `MytartConfig` to activate detailed request/response capture and provider-native validation modes. This is the recommended first step when diagnosing "events aren't showing up" issues.

```typescript
const analytics = new Mytart({
  debug: true,
  retry: {},
  providers: [
    { provider: 'google-analytics', measurementId: 'G-XXX', apiSecret: 'SECRET', enabled: true },
    { provider: 'segment', writeKey: 'YOUR_KEY', enabled: true },
  ],
});

const results = await analytics.track({ event: 'purchase', value: 42 });

for (const result of results) {
  if (result.debugInfo) {
    console.log(result.debugInfo.requestUrl);      // e.g. 'https://www.google-analytics.com/debug/mp/collect'
    console.log(result.debugInfo.requestPayload);   // the exact JSON sent
    console.log(result.debugInfo.responseBody);     // the provider's response
    console.log(result.debugInfo.responseStatus);   // HTTP status code
    console.log(result.debugInfo.duration);         // ms
    console.log(result.debugInfo.validationErrors); // provider-specific errors (if any)
  }
}
```

### What debug mode activates

The global `debug` flag cascades to all providers. Provider-level `config.debug` always takes precedence when explicitly set.

| Provider | Debug behavior |
|---|---|
| **GA4** | Routes server requests to `/debug/mp/collect` (validation endpoint) |
| **Google Ads** | Sets `validate_only: true` on API requests (validates without processing) |
| **Snapchat** | Routes server requests to `/events/validate` (validation endpoint) |
| **Meta Pixel** | Calls `fbq('set', 'debug', true)` in browser mode (verbose console logging) |
| **Heap** | Calls `heap('setDebug', true)` in browser mode (verbose console logging) |
| **All providers** | Captures full request/response in `TrackResult.debugInfo` |

### Provider-level override

You can enable debug for a specific provider without affecting others:

```typescript
const analytics = new Mytart({
  // debug: false (default) — most providers run normally
  providers: [
    {
      provider: 'google-analytics',
      measurementId: 'G-XXX',
      apiSecret: 'SECRET',
      debug: true,  // only GA4 uses the debug endpoint
      enabled: true,
    },
    { provider: 'segment', writeKey: 'YOUR_KEY', enabled: true },
  ],
});
```

Or disable debug for a specific provider when the global flag is on:

```typescript
const analytics = new Mytart({
  debug: true,
  providers: [
    {
      provider: 'google-analytics',
      measurementId: 'G-XXX',
      apiSecret: 'SECRET',
      debug: false,  // GA4 uses production endpoint despite global debug
      enabled: true,
    },
    { provider: 'snapchat', pixelId: 'SNAP_ID', accessToken: 'TOKEN', enabled: true },
    // Snapchat will use /events/validate because global debug is true
  ],
});
```

## State Management

Mytart maintains a central state (`userId`, `anonymousId`, `sessionId`) that all providers can access. This enables consistent user identification across all analytics calls without manually passing IDs to every method.

### How it works

1. **Initial state** is set from `defaultUserId`, `defaultAnonymousId`, and `defaultSessionId` in the config
2. **`identify()` updates state** — when you call `identify({ userId: 'abc' })`, the state is updated BEFORE dispatching to providers, ensuring all providers receive the updated userId
3. **All methods use state** — `track()`, `page()`, and `identify()` all receive the current state values (user-provided values override state defaults)

### State methods

```typescript
const analytics = new Mytart({
  defaultUserId: 'default-user',
  defaultAnonymousId: 'anon-123',  // explicit value — overrides browserFingerprint
  defaultSessionId: 'session-abc',
  providers: [{ provider: 'segment', writeKey: 'YOUR_KEY', enabled: true }],
});

// Get current state
const state = analytics.getState();
// { userId: 'default-user', anonymousId: 'anon-123', sessionId: 'session-abc' }

// Set individual IDs
analytics.setUserId('user-456');
analytics.setAnonymousId('anon-789');
analytics.setSessionId('session-xyz');

// Clear userId (e.g., on logout)
analytics.clearUserId();

// Clear session only (e.g., when session expires)
analytics.clearSessionId();

// Clear all state
analytics.clearState();

// identify() also updates state
await analytics.identify({ userId: 'user-789', traits: { email: 'alice@example.com' } });
// State is now: { userId: 'user-789', ... }
```

### How state flows to providers

All providers receive the current state in their `track()`, `identify()`, and `page()` calls. Each provider maps state to its API correctly:

- **GA4** — `user_id` and `session_id` in Measurement Protocol and gtag.js event params
- **Google Ads (browser)** — `user_id` set via `gtag('set')`, email/phone/address as enhanced conversion params
- **Google Ads (server)** — hashed user identifiers in `user_identifiers[]` payload; `identify()` caches identifiers
- **Segment** — `userId`, `anonymousId`, `sessionId` in track/identify/page
- **Amplitude** — `user_id`, `device_id`, `session_id` in events
- **PostHog** — `distinct_id` and `$session_id` in properties
- **Mixpanel** — `distinct_id` and `$session_id` in events
- **Meta Pixel (server)** — `external_id` in user_data, `xpl_anonymous_id` and `xpl_session_id` in custom_data
- **Meta Pixel (browser)** — `xpl_session_id` in event properties
- **Clarity** — `userId` passed to `clarity('identify', userId, sessionId, undefined, friendlyName)`, `sessionId` also set as custom tag
- **TikTok (server)** — `external_id` in user data (hashed), `xpl_anonymous_id` and `xpl_session_id` in event properties, `ttclid` from cross-provider linking
- **TikTok (browser)** — `external_id` via `ttq.identify({ external_id })`, email/phone_number also forwarded
- **Snapchat (server)** — `external_id` in user_data (NOT hashed), PII fields hashed via `hashUserData()`, `xpl_anonymous_id` and `xpl_session_id` in custom_data
- **Snapchat (browser)** — `user_email` and `user_phone_number` via `snaptr('init')` re-init for advanced matching
- **Twitter (server)** — `hashed_email` and `hashed_phone_number` in identifiers array (SHA-256), `twclid` from cross-provider linking
- **Twitter (browser)** — email/phone via `twq('event', eventTag, { email_address, phone_number })` (auto-hashed by pixel)
- **Reddit (server)** — `external_id` in user data (SHA-256 hashed), `email` hashed, `uuid` forwarded unhashed, `xpl_anonymous_id`/`xpl_session_id` in metadata
- **Reddit (browser)** — `externalId` and `email` via `rdt('init')` re-init for advanced matching
- **Pinterest (server)** — `external_id` in user_data (hashed, wrapped in array), PII fields hashed and wrapped in arrays, `click_id` (_epik cookie) forwarded unhashed
- **Pinterest (browser)** — `external_id` and `em` via `pintrk('set', { external_id, em })` for enhanced matching
- **Microsoft Ads (server)** — `HashedEmailAddress` and `HashedPhoneNumber` (SHA-256) in OfflineConversion payload, `MicrosoftClickId` (msclkid) forwarded unhashed
- **Microsoft Ads (browser)** — `em` and `ph` (SHA-256 hashed) via `uetq.push('set', { pid: { em, ph } })` for Enhanced Conversions
- **Plausible** — does not support user identification (privacy-first, by design)

## API Reference

### `new Mytart(config: MytartConfig)`

```typescript
interface MytartConfig {
  providers: ProviderConfig[];
  defaultUserId?: string;       // applied to every track/page call if no userId given
  defaultAnonymousId?: string;  // applied to every track/page call if no anonymousId given
  defaultSessionId?: string;    // applied to every track/page call if no sessionId given
  debug?: boolean;               // activates provider debug/validate modes + debugInfo capture
  ignoreBots?: boolean;          // when true, silently drops all tracking calls from known bots/crawlers
  crossProviderLinking?: boolean; // when true, auto-captures click IDs & cookies and injects them into every call
  browserFingerprint?: boolean;  // when true (default), generates a stable device fingerprint as anonymousId via ThumbmarkJS
  retry?: RetryConfig;           // enable automatic retries (pass {} for defaults, omit to disable)
  deadLetterMaxSize?: number;    // max DLQ entries (default: 1000)
  onDeadLetter?: (event: DeadLetterEvent) => void;  // callback when an event enters the DLQ
}
```

Every provider config accepts an `enabled` field.  Providers are **off by default** — you must set `enabled: true` to activate a provider:

```typescript
const analytics = new Mytart({
  providers: [
    { provider: 'segment',  writeKey: 'YOUR_KEY', enabled: true  }, // active
    { provider: 'mixpanel', token: 'YOUR_TOKEN'                  }, // skipped (off by default)
  ],
});
```

### `analytics.track(options: TrackOptions): Promise<TrackResult[]>`

```typescript
interface TrackOptions {
  event: string | StandardEventName;      // StandardEventName gives typed properties
  properties?: Record<string, unknown> & Partial<CrossProviderLinkingProperties>;  // xpl_* properties when crossProviderLinking is enabled
  userId?: string;
  anonymousId?: string;
  sessionId?: string;
  timestamp?: Date;
  context?: EventContext;
  orderId?: string;                       // transaction ID for deduplication
  value?: number;                         // conversion value
  currency?: string;                      // ISO 4217 currency code
  groups?: ProviderGroup[];               // only dispatch to providers in these groups
}
```

When you pass a `StandardEventName` as `event`, the `properties` are typed to that event's properties interface (e.g., `PurchaseEventProperties` for `'purchase'`).

### `analytics.identify(options: IdentifyOptions): Promise<TrackResult[]>`

```typescript
interface IdentifyOptions {
  userId: string;
  traits?: Record<string, unknown> & Partial<CrossProviderLinkingProperties>;  // xpl_* properties when crossProviderLinking is enabled
  anonymousId?: string;
  sessionId?: string;
  timestamp?: Date;
  groups?: ProviderGroup[];               // only dispatch to providers in these groups
}
```

### `analytics.page(options: PageOptions): Promise<TrackResult[]>`

```typescript
interface PageOptions {
  url: string;
  name?: string;
  referrer?: string;
  properties?: Record<string, unknown> & Partial<CrossProviderLinkingProperties>;  // xpl_* properties when crossProviderLinking is enabled
  userId?: string;
  anonymousId?: string;
  sessionId?: string;
  timestamp?: Date;
  groups?: ProviderGroup[];               // only dispatch to providers in these groups
}
```

### `analytics.updateConsent(consent: ConsentSettings): Promise<void>`

Updates Google Consent Mode v2 state at runtime. Call this when the user interacts with a cookie/consent banner. Only affects Google Analytics in browser mode; all other providers ignore it.

```typescript
interface ConsentSettings {
  ad_storage?: 'granted' | 'denied';
  analytics_storage?: 'granted' | 'denied';
  ad_user_data?: 'granted' | 'denied';
  ad_personalization?: 'granted' | 'denied';
  functionality_storage?: 'granted' | 'denied';
  personalization_storage?: 'granted' | 'denied';
  security_storage?: 'granted' | 'denied';
}
```

### `analytics.addProvider(config: ProviderConfig): void`

Dynamically add a provider at runtime.

### `analytics.removeProvider(name: string): void`

Remove a provider by name.

### `analytics.getProviders(): string[]`

Returns the list of active provider names.

### `analytics.getDeadLetterQueue(): DeadLetterEvent[]`

Returns a shallow copy of the dead-letter queue. Each entry contains:

```typescript
interface DeadLetterEvent {
  id: string;          // unique ID
  provider: string;    // provider name
  method: 'track' | 'identify' | 'page';
  args: unknown;       // original method arguments
  error: string;       // error message
  timestamp: number;   // epoch ms
  attempts: number;    // total attempts (including retries)
}
```

### `analytics.replayDeadLetterQueue(): Promise<ReplayResult>`

Replays all entries in the DLQ. Successfully replayed events are removed; failures remain in the queue.

```typescript
interface ReplayResult {
  replayed: number;            // count of successfully replayed events
  failed: DeadLetterEvent[];   // entries that failed again
}
```

### `analytics.clearDeadLetterQueue(): void`

Removes all entries from the dead-letter queue.

### `TrackResult`

Every method returns `Promise<TrackResult[]>` — one result per provider:

```typescript
interface TrackResult {
  provider: string;
  success: boolean;
  statusCode?: number;
  error?: MytartError;
  attempts?: number;       // total HTTP attempts (including retries)
  retryable?: boolean;     // true if the failure can be retried
  duration?: number;       // wall-clock time in ms
  debugInfo?: DebugInfo;   // populated when debug mode is active
}

interface MytartError {
  message: string;
  code: string;
  provider: string;
  originalError?: unknown;
}

interface DebugInfo {
  requestUrl?: string;
  requestMethod?: string;
  requestPayload?: unknown;
  requestHeaders?: Record<string, string>;
  responseBody?: unknown;
  responseHeaders?: Record<string, string>;
  responseStatus?: number;
  duration?: number;
  validationErrors?: string[];
}
```

## TypeScript

All types are exported:

```typescript
import type {
  MytartConfig, MytartState, BaseProviderConfig, ProviderConfig, ProviderGroup,
  TrackOptions, IdentifyOptions, PageOptions,
  TrackResult, MytartError, EventContext, ProviderName, StandardEventName, StandardEventProperties,
  EventTaxonomyConfig, TypedTrackOptions, CrossProviderLinkingProperties,
  RetryConfig, DebugInfo, DeadLetterEvent, ReplayResult,
  GoogleAnalyticsAppType, GoogleAnalyticsConfig, GoogleAdsConfig, GoogleAdsAppType, GoogleAdsUserIdentifier,
  ConsentSettings, ConsentState, MixpanelConfig, SegmentConfig, AmplitudeConfig, PlausibleConfig,
  PostHogConfig, MetaPixelConfig, MetaPixelAppType, MetaPixelAdvancedMatching, ClarityConfig,
  HotjarConfig, HeapConfig, HeapAppType,
  TikTokConfig, TikTokAppType, TikTokEventName, TikTokEventMapping,
  SnapchatConfig, SnapchatAppType, SnapchatEventName, SnapchatEventMapping,
  TwitterConfig, TwitterAppType, TwitterEventName, TwitterEventMapping,
  RedditConfig, RedditAppType, RedditEventName, RedditEventMapping,
  PinterestConfig, PinterestAppType, PinterestEventName, PinterestEventMapping,
  MicrosoftAdsConfig, MicrosoftAdsAppType, MicrosoftAdsEventName, MicrosoftAdsEventMapping,
} from 'mytart';

import type { MytartLike } from 'mytart'; // interface for custom providers
```

### Typed track() with Standard Events

When using a standard event name, you get typed properties with IDE intellisense:

```typescript
// Typed — properties are inferred as PurchaseEventProperties
await analytics.track({
  event: 'purchase',
  properties: {
    value: 99.99,
    currency: 'USD',
    transaction_id: 'order-123',
    items: [{ item_id: 'SKU-001', price: 99.99, quantity: 1 }],
  },
});

// Also typed — begin_checkout properties
await analytics.track({
  event: 'begin_checkout',
  properties: {
    value: 149.99,
    currency: 'USD',
    coupon: 'SAVE20',
    items: [{ item_id: 'SKU-002', price: 149.99 }],
  },
});

// Betting events are also typed
await analytics.track({
  event: 'bet_placed',
  properties: {
    stake_amount: 25.00,
    odds: 2.5,
    odds_format: 'decimal',
    market_type: 'match_winner',
    sport: 'football',
    bet_type: 'single',
    is_live: true,
    bet_id: 'bet-xyz-789',
  },
});

// Custom events still work with generic properties
await analytics.track({
  event: 'my_custom_event',
  properties: { any: 'thing', goes: true },
});
```

### Typed Cross-Provider Linking Properties

When `crossProviderLinking` is enabled, `xpl_`-prefixed properties are available with intellisense:

```typescript
await analytics.track({
  event: 'purchase',
  properties: {
    value: 99.99,
    // These are typed when crossProviderLinking is enabled:
    xpl_gclid: 'CLICK_ID',      // Google click ID
    xpl_fbclid: 'FB_CLICK_ID',  // Meta click ID
    xpl_ttclid: 'TT_CLICK_ID',  // TikTok click ID
    xpl_msclkid: 'MS_CLICK_ID', // Microsoft Ads click ID
    xpl_fbp: '_fbp cookie',      // Meta browser ID
    xpl_fbc: '_fbc cookie',      // Meta click ID cookie
    xpl_ga_client_id: 'GA_ID',  // GA client ID
  },
});
```

The same typed properties are available on `IdentifyOptions.traits` and `PageOptions.properties`.

## Custom Providers

Extend `BaseProvider` to create your own:

```typescript
import { BaseProvider, MytartLike, TrackOptions, IdentifyOptions, PageOptions, TrackResult } from 'mytart';

export class MyProvider extends BaseProvider {
  readonly name = 'my-provider';

  constructor(config: MyProviderConfig, mytart: MytartLike) {
    super(mytart);
    // Store config, set up HTTP client, etc.
  }

  async track(options: TrackOptions): Promise<TrackResult> {
    // Access central state via this.mytart.getState()
    const state = this.mytart.getState();
    // your HTTP call here
    return this.buildSuccess(200);
  }

  async identify(options: IdentifyOptions): Promise<TrackResult> {
    return this.buildSuccess(200);
  }

  async page(options: PageOptions): Promise<TrackResult> {
    return this.buildSuccess(200);
  }
}
```

## Framework Integration

### SvelteKit (Svelte 5) — Real-World Example

```typescript
// src/lib/analytics.ts
import { Mytart, type Mytart as MytartType } from 'mytart';

let analytics: MytartType | null = null;

export function initAnalytics(sessionId: string): void {
	if (analytics) return;

	analytics = new Mytart({
		defaultSessionId: sessionId,
		ignoreBots: true,
		crossProviderLinking: true,
		// browserFingerprint is on by default — generates a stable device ID as anonymousId
		// across sessions, cache clears, and incognito mode via ThumbmarkJS
		providers: [
			{
				provider: 'google-analytics',
				measurementId: 'G-123456789',
				appType: 'browser',
				enabled: true,
				signals: true,
				defaultConsent: {
					ad_personalization: 'granted',
					analytics_storage: 'granted',
					security_storage: 'granted',
					ad_user_data: 'granted',
					ad_storage: 'granted',
					functionality_storage: 'granted',
					personalization_storage: 'granted'
				}
			},
			{
				provider: 'clarity',
				projectId: '9876543543',
				enabled: true,
				cookie: true
			},
			{
				provider: 'meta-pixel',
				pixelId: '123456789',
				appType: 'browser',
				enabled: true,
			}
		]
	});
}

export function getSessionId(): string | undefined {
	return analytics?.getState().sessionId;
}

export function getCapturedIds() {
	return analytics?.getCapturedIds();
}

export function trackPage(url: string, title?: string) {
	analytics?.page({ url, name: title });
}

export function trackEvent(event: string, properties?: Record<string, string | number | boolean>) {
	analytics?.track({ event, properties });
	vercelTrack(event, properties);  // also send to Vercel Analytics
}
```

```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
	import { page } from '$app/state';
	import { onMount } from 'svelte';
	import { initAnalytics, trackPage, trackEvent } from '$lib/analytics';

	onMount(() => {
		// Initialize with a session ID (generate or retrieve from cookie/storage)
		const sessionId = crypto.randomUUID();
		initAnalytics(sessionId);

		// Track initial page view
		trackPage(window.location.href, document.title);

		// Example: track a custom event
		trackEvent('button_clicked', { button: 'cta' });
	});
</script>

<slot />
```

### Page View Tracking

mytart does **not** send automatic page views — `send_page_view: false` is set in the GA `gtag('config')` call, and other providers have no auto-pageview behaviour. This means your framework is responsible for calling `analytics.page()` on navigation.

### SvelteKit (Svelte 5)

Create a shared analytics instance and use `$effect` in your root layout to reactively track page URL changes:

```typescript
// src/lib/analytics.ts
import { Mytart } from 'mytart';

export const analytics = new Mytart({
  // browserFingerprint enabled by default — stable anonymousId across sessions
  providers: [
    { provider: 'google-analytics', measurementId: 'G-XXXXXXXXXX', appType: 'browser', enabled: true },
    { provider: 'meta-pixel', pixelId: '123456789', appType: 'browser', enabled: true },
  ],
});
```

```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { page } from '$app/state';
  import { analytics } from '$lib/analytics';

  $effect(() => {
    // Runs on initial load and every client-side navigation
    analytics.page({ url: page.url.href });
  });
</script>

<slot />
```

> **Why `$effect`?** Svelte 5's `$effect` reactively tracks `page.url.href`. Whenever SvelteKit performs a client-side navigation and the URL changes, the effect re-runs and sends a single page view. This avoids the double-fire problem that can occur with `onMount` + `afterNavigate`, and works correctly on initial load.

## Internal Analytics

Starting from version **0.7.1**, mytart may include internal analytics to help us gather anonymous usage metrics. No personally identifiable information (PII) is collected. Some versions will have internal analytics enabled by default, and some will not — this varies at our discretion.

You can permanently disable internal analytics by setting `internalMetrics: false` in your config:

```typescript
const analytics = new Mytart({
  internalMetrics: false,
  providers: [
    // ...
  ],
});
```

## Author

Created by [Ashley Jackson](https://github.com/ashleyjackson). Currently working as a Systems Engineer at [MyAffiliates.com](https://www.myaffiliates.com).

## License

MIT
