@indiebob/tracker
v0.2.0Privacy-first analytics SDK. Under 5KB gzipped. No cookies. GDPR, CCPA, and GPC compliant.
Installation
npm install @indiebob/tracker
# or
pnpm add @indiebob/trackerOr via script tag (no build step required):
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js"></script>Quick Start
Three lines to start tracking page views and UTM attribution:
import { IndieBob } from '@indiebob/tracker'
const bob = IndieBob.init({ projectId: 'proj_your16charId' })
bob.page() Get your projectId from your IndieBob dashboard → Project Settings → SDK Key.
Configuration
const bob = IndieBob.init({
projectId: 'proj_your16charId', // Required
endpoint: 'https://indiebob.com/api/collect', // Optional. Default shown.
debug: false, // Optional. Log events to console.
respectDnt: true, // Optional. Honor Do Not Track.
respectGpc: true, // Optional. Honor Global Privacy Control.
autoDetect: true, // Optional. Auto-detect signups, logins, etc.
})| 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 (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. |
API Reference
bob.page(overrides?)
Track a page view. Call this on every page load and SPA navigation.
bob.page() // Auto-detects path, title, referrer
bob.page({ path: '/pricing' }) // Override path
bob.page({ title: 'Pricing Page' }) // Override titleAuto-detected: URL path, page title, referrer, UTM parameters, device type.
bob.track(event, properties?)
Track a custom event.
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. Usesnake_caseby 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.
bob.identify('user_123', {
email: '[email protected]',
name: 'Jane Smith',
plan: 'pro',
createdAt: '2026-01-15',
})userId— string, required. Your internal user ID. Never pass PII directly as the userId.traits— plain object, optional. Includeemailto enable Stripe 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.
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
bob.optOut() // Stop all tracking. Clears all stored identifiers.
bob.optIn() // Re-enable tracking.
bob.isOptedOut() // Returns boolean.Debug helpers
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 immediatelyAuto-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)
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 pagelogin_submit— form submitted on a login pagecheckout_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:
<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:
const bob = IndieBob.init({
projectId: 'proj_abc123',
autoDetect: {
// Custom URL patterns — strings or RegExp (override defaults)
signupPaths: ['/my-signup-page'], // string: matched via path.includes()
checkoutPaths: [/\/buy|\/purchase/i], // RegExp: matched via .test()
// Disable specific features
outboundClicks: false,
formSubmit: false,
dataAttributes: false,
},
}) Set autoDetect: false to disable all auto-detection entirely.
| Option | Type | Default | Description |
|---|---|---|---|
| signupPaths | (string | RegExp)[] | built-in i18n | URL patterns for signup pages |
| loginPaths | (string | RegExp)[] | built-in i18n | URL patterns for login pages |
| pricingPaths | (string | RegExp)[] | built-in i18n | URL patterns for pricing pages |
| checkoutPaths | (string | RegExp)[] | built-in i18n | URL patterns for checkout/cart pages |
| successPaths | (string | RegExp)[] | built-in i18n | URL patterns for success/thank-you pages |
| onboardingPaths | (string | RegExp)[] | built-in i18n | URL patterns for onboarding pages |
| contactPaths | (string | 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
import { IndieBobPlugin } from '@indiebob/tracker/vue'
app.use(IndieBobPlugin, { projectId: 'proj_abc123def456ghij' })Then in any component:
import { useTracker } from '@indiebob/tracker/vue'
const bob = useTracker()
bob.track('button_clicked', { button: 'cta_hero' })Nuxt plugin (plugins/tracker.client.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 / Next.js
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 — call bob.page() on every navigation
useEffect(() => {
bob.page()
}, [pathname])— Vanilla JS (Script Tag)
<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)
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 as _ib_anon. This persists across sessions so you can track a visitor's journey before they sign up. Falls back to sessionStorage in private browsing. Never uses cookies.
Session ID
Generated per browser tab via crypto.randomUUID(). Sessions expire after 30 minutes of inactivity or when the tab closes.
UTM Attribution
UTM parameters are captured from the URL automatically on init().
- _ib_first_utm — first-touch attribution, never overwritten
- _ib_last_utm — last-touch attribution, updated each time UTMs are present
This enables the core attribution question: "which campaign drove this paying customer?"
Event Batching
- 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 & Compliance
GDPR (EU)
No cookies, no personal data by default. Anonymous analytics may not require a consent banner — but consult your legal team.
CCPA (California)
Global Privacy Control (GPC) is automatically honored. If a user has GPC enabled, the SDK sends no events — the legally correct behavior under CCPA/CPRA.
Cookie Law (EU)
No cookies are set. localStorage identifiers are not cookies and fall outside the scope of the EU ePrivacy Directive.
Self-Hosting
Point the SDK at your own IndieBob instance:
const bob = IndieBob.init({
projectId: 'proj_abc123def456ghij',
endpoint: 'https://your-indiebob.example.com/api/collect',
})Bundle Size
| Bundle | Size (gzip) |
|---|---|
| Browser IIFE (script tag) | < 5KB |
| ESM (tree-shakeable) | < 4KB |
| Server (Node.js) | No limit |
Zero runtime dependencies.