npm package: @indiebob/tracker
Description: Privacy-first analytics SDK for web applications. <5KB gzipped. No cookies.
Author: IndieBob
Latest version: 0.2.0

# @indiebob/tracker

Privacy-first analytics SDK for web applications. Under 5KB gzipped. No cookies. GDPR/CCPA/GPC compliant.

## Installation

```bash
npm install @indiebob/tracker
# or
pnpm add @indiebob/tracker
```

Or via script tag (CDN):

```html
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js"></script>
```

## Quick Start

Three lines to start tracking:

```js
import { IndieBob } from '@indiebob/tracker'

const bob = IndieBob.init({ projectId: 'proj_your16charId' })
bob.page()
```

That's it. Page views are now tracked. UTM parameters are auto-captured.

## Configuration

```ts
const bob = IndieBob.init({
  projectId: 'proj_your16charId',  // Required. From your IndieBob dashboard.
  endpoint: 'https://indiebob.com/api/collect',  // Optional. Default shown.
  debug: false,           // Optional. Logs events to console when true.
  respectDnt: true,       // Optional. Honor browser Do Not Track. Default: true.
  respectGpc: true,       // Optional. Honor Global Privacy Control. Default: true.
  autoDetect: true,       // Optional. Auto-detect signups, logins, checkouts, etc. Default: true.
})
```

| Option | Type | Default | Description |
|---|---|---|---|
| `projectId` | `string` | **required** | Your SDK project ID from the dashboard |
| `endpoint` | `string` | `'https://indiebob.com/api/collect'` | Override for self-hosted instances |
| `debug` | `boolean` | `false` | Log all events to browser console |
| `respectDnt` | `boolean` | `true` | Honor the browser's Do Not Track header |
| `respectGpc` | `boolean` | `true` | Honor Global Privacy Control signal (legally binding in CA/GDPR) |
| `autoDetect` | `boolean \| object` | `true` | Auto-detect user intent from URL patterns, form submissions, outbound clicks, and data attributes. See [Auto-Detection](#auto-detection). |

## API Reference

### `IndieBob.init(config)`

Creates and returns a tracker instance. Call once at app startup.

```ts
const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
```

---

### `bob.page(overrides?)`

Track a page view. Call this on every page load and SPA navigation.

```ts
bob.page()                           // Auto-detects path, title, referrer
bob.page({ path: '/pricing' })       // Override path
bob.page({ title: 'Pricing Page' }) // Override title
```

**Auto-detected:** URL path, page title, referrer, UTM parameters, device type.

---

### `bob.track(event, properties?)`

Track a custom event.

```ts
bob.track('signup_started')
bob.track('trial_activated', { plan: 'pro' })
bob.track('feature_used', { feature: 'csv_export', count: 3 })
bob.track('checkout_completed', { revenue: 29.00, currency: 'USD', plan: 'pro-monthly' })
```

**`event`** — string, required. Max 256 characters. Use `snake_case` by convention.

**`properties`** — plain object, optional. Values must be JSON-serializable.

---

### `bob.identify(userId, traits?)`

Associate the current anonymous visitor with a known user ID. Call this after login or signup.

```ts
bob.identify('user_123', {
  email: 'jane@example.com',
  name: 'Jane Smith',
  plan: 'pro',
  createdAt: '2026-01-15',
})
```

**`userId`** — string, required. Your internal user ID. Never pass PII as the userId directly.

**`traits`** — plain object, optional. Can include `email` for attribution linking.

After `identify()`, all subsequent events are linked to this userId until the session ends.

---

### `bob.revenue(data)`

Track a revenue event. Use alongside Stripe webhooks for complete attribution.

```ts
bob.revenue({
  amount: 29.00,           // Required. Positive number.
  currency: 'USD',         // Optional. Default: 'USD'. ISO 4217 code.
  plan: 'pro-monthly',     // Optional. Your plan name.
  type: 'new',             // Optional. 'new' | 'renewal' | 'upgrade' | 'downgrade'
})
```

---

### Privacy Controls

```ts
bob.optOut()       // Stop all tracking. Clears all stored identifiers.
bob.optIn()        // Re-enable tracking.
bob.isOptedOut()   // Returns boolean.
```

---

### Debug / Testing

```ts
bob.getAnonymousId()  // Returns the current anonymous ID
bob.getSessionId()    // Returns the current session ID
bob.getConfig()       // Returns a read-only copy of the config
bob.flush()           // Force-send all queued events immediately
```

---

## Auto-Detection

The SDK automatically fires semantic events out of the box — no manual `bob.track()` calls needed for common user journeys. This powers IndieBob's attribution funnels (visit → signup → checkout → paid).

### URL Intent Detection

When `bob.page()` is called, the SDK checks the URL path against known patterns (multilingual: EN, ES, FR, DE, PT, IT, NL) and fires the corresponding event:

| Event | Triggered By |
|---|---|
| `signup_start` | `/signup`, `/register`, `/join`, `/registro`, `/inscription`, `/anmeldung`, ... |
| `login_view` | `/login`, `/sign-in`, `/iniciar-sesion`, `/connexion`, `/anmelden`, ... |
| `pricing_view` | `/pricing`, `/plans`, `/precios`, `/tarifs`, `/preise`, ... |
| `checkout_view` | `/checkout`, `/cart`, `/basket`, `/panier`, `/warenkorb`, `/kasse`, ... |
| `success_view` | `/success`, `/thank-you`, `/confirmation`, `/danke`, `/merci`, ... |
| `onboarding_view` | `/onboarding`, `/welcome`, `/getting-started`, `/bienvenido`, ... |
| `contact_view` | `/contact`, `/contacto`, `/kontakt`, `/nous-contacter`, ... |

### Signup vs Login Detection

When `bob.identify()` is called:
- **First call** for this visitor fires `signup` (new user)
- **Subsequent calls** fire `login` (returning user)

This uses a localStorage flag — no server round-trip needed.

### Form Submit Detection

On signup, login, and checkout pages, form submissions are automatically tracked:
- `signup_submit` — form submitted on a signup page
- `login_submit` — form submitted on a login page
- `checkout_submit` — form submitted on a checkout page

### Outbound Click Tracking

Clicks on links pointing to external domains automatically fire `outbound_click` with `{ url, hostname }` properties.

### Data Attribute Tracking

Add `data-ib-event` to any element for zero-JS custom event tracking:

```html
<button data-ib-event="add_to_cart" data-ib-plan="pro" data-ib-price="29">
  Add to Cart
</button>
```

This fires `track('add_to_cart', { plan: 'pro', price: '29' })` on click. All `data-ib-*` attributes (except `data-ib-event`) become event properties.

### Customizing Auto-Detection

Override the default URL patterns or disable specific features:

```ts
const bob = IndieBob.init({
  projectId: 'proj_abc123',
  autoDetect: {
    // Custom URL patterns (override defaults)
    signupPaths: [/\/my-signup-page/i],
    checkoutPaths: [/\/buy|\/purchase/i],

    // Disable specific features
    outboundClicks: false,
    formSubmit: false,
    dataAttributes: false,
  },
})
```

Set `autoDetect: false` to disable all auto-detection entirely.

| Option | Type | Default | Description |
|---|---|---|---|
| `signupPaths` | `RegExp[]` | built-in i18n | URL patterns for signup pages |
| `loginPaths` | `RegExp[]` | built-in i18n | URL patterns for login pages |
| `pricingPaths` | `RegExp[]` | built-in i18n | URL patterns for pricing pages |
| `checkoutPaths` | `RegExp[]` | built-in i18n | URL patterns for checkout/cart pages |
| `successPaths` | `RegExp[]` | built-in i18n | URL patterns for success/thank-you pages |
| `onboardingPaths` | `RegExp[]` | built-in i18n | URL patterns for onboarding pages |
| `contactPaths` | `RegExp[]` | built-in i18n | URL patterns for contact pages |
| `dataAttributes` | `boolean` | `true` | Track clicks on `data-ib-event` elements |
| `formSubmit` | `boolean` | `true` | Track form submissions on intent pages |
| `outboundClicks` | `boolean` | `true` | Track clicks on external links |

---

## Framework Integrations

### Vue / Nuxt

```ts
// main.ts or plugins/tracker.client.ts
import { IndieBobPlugin } from '@indiebob/tracker/vue'

app.use(IndieBobPlugin, { projectId: 'proj_abc123def456ghij' })
```

Then in any component:

```ts
import { useTracker } from '@indiebob/tracker/vue'

const bob = useTracker()
bob.track('button_clicked', { button: 'cta_hero' })
```

**Nuxt plugin** (`plugins/tracker.client.ts`):

```ts
import { IndieBobPlugin } from '@indiebob/tracker/vue'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(IndieBobPlugin, {
    projectId: useRuntimeConfig().public.trackerProjectId,
  })
})
```

Page views auto-track on vue-router navigation.

### React

```tsx
import { IndieBob } from '@indiebob/tracker'
import { useEffect } from 'react'

// Initialize once at app root
const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })

// In a page component or layout
useEffect(() => {
  bob.page()
}, [pathname])
```

### Vanilla JS (Script Tag)

```html
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js"></script>
<script>
  const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
  bob.page()
</script>
```

### Server-Side (Node.js)

```ts
import { IndieBob } from '@indiebob/tracker/server'

const bob = IndieBob.initServer({ projectId: 'proj_abc123def456ghij' })
bob.track('server_event', { source: 'cron_job' })
```

---

## How It Works

### Anonymous ID

A random 21-character ID is generated on first visit and stored in `localStorage`. This persists across sessions so you can track a visitor's journey before they sign up.

- Stored as `_ib_anon` in localStorage
- Falls back to sessionStorage if localStorage is blocked
- Never uses cookies — no consent banner required
- Cleared when `optOut()` is called

### Session ID

A session ID is generated per browser tab/window using `crypto.randomUUID()`. Sessions expire after 30 minutes of inactivity or when the tab closes.

### UTM Attribution

UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`) are captured from the URL automatically on `init()`.

- **First-touch** attribution stored in `_ib_first_utm` — never overwritten
- **Last-touch** attribution in `_ib_last_utm` — updated each time UTMs are present in the URL
- Enables "which campaign drove this paying customer?" — the core attribution question

### Event Batching

Events are batched for efficiency:
- Sent when 10 events accumulate
- Sent every 5 seconds regardless
- Sent immediately when the page becomes hidden (tab switch, close)
- Queued in localStorage when offline; flushed when back online

### Privacy by Default

The SDK respects:
- **Do Not Track** (`navigator.doNotTrack === '1'`): no events sent, `respectDnt: true` by default
- **Global Privacy Control** (`navigator.globalPrivacyControl`): legally binding under CCPA in California, `respectGpc: true` by default
- Neither cookies nor fingerprinting is used at any point
- IP addresses are never stored by the server — only the country code derived at ingestion time

---

## What Is and Isn't Collected

### Automatically collected (on `page()`)

| Data | What | Stored As |
|---|---|---|
| Page path | `/pricing`, `/blog/post-1` | String |
| Page title | `document.title` | String |
| Referrer | `document.referrer` | String |
| UTM params | From URL query string | Object |
| Screen size | `window.innerWidth/Height` | Numbers |
| Device type | Derived from screen size | `'desktop'` \| `'mobile'` \| `'tablet'` |
| Browser locale | `navigator.language` | String |
| Timezone | `Intl.DateTimeFormat().resolvedOptions().timeZone` | String |
| Country | Derived server-side from IP | String (e.g. `'US'`) |

### Auto-detected events (when `autoDetect` is enabled)

| Event | Trigger |
|---|---|
| `signup_start` | User visits a signup/register page |
| `signup_submit` | User submits a form on a signup page |
| `signup` | First `identify()` call (new user) |
| `login_view` | User visits a login page |
| `login_submit` | User submits a form on a login page |
| `login` | Subsequent `identify()` call (returning user) |
| `pricing_view` | User visits a pricing page |
| `checkout_view` | User visits a checkout/cart page |
| `checkout_submit` | User submits a form on a checkout page |
| `success_view` | User visits a success/thank-you page |
| `onboarding_view` | User visits an onboarding page |
| `contact_view` | User visits a contact page |
| `outbound_click` | User clicks an external link |
| Custom via `data-ib-event` | User clicks an element with the attribute |

### NOT collected

- IP addresses (never stored, used only for country lookup then discarded)
- Browser fingerprint
- Cookies
- Personal data (name, email, etc.) unless you pass it to `identify()`
- Cross-site tracking data

---

## Privacy & Compliance

### GDPR (EU)

The SDK does not use cookies and does not collect personal data by default. The `anonymousId` is a random identifier with no link to personal identity. Under GDPR, anonymous analytics may not require a consent banner — but consult your legal team.

When a user calls `optOut()`, all stored keys are cleared: `_ib_anon`, `_ib_session`, `_ib_first_utm`, `_ib_last_utm`, `_ib_referrer`, `_ib_queue`.

### CCPA (California)

Global Privacy Control (GPC) is automatically honored. If a user has GPC enabled in their browser, the SDK sends no events. This is the legally correct behavior under CCPA as amended by CPRA.

### Cookie Law (EU ePrivacy Directive)

No cookies are set. The localStorage identifiers are not cookies and fall outside the scope of the Cookie Law.

---

## Self-Hosting

Point the SDK at your own IndieBob instance:

```ts
const bob = IndieBob.init({
  projectId: 'proj_abc123def456ghij',
  endpoint: 'https://your-indiebob.example.com/api/collect',
})
```

---

## Versioning & Backwards Compatibility

This SDK follows [Semantic Versioning](https://semver.org/).

**1.x guarantee**: All public methods listed in this README are stable. We will never:
- Remove a method in a 1.x release
- Change the behavior of an existing method without a deprecation period
- Break the wire format for events that include `v: 1`

**Upgrade path**: `npm update @indiebob/tracker` — no code changes required within 1.x.

**Deprecation process**: Methods marked `@deprecated` in JSDoc will log a warning in `debug: true` mode. They remain functional until the next major version.

---

## Bundle Size

| Bundle | Size (gzip) |
|---|---|
| Browser IIFE (`tracker.js`) | < 5KB |
| ESM tree-shakeable | < 4KB |
| Server (Node.js) | No limit |

Zero runtime dependencies.

---

## License

MIT