<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://swetrix.com/assets/logo_white.png?v=2">
  <img alt="Swetrix" src="https://swetrix.com/assets/logo_blue.png?v=2" height="80">
</picture>
<br /><br />

[![NPM](https://img.shields.io/npm/v/swetrix)](https://www.npmjs.com/package/swetrix)
[![Package size](https://img.shields.io/bundlephobia/minzip/swetrix)](https://bundlephobia.com/package/swetrix)
[![JSDelivr hits](https://data.jsdelivr.com/v1/package/npm/swetrix/badge?style=rounded)](https://www.jsdelivr.com/package/npm/swetrix)
[![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/swetrix/swetrix-js/issues)

# Swetrix Tracking Script

Privacy-first, lightweight analytics tracking library for [Swetrix](https://swetrix.com). Tracks page views, custom events, errors, feature flags, and A/B experiments — all without cookies or invading user privacy.

## Installation

### npm / yarn / pnpm

```bash
npm install swetrix
```

### CDN

```html
<script src="https://swetrix.org/swetrix.js" defer></script>
```

## Quick Start

### ES Modules

```javascript
import { init, trackViews, trackErrors } from 'swetrix'

init('YOUR_PROJECT_ID')
trackViews()
trackErrors()
```

### CDN / Script Tag

```html
<script src="https://swetrix.org/swetrix.js" defer></script>
<script>
  document.addEventListener('DOMContentLoaded', () => {
    swetrix.init('YOUR_PROJECT_ID')
    swetrix.trackViews()
    swetrix.trackErrors()
  })
</script>
```

## API

### `init(projectId, options?)`

Initialise the library. Must be called before any other method.

```javascript
init('YOUR_PROJECT_ID', {
  apiURL: 'https://api.swetrix.com/log',
  devMode: false,
  disabled: false,
  respectDNT: false,
  profileId: 'user-123',
  preloadSessionReplay: false,
})
```

| Option                 | Description                                                                                             | Default                         |
| ---------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `apiURL`               | API endpoint. Change this if you're self-hosting.                                                       | `'https://api.swetrix.com/log'` |
| `devMode`              | When `true`, localhost events are sent to the server.                                                   | `false`                         |
| `disabled`             | When `true`, no data is sent. Useful for dev environments.                                              | `false`                         |
| `respectDNT`           | When `true`, disables tracking for users with Do Not Track enabled.                                     | `false`                         |
| `profileId`            | Profile ID for long-term user tracking (MAU/DAU).                                                       | `undefined`                     |
| `preloadSessionReplay` | Preload the session replay recorder after `init()`. Recording only starts after `startSessionReplay()`. | `undefined`                     |

### `trackViews(options?)`

Automatically tracks page views, including navigation changes in SPAs. Returns a `Promise<{ stop() }>`.

```javascript
const { stop } = await trackViews({
  hash: ['pricing', 'features'],
  search: ['query', 'page'],
  unique: false,
  heartbeatOnBackground: false,
  callback: undefined,
})

// Stop tracking when needed
stop()
```

| Option                  | Description                                                                                                                                                                          | Default     |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| `hash`                  | Set to `true` to track every hash (e.g. `/#/path`), or provide an array of exact hash values to include (e.g. `['pricing', 'features']`). A leading `#` is optional in array values. | `false`     |
| `search`                | Set to `true` to track every query parameter (e.g. `/path?query=value`), or provide an array of parameter names to include (e.g. `['query', 'page']`).                               | `false`     |
| `unique`                | Only count unique page views per session.                                                                                                                                            | `false`     |
| `heartbeatOnBackground` | Send heartbeat when the tab is not active.                                                                                                                                           | `false`     |
| `callback`              | A function to edit or prevent pageview payloads. Return `false` to block, `true` to send as-is, or return a modified payload object.                                                 | `undefined` |

### `track(event)`

Track custom events (e.g. button clicks, sign-ups).

```javascript
track({
  ev: 'signup',
  unique: true,
  meta: { plan: 'pro', source: 'landing' },
  profileId: 'user-123',
})
```

| Option      | Description                                                           | Default      |
| ----------- | --------------------------------------------------------------------- | ------------ |
| `ev`        | Event name (max 256 chars).                                           | **required** |
| `unique`    | Only count once per session.                                          | `false`      |
| `meta`      | Key-value metadata (max 20 keys, 1000 chars total).                   | `{}`         |
| `profileId` | Optional profile ID. Overrides the global `profileId` for this event. | `undefined`  |

### `trackErrors(options?)`

Automatically captures client-side errors. Returns `{ stop() }`.

```javascript
const { stop } = trackErrors({
  sampleRate: 1,
  callback: undefined,
})
```

| Option       | Description                                              | Default     |
| ------------ | -------------------------------------------------------- | ----------- |
| `sampleRate` | Fraction of errors to send (`0` to `1`).                 | `1`         |
| `callback`   | Edit or prevent error payloads. Return `false` to block. | `undefined` |

### `trackError(payload)`

Manually report an error.

```javascript
trackError({
  name: 'PaymentError',
  message: 'Card declined',
  meta: { gateway: 'stripe' },
})
```

### `pageview(options)`

Manually track a single page view (useful for custom routing).

```javascript
pageview({
  payload: { pg: '/checkout', lc: 'en-US' },
  unique: true,
})
```

### Feature Flags

```javascript
// Get all flags. Results are cached for 5 minutes.
const flags = await getFeatureFlags({ profileId: 'user-123' })

// Force a fresh fetch
const freshFlags = await getFeatureFlags({ profileId: 'user-123' }, true)

// Get a single flag. The third argument is an optional fallback value.
const enabled = await getFeatureFlag('dark-mode', { profileId: 'user-123' })
const enabledWithFallback = await getFeatureFlag('dark-mode', { profileId: 'user-123' }, false)

// Clear the shared feature flag / experiment cache
clearFeatureFlagsCache()
```

### A/B Experiments

```javascript
// Get all running experiment assignments. Results are cached for 5 minutes.
const experiments = await getExperiments({ profileId: 'user-123' })

// Force a fresh fetch
const freshExperiments = await getExperiments({ profileId: 'user-123' }, true)

// Get a specific experiment variant. The third argument is an optional fallback variant.
const variant = await getExperiment('checkout-redesign-experiment-id', { profileId: 'user-123' })
const variantWithFallback = await getExperiment('checkout-redesign-experiment-id', { profileId: 'user-123' }, 'control')

// Clear the shared feature flag / experiment cache
clearExperimentsCache()
```

### `startSessionReplay(options?)`

Start recording a session replay. Session replays use `total` privacy by default, which masks text and inputs and blocks media/canvas capture unless you explicitly choose another mode.

If you use the npm package, the recorder (`@rrweb/record`) is dynamically imported from your installed dependencies only when the recorder is preloaded or started. If you use the CDN/script-tag build, the standalone replay recorder is loaded with an async script tag.

```javascript
const replay = await startSessionReplay({
  privacy: 'total',
  maskAllText: true,
  sampleRate: 0.25,
  maxDurationMs: 10 * 60 * 1000,
  idleTimeoutMs: 2 * 60 * 1000,
  maxBytesPerChunk: 512 * 1024,
})

// Stop or flush manually when needed
await replay.flush()
await replay.stop()
```

| Option              | Description                                                                                                                                  | Default       |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `privacy`           | Privacy mode: `total`, `normal`, or `none`.                                                                                                  | `'total'`     |
| `maskAllText`       | Mask all non-input text with asterisks. Defaults to `true` when `privacy` is `total`, otherwise `false`.                                     | privacy-based |
| `sampleRate`        | Fraction of sessions to record (`0` to `1`).                                                                                                 | `1`           |
| `maxDurationMs`     | Stop recording after this duration.                                                                                                          | `undefined`   |
| `idleTimeoutMs`     | Stop recording after this much visitor inactivity.                                                                                           | `undefined`   |
| `flushIntervalMs`   | Upload buffered replay events at this interval.                                                                                              | `5000`        |
| `maxEventsPerChunk` | Upload once this many events are buffered.                                                                                                   | `100`         |
| `maxBytesPerChunk`  | Upload once buffered replay events reach this approximate byte size.                                                                         | `524288`      |
| `maxBytesPerEvent`  | Drop a single replay event if it is larger than this many bytes.                                                                             | `5242880`     |
| `recordIframes`     | Allow iframe elements to be captured. Iframes are blocked by default to reduce replay size and avoid recording embedded third-party content. | `false`       |
| `rrweb`             | Additional rrweb record options.                                                                                                             | `undefined`   |

To mask text while keeping media less restricted than `total` privacy, combine `normal` privacy with `maskAllText`:

```javascript
await startSessionReplay({
  privacy: 'normal',
  maskAllText: true,
})
```

By default, Swetrix blocks iframe elements from replay snapshots. If you own the iframe content and need it in the replay, opt in explicitly:

```javascript
await startSessionReplay({
  privacy: 'normal',
  recordIframes: true,
})
```

Cross-origin iframe recording also requires rrweb's cross-origin iframe support and should only be enabled for domains you control:

```javascript
await startSessionReplay({
  recordIframes: true,
  rrweb: {
    recordCrossOriginIframes: true,
  },
})
```

### `identify(profileId, traits?)` / `setTraits(traits)` / `reset()`

Identify the current visitor with your own user ID (e.g. after they log in). Their current anonymous profile gets linked to the identified profile server-side, so pre-login activity is attributed to it, and all subsequent events are tracked under the identified profile.

```javascript
// After the user logs in (or on page load if they're already logged in)
identify('user-12345')

// On logout
reset()
```

Use a unique, stable identifier (e.g. an internal user ID). The ID is stored as provided and shown in your dashboard, so don't pass values you wouldn't want to see there. Swetrix stores nothing in the browser, so call `identify()` on every page load while the user is logged in.

Optionally pass **traits** — key / value metadata displayed on the user's profile:

```javascript
identify('user-12345', { email: 'john@example.com', plan: 'premium' })

// Later, without repeating the user ID. null removes a trait
setTraits({ plan: 'enterprise', trialEndsAt: null })
```

Traits are merged per key. Per call: max 50 keys, 128 characters per key, and 2000 characters for all keys and values combined.

### Session & Profile IDs

```javascript
const profileId = await getProfileId()
const sessionId = await getSessionId()
```

These are useful for revenue attribution with payment providers like Paddle.

`getProfileId()` returns the ID the visitor's events are actually stored under — `usr_`-prefixed once they're identified (via `identify()` or a `profileId` on `init()`), `anon_`-prefixed otherwise. Attribute revenue with this value, not the raw ID you passed in.

## Self-Hosting

If you're running a self-hosted [Swetrix API](https://github.com/Swetrix/swetrix-api) instance, point the `apiURL` to your server:

```javascript
init('YOUR_PROJECT_ID', {
  apiURL: 'https://your-api.example.com/log',
})
```

## Documentation

Full reference and guides are available at [docs.swetrix.com](https://swetrix.com/docs).

## Contributing

Contributions are welcome — feel free to [open an issue](https://github.com/swetrix/swetrix-js/issues) or submit a pull request.

## License

MIT
