@indiebob/tracker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@indiebob/tracker` will be documented here.
4
+
5
+ This project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ---
8
+
9
+ ## [1.0.0] — 2026-02-24
10
+
11
+ ### Added
12
+ - `IndieBob.init()` factory — creates a tracker instance with config validation
13
+ - `bob.page()` — track page views with auto-detection of URL, title, referrer
14
+ - `bob.track()` — custom event tracking with arbitrary properties
15
+ - `bob.identify()` — link anonymous visitors to known user IDs
16
+ - `bob.revenue()` — track revenue events for attribution
17
+ - `bob.optOut()` / `bob.optIn()` / `bob.isOptedOut()` — privacy controls
18
+ - `bob.flush()` — force-send all queued events
19
+ - `bob.getAnonymousId()`, `bob.getSessionId()`, `bob.getConfig()` — introspection
20
+ - Event batching: auto-flush at 10 events or 5-second interval
21
+ - `visibilitychange` flush for reliable mobile delivery
22
+ - Offline queue via localStorage with auto-flush on reconnect
23
+ - UTM first-touch and last-touch attribution
24
+ - DNT (Do Not Track) and GPC (Global Privacy Control) support
25
+ - `@indiebob/tracker/vue` — Vue plugin + `useTracker()` composable
26
+ - `@indiebob/tracker/server` — Node.js server-side tracking
27
+ - Browser IIFE bundle: < 5KB gzipped, zero runtime dependencies
28
+ - Event schema v1 with `v: 1` version field for backwards compatibility
package/README.md ADDED
@@ -0,0 +1,349 @@
1
+ # @indiebob/tracker
2
+
3
+ Privacy-first analytics SDK for web applications. Under 5KB gzipped. No cookies. GDPR/CCPA/GPC compliant.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @indiebob/tracker
9
+ # or
10
+ pnpm add @indiebob/tracker
11
+ ```
12
+
13
+ Or via script tag (CDN):
14
+
15
+ ```html
16
+ <script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js"></script>
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ Three lines to start tracking:
22
+
23
+ ```js
24
+ import { IndieBob } from '@indiebob/tracker'
25
+
26
+ const bob = IndieBob.init({ projectId: 'proj_your16charId' })
27
+ bob.page()
28
+ ```
29
+
30
+ That's it. Page views are now tracked. UTM parameters are auto-captured.
31
+
32
+ ## Configuration
33
+
34
+ ```ts
35
+ const bob = IndieBob.init({
36
+ projectId: 'proj_your16charId', // Required. From your IndieBob dashboard.
37
+ endpoint: 'https://indiebob.com/api/collect', // Optional. Default shown.
38
+ debug: false, // Optional. Logs events to console when true.
39
+ respectDnt: true, // Optional. Honor browser Do Not Track. Default: true.
40
+ respectGpc: true, // Optional. Honor Global Privacy Control. Default: true.
41
+ })
42
+ ```
43
+
44
+ | Option | Type | Default | Description |
45
+ |---|---|---|---|
46
+ | `projectId` | `string` | **required** | Your SDK project ID from the dashboard |
47
+ | `endpoint` | `string` | `'https://indiebob.com/api/collect'` | Override for self-hosted instances |
48
+ | `debug` | `boolean` | `false` | Log all events to browser console |
49
+ | `respectDnt` | `boolean` | `true` | Honor the browser's Do Not Track header |
50
+ | `respectGpc` | `boolean` | `true` | Honor Global Privacy Control signal (legally binding in CA/GDPR) |
51
+
52
+ ## API Reference
53
+
54
+ ### `IndieBob.init(config)`
55
+
56
+ Creates and returns a tracker instance. Call once at app startup.
57
+
58
+ ```ts
59
+ const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
60
+ ```
61
+
62
+ ---
63
+
64
+ ### `bob.page(overrides?)`
65
+
66
+ Track a page view. Call this on every page load and SPA navigation.
67
+
68
+ ```ts
69
+ bob.page() // Auto-detects path, title, referrer
70
+ bob.page({ path: '/pricing' }) // Override path
71
+ bob.page({ title: 'Pricing Page' }) // Override title
72
+ ```
73
+
74
+ **Auto-detected:** URL path, page title, referrer, UTM parameters, device type.
75
+
76
+ ---
77
+
78
+ ### `bob.track(event, properties?)`
79
+
80
+ Track a custom event.
81
+
82
+ ```ts
83
+ bob.track('signup_started')
84
+ bob.track('trial_activated', { plan: 'pro' })
85
+ bob.track('feature_used', { feature: 'csv_export', count: 3 })
86
+ bob.track('checkout_completed', { revenue: 29.00, currency: 'USD', plan: 'pro-monthly' })
87
+ ```
88
+
89
+ **`event`** — string, required. Max 256 characters. Use `snake_case` by convention.
90
+
91
+ **`properties`** — plain object, optional. Values must be JSON-serializable.
92
+
93
+ ---
94
+
95
+ ### `bob.identify(userId, traits?)`
96
+
97
+ Associate the current anonymous visitor with a known user ID. Call this after login or signup.
98
+
99
+ ```ts
100
+ bob.identify('user_123', {
101
+ email: 'jane@example.com',
102
+ name: 'Jane Smith',
103
+ plan: 'pro',
104
+ createdAt: '2026-01-15',
105
+ })
106
+ ```
107
+
108
+ **`userId`** — string, required. Your internal user ID. Never pass PII as the userId directly.
109
+
110
+ **`traits`** — plain object, optional. Can include `email` for attribution linking.
111
+
112
+ After `identify()`, all subsequent events are linked to this userId until the session ends.
113
+
114
+ ---
115
+
116
+ ### `bob.revenue(data)`
117
+
118
+ Track a revenue event. Use alongside Stripe webhooks for complete attribution.
119
+
120
+ ```ts
121
+ bob.revenue({
122
+ amount: 29.00, // Required. Positive number.
123
+ currency: 'USD', // Optional. Default: 'USD'. ISO 4217 code.
124
+ plan: 'pro-monthly', // Optional. Your plan name.
125
+ type: 'new', // Optional. 'new' | 'renewal' | 'upgrade' | 'downgrade'
126
+ })
127
+ ```
128
+
129
+ ---
130
+
131
+ ### Privacy Controls
132
+
133
+ ```ts
134
+ bob.optOut() // Stop all tracking. Clears all stored identifiers.
135
+ bob.optIn() // Re-enable tracking.
136
+ bob.isOptedOut() // Returns boolean.
137
+ ```
138
+
139
+ ---
140
+
141
+ ### Debug / Testing
142
+
143
+ ```ts
144
+ bob.getAnonymousId() // Returns the current anonymous ID
145
+ bob.getSessionId() // Returns the current session ID
146
+ bob.getConfig() // Returns a read-only copy of the config
147
+ bob.flush() // Force-send all queued events immediately
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Framework Integrations
153
+
154
+ ### Vue / Nuxt
155
+
156
+ ```ts
157
+ // main.ts or plugins/tracker.client.ts
158
+ import { IndieBobPlugin } from '@indiebob/tracker/vue'
159
+
160
+ app.use(IndieBobPlugin, { projectId: 'proj_abc123def456ghij' })
161
+ ```
162
+
163
+ Then in any component:
164
+
165
+ ```ts
166
+ import { useTracker } from '@indiebob/tracker/vue'
167
+
168
+ const bob = useTracker()
169
+ bob.track('button_clicked', { button: 'cta_hero' })
170
+ ```
171
+
172
+ **Nuxt plugin** (`plugins/tracker.client.ts`):
173
+
174
+ ```ts
175
+ import { IndieBobPlugin } from '@indiebob/tracker/vue'
176
+
177
+ export default defineNuxtPlugin((nuxtApp) => {
178
+ nuxtApp.vueApp.use(IndieBobPlugin, {
179
+ projectId: useRuntimeConfig().public.trackerProjectId,
180
+ })
181
+ })
182
+ ```
183
+
184
+ Page views auto-track on vue-router navigation.
185
+
186
+ ### React
187
+
188
+ ```tsx
189
+ import { IndieBob } from '@indiebob/tracker'
190
+ import { useEffect } from 'react'
191
+
192
+ // Initialize once at app root
193
+ const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
194
+
195
+ // In a page component or layout
196
+ useEffect(() => {
197
+ bob.page()
198
+ }, [pathname])
199
+ ```
200
+
201
+ ### Vanilla JS (Script Tag)
202
+
203
+ ```html
204
+ <script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js"></script>
205
+ <script>
206
+ const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
207
+ bob.page()
208
+ </script>
209
+ ```
210
+
211
+ ### Server-Side (Node.js)
212
+
213
+ ```ts
214
+ import { IndieBob } from '@indiebob/tracker/server'
215
+
216
+ const bob = IndieBob.initServer({ projectId: 'proj_abc123def456ghij' })
217
+ bob.track('server_event', { source: 'cron_job' })
218
+ ```
219
+
220
+ ---
221
+
222
+ ## How It Works
223
+
224
+ ### Anonymous ID
225
+
226
+ 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.
227
+
228
+ - Stored as `_ib_anon` in localStorage
229
+ - Falls back to sessionStorage if localStorage is blocked
230
+ - Never uses cookies — no consent banner required
231
+ - Cleared when `optOut()` is called
232
+
233
+ ### Session ID
234
+
235
+ A session ID is generated per browser tab/window using `crypto.randomUUID()`. Sessions expire after 30 minutes of inactivity or when the tab closes.
236
+
237
+ ### UTM Attribution
238
+
239
+ UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`) are captured from the URL automatically on `init()`.
240
+
241
+ - **First-touch** attribution stored in `_ib_first_utm` — never overwritten
242
+ - **Last-touch** attribution in `_ib_last_utm` — updated each time UTMs are present in the URL
243
+ - Enables "which campaign drove this paying customer?" — the core attribution question
244
+
245
+ ### Event Batching
246
+
247
+ Events are batched for efficiency:
248
+ - Sent when 10 events accumulate
249
+ - Sent every 5 seconds regardless
250
+ - Sent immediately when the page becomes hidden (tab switch, close)
251
+ - Queued in localStorage when offline; flushed when back online
252
+
253
+ ### Privacy by Default
254
+
255
+ The SDK respects:
256
+ - **Do Not Track** (`navigator.doNotTrack === '1'`): no events sent, `respectDnt: true` by default
257
+ - **Global Privacy Control** (`navigator.globalPrivacyControl`): legally binding under CCPA in California, `respectGpc: true` by default
258
+ - Neither cookies nor fingerprinting is used at any point
259
+ - IP addresses are never stored by the server — only the country code derived at ingestion time
260
+
261
+ ---
262
+
263
+ ## What Is and Isn't Collected
264
+
265
+ ### Automatically collected (on `page()`)
266
+
267
+ | Data | What | Stored As |
268
+ |---|---|---|
269
+ | Page path | `/pricing`, `/blog/post-1` | String |
270
+ | Page title | `document.title` | String |
271
+ | Referrer | `document.referrer` | String |
272
+ | UTM params | From URL query string | Object |
273
+ | Screen size | `window.innerWidth/Height` | Numbers |
274
+ | Device type | Derived from screen size | `'desktop'` \| `'mobile'` \| `'tablet'` |
275
+ | Browser locale | `navigator.language` | String |
276
+ | Timezone | `Intl.DateTimeFormat().resolvedOptions().timeZone` | String |
277
+ | Country | Derived server-side from IP | String (e.g. `'US'`) |
278
+
279
+ ### NOT collected
280
+
281
+ - IP addresses (never stored, used only for country lookup then discarded)
282
+ - Browser fingerprint
283
+ - Cookies
284
+ - Personal data (name, email, etc.) unless you pass it to `identify()`
285
+ - Cross-site tracking data
286
+
287
+ ---
288
+
289
+ ## Privacy & Compliance
290
+
291
+ ### GDPR (EU)
292
+
293
+ 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.
294
+
295
+ When a user calls `optOut()`, all stored keys are cleared: `_ib_anon`, `_ib_session`, `_ib_first_utm`, `_ib_last_utm`, `_ib_referrer`, `_ib_queue`.
296
+
297
+ ### CCPA (California)
298
+
299
+ 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.
300
+
301
+ ### Cookie Law (EU ePrivacy Directive)
302
+
303
+ No cookies are set. The localStorage identifiers are not cookies and fall outside the scope of the Cookie Law.
304
+
305
+ ---
306
+
307
+ ## Self-Hosting
308
+
309
+ Point the SDK at your own IndieBob instance:
310
+
311
+ ```ts
312
+ const bob = IndieBob.init({
313
+ projectId: 'proj_abc123def456ghij',
314
+ endpoint: 'https://your-indiebob.example.com/api/collect',
315
+ })
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Versioning & Backwards Compatibility
321
+
322
+ This SDK follows [Semantic Versioning](https://semver.org/).
323
+
324
+ **1.x guarantee**: All public methods listed in this README are stable. We will never:
325
+ - Remove a method in a 1.x release
326
+ - Change the behavior of an existing method without a deprecation period
327
+ - Break the wire format for events that include `v: 1`
328
+
329
+ **Upgrade path**: `npm update @indiebob/tracker` — no code changes required within 1.x.
330
+
331
+ **Deprecation process**: Methods marked `@deprecated` in JSDoc will log a warning in `debug: true` mode. They remain functional until the next major version.
332
+
333
+ ---
334
+
335
+ ## Bundle Size
336
+
337
+ | Bundle | Size (gzip) |
338
+ |---|---|
339
+ | Browser IIFE (`tracker.js`) | < 5KB |
340
+ | ESM tree-shakeable | < 4KB |
341
+ | Server (Node.js) | No limit |
342
+
343
+ Zero runtime dependencies.
344
+
345
+ ---
346
+
347
+ ## License
348
+
349
+ MIT
@@ -0,0 +1,2 @@
1
+ var J=Object.defineProperty,z=Object.defineProperties;var G=Object.getOwnPropertyDescriptors;var k=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var A=(e,t,o)=>t in e?J(e,t,{enumerable:true,configurable:true,writable:true,value:o}):e[t]=o,d=(e,t)=>{for(var o in t||(t={}))W.call(t,o)&&A(e,o,t[o]);if(k)for(var o of k(t))Z.call(t,o)&&A(e,o,t[o]);return e},x=(e,t)=>z(e,G(t));var a=(e,t,o)=>new Promise((n,i)=>{var r=s=>{try{l(o.next(s));}catch(c){i(c);}},g=s=>{try{l(o.throw(s));}catch(c){i(c);}},l=s=>s.done?n(s.value):Promise.resolve(s.value).then(r,g);l((o=o.apply(e,t)).next());});var $="https://indiebob.com/api/collect";function D(e){var t,o,n,i;if(!e.projectId||!e.projectId.startsWith("proj_"))throw new Error(`[IndieBob] Invalid projectId "${e.projectId}". Must start with "proj_".`);return {projectId:e.projectId,endpoint:(t=e.endpoint)!=null?t:$,debug:(o=e.debug)!=null?o:false,respectDnt:(n=e.respectDnt)!=null?n:true,respectGpc:(i=e.respectGpc)!=null?i:true}}var u="_ib_anon";function h(){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-",t="";if(typeof crypto!="undefined"&&crypto.getRandomValues){let o=new Uint8Array(21);crypto.getRandomValues(o);for(let n of o)t+=e[n%e.length];}else for(let o=0;o<21;o++)t+=e[Math.floor(Math.random()*e.length)];return t}function v(){try{let e=localStorage.getItem(u);return e||(e=h(),localStorage.setItem(u,e)),e}catch(e){try{let t=sessionStorage.getItem(u);return t||(t=h(),sessionStorage.setItem(u,t)),t}catch(t){return h()}}}function P(){try{localStorage.removeItem(u);}catch(e){}try{sessionStorage.removeItem(u);}catch(e){}}var y="_ib_session",I="_ib_session_last";function N(){try{return crypto.randomUUID()}catch(e){return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}}function S(){var e;try{let t=Date.now(),o=parseInt((e=sessionStorage.getItem(I))!=null?e:"0",10),n=sessionStorage.getItem(y),i=o>0&&t-o>18e5;return (!n||i)&&(n=N(),sessionStorage.setItem(y,n)),sessionStorage.setItem(I,String(t)),n}catch(t){return N()}}function U(){try{sessionStorage.removeItem(y);}catch(e){}try{sessionStorage.removeItem(I);}catch(e){}}var b="_ib_first_utm",M="_ib_last_utm",_="_ib_referrer";function q(e){let t=new URLSearchParams(e),o={},n=false;return t.has("utm_source")&&(o.source=t.get("utm_source"),n=true),t.has("utm_medium")&&(o.medium=t.get("utm_medium"),n=true),t.has("utm_campaign")&&(o.campaign=t.get("utm_campaign"),n=true),t.has("utm_term")&&(o.term=t.get("utm_term"),n=true),t.has("utm_content")&&(o.content=t.get("utm_content"),n=true),n?o:null}function B(e,t){let o=q(e);try{o&&!localStorage.getItem(b)&&localStorage.setItem(b,JSON.stringify(o)),o&&localStorage.setItem(M,JSON.stringify(o)),t&&!localStorage.getItem(_)&&localStorage.setItem(_,t);}catch(n){}}function F(e){var t;return (t=q(e))!=null?t:void 0}function L(){try{localStorage.removeItem(b);}catch(e){}try{localStorage.removeItem(M);}catch(e){}try{localStorage.removeItem(_);}catch(e){}}var p="_ib_optout",tt="_ib_queue";function f(e){try{if(localStorage.getItem(p))return !1}catch(t){}return !(e.respectDnt&&typeof navigator!="undefined"&&(navigator.doNotTrack==="1"||navigator.msDoNotTrack==="1")||e.respectGpc&&typeof navigator!="undefined"&&navigator.globalPrivacyControl===true)}function j(){try{localStorage.setItem(p,"1");}catch(e){}P(),U(),L();try{localStorage.removeItem(tt);}catch(e){}}function Y(){try{localStorage.removeItem(p);}catch(e){}}function K(){try{return !!localStorage.getItem(p)}catch(e){return false}}var et=[1e3,2e3,4e3];function ot(e){return a(this,null,function*(){return new Promise(t=>setTimeout(t,e))})}function w(e,t,o=false){return a(this,null,function*(){let n=JSON.stringify(e);if(o&&typeof navigator!="undefined"&&navigator.sendBeacon){let i=new Blob([n],{type:"application/json"}),r=navigator.sendBeacon(t.endpoint,i);return t.debug&&console.log("[IndieBob] sendBeacon:",r,e.batch.length,"events"),r}for(let i=0;i<3;i++){try{let r=yield fetch(t.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:n,keepalive:!0});if(r.ok)return t.debug&&console.log("[IndieBob] sent:",e.batch.length,"events"),!0;if(r.status>=400&&r.status<500)return t.debug&&console.warn("[IndieBob] 4xx error, not retrying:",r.status),!1}catch(r){t.debug&&console.warn("[IndieBob] fetch error (attempt",i+1,"):",r);}i<2&&(yield ot(et[i]));}return t.debug&&console.warn("[IndieBob] failed to send after",3,"attempts"),false})}var E="_ib_queue",nt=10,it=5e3,rt=50,m=class{constructor(t){this.queue=[];this.timer=null;this.flushing=false;this.config=t,this.startTimer(),this.setupVisibilityFlush(),this.setupOnlineFlush();}push(t){this.queue.push(t),this.queue.length>=nt&&this.flush();}flush(t=false){return a(this,null,function*(){if(this.flushing&&!t||this.queue.length===0)return;this.flushing=true;let o=this.queue.splice(0);(yield w({batch:o},this.config,t))||this.pushToOfflineQueue(o),this.flushing=false;})}flushOfflineQueue(){return a(this,null,function*(){let t=this.getOfflineQueue();if(t.length===0)return;this.clearOfflineQueue(),yield w({batch:t},this.config);})}destroy(){this.timer&&clearInterval(this.timer);}startTimer(){this.timer=setInterval(()=>{this.queue.length>0&&this.flush();},it);}setupVisibilityFlush(){typeof document!="undefined"&&document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush(true);},{passive:true});}setupOnlineFlush(){typeof window!="undefined"&&(window.addEventListener("online",()=>{this.flushOfflineQueue();}),navigator.onLine&&this.flushOfflineQueue());}pushToOfflineQueue(t){try{let i=[...this.getOfflineQueue(),...t].slice(-rt);localStorage.setItem(E,JSON.stringify(i));}catch(o){}}getOfflineQueue(){try{let t=localStorage.getItem(E);return t?JSON.parse(t):[]}catch(t){return []}}clearOfflineQueue(){try{localStorage.removeItem(E);}catch(t){}}};function Q(){if(typeof window=="undefined")return "desktop";let e=window.innerWidth;return e<=768?"mobile":e<=1024?"tablet":"desktop"}function V(){return typeof window=="undefined"?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}function T(e){var c,O,R,C;let t=typeof window=="undefined",o=(c=e==null?void 0:e.path)!=null?c:t?"":window.location.pathname+window.location.search,n=t?"":window.location.href,i=(O=e==null?void 0:e.title)!=null?O:t?"":document.title,r=(R=e==null?void 0:e.referrer)!=null?R:t?"":document.referrer,g=V(),l=Q(),s=t?void 0:F(window.location.search);return {page:{path:o,url:n,title:i,referrer:r},device:x(d({},g),{type:l}),utm:s,locale:t?"en":navigator.language,timezone:t?"UTC":(C=Intl.DateTimeFormat().resolvedOptions().timeZone)!=null?C:"UTC",sdkVersion:"0.1.0"}}function H(e){if(typeof window=="undefined")return ()=>{};let t=history.pushState.bind(history),o=history.replaceState.bind(history);return history.pushState=function(...n){t(...n),e();},history.replaceState=function(...n){o(...n);},window.addEventListener("popstate",e),()=>{history.pushState=t,history.replaceState=o,window.removeEventListener("popstate",e);}}var X=class e{constructor(t){this.cleanupHistory=null;this.config=t,this.queue=new m(t),typeof window!="undefined"&&B(window.location.search,document.referrer);}static init(t){let o=D(t);return new e(o)}page(t){f(this.config)&&this.queue.push(this.buildEvent("page",{context:T(t)}));}track(t,o){f(this.config)&&this.queue.push(this.buildEvent("track",{event:t,properties:o}));}identify(t,o){f(this.config)&&(this.userId=t,this.queue.push(this.buildEvent("identify",{traits:o,userId:t})));}revenue(t){var o;f(this.config)&&this.queue.push(this.buildEvent("revenue",{revenue:{amount:t.amount,currency:(o=t.currency)!=null?o:"USD",plan:t.plan,type:t.type}}));}optOut(){this.queue.flush(),j(),this.userId=void 0,this.config.debug&&console.log("[IndieBob] opted out");}optIn(){Y(),this.config.debug&&console.log("[IndieBob] opted in");}isOptedOut(){return K()}flush(){return a(this,null,function*(){yield this.queue.flush();})}getAnonymousId(){return v()}getSessionId(){return S()}getConfig(){return Object.freeze(d({},this.config))}enableAutoPage(){var t;return (t=this.cleanupHistory)==null||t.call(this),this.cleanupHistory=H(()=>this.page()),()=>{var o;(o=this.cleanupHistory)==null||o.call(this),this.cleanupHistory=null;}}buildEvent(t,o){var n,i;return {v:1,projectId:this.config.projectId,type:t,anonymousId:v(),userId:(n=this.userId)!=null?n:o.userId,sessionId:S(),timestamp:new Date().toISOString(),context:(i=o.context)!=null?i:T(),event:o.event,properties:o.properties,traits:o.traits,revenue:o.revenue}}};
2
+ export{X as a};
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var J=Object.defineProperty,z=Object.defineProperties;var G=Object.getOwnPropertyDescriptors;var k=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var A=(e,t,o)=>t in e?J(e,t,{enumerable:true,configurable:true,writable:true,value:o}):e[t]=o,d=(e,t)=>{for(var o in t||(t={}))W.call(t,o)&&A(e,o,t[o]);if(k)for(var o of k(t))Z.call(t,o)&&A(e,o,t[o]);return e},x=(e,t)=>z(e,G(t));var a=(e,t,o)=>new Promise((n,i)=>{var r=s=>{try{l(o.next(s));}catch(c){i(c);}},g=s=>{try{l(o.throw(s));}catch(c){i(c);}},l=s=>s.done?n(s.value):Promise.resolve(s.value).then(r,g);l((o=o.apply(e,t)).next());});var $="https://indiebob.com/api/collect";function D(e){var t,o,n,i;if(!e.projectId||!e.projectId.startsWith("proj_"))throw new Error(`[IndieBob] Invalid projectId "${e.projectId}". Must start with "proj_".`);return {projectId:e.projectId,endpoint:(t=e.endpoint)!=null?t:$,debug:(o=e.debug)!=null?o:false,respectDnt:(n=e.respectDnt)!=null?n:true,respectGpc:(i=e.respectGpc)!=null?i:true}}var u="_ib_anon";function h(){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-",t="";if(typeof crypto!="undefined"&&crypto.getRandomValues){let o=new Uint8Array(21);crypto.getRandomValues(o);for(let n of o)t+=e[n%e.length];}else for(let o=0;o<21;o++)t+=e[Math.floor(Math.random()*e.length)];return t}function v(){try{let e=localStorage.getItem(u);return e||(e=h(),localStorage.setItem(u,e)),e}catch(e){try{let t=sessionStorage.getItem(u);return t||(t=h(),sessionStorage.setItem(u,t)),t}catch(t){return h()}}}function P(){try{localStorage.removeItem(u);}catch(e){}try{sessionStorage.removeItem(u);}catch(e){}}var y="_ib_session",I="_ib_session_last";function N(){try{return crypto.randomUUID()}catch(e){return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}}function S(){var e;try{let t=Date.now(),o=parseInt((e=sessionStorage.getItem(I))!=null?e:"0",10),n=sessionStorage.getItem(y),i=o>0&&t-o>18e5;return (!n||i)&&(n=N(),sessionStorage.setItem(y,n)),sessionStorage.setItem(I,String(t)),n}catch(t){return N()}}function U(){try{sessionStorage.removeItem(y);}catch(e){}try{sessionStorage.removeItem(I);}catch(e){}}var b="_ib_first_utm",M="_ib_last_utm",_="_ib_referrer";function q(e){let t=new URLSearchParams(e),o={},n=false;return t.has("utm_source")&&(o.source=t.get("utm_source"),n=true),t.has("utm_medium")&&(o.medium=t.get("utm_medium"),n=true),t.has("utm_campaign")&&(o.campaign=t.get("utm_campaign"),n=true),t.has("utm_term")&&(o.term=t.get("utm_term"),n=true),t.has("utm_content")&&(o.content=t.get("utm_content"),n=true),n?o:null}function B(e,t){let o=q(e);try{o&&!localStorage.getItem(b)&&localStorage.setItem(b,JSON.stringify(o)),o&&localStorage.setItem(M,JSON.stringify(o)),t&&!localStorage.getItem(_)&&localStorage.setItem(_,t);}catch(n){}}function F(e){var t;return (t=q(e))!=null?t:void 0}function L(){try{localStorage.removeItem(b);}catch(e){}try{localStorage.removeItem(M);}catch(e){}try{localStorage.removeItem(_);}catch(e){}}var p="_ib_optout",tt="_ib_queue";function f(e){try{if(localStorage.getItem(p))return !1}catch(t){}return !(e.respectDnt&&typeof navigator!="undefined"&&(navigator.doNotTrack==="1"||navigator.msDoNotTrack==="1")||e.respectGpc&&typeof navigator!="undefined"&&navigator.globalPrivacyControl===true)}function j(){try{localStorage.setItem(p,"1");}catch(e){}P(),U(),L();try{localStorage.removeItem(tt);}catch(e){}}function Y(){try{localStorage.removeItem(p);}catch(e){}}function K(){try{return !!localStorage.getItem(p)}catch(e){return false}}var et=[1e3,2e3,4e3];function ot(e){return a(this,null,function*(){return new Promise(t=>setTimeout(t,e))})}function w(e,t,o=false){return a(this,null,function*(){let n=JSON.stringify(e);if(o&&typeof navigator!="undefined"&&navigator.sendBeacon){let i=new Blob([n],{type:"application/json"}),r=navigator.sendBeacon(t.endpoint,i);return t.debug&&console.log("[IndieBob] sendBeacon:",r,e.batch.length,"events"),r}for(let i=0;i<3;i++){try{let r=yield fetch(t.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:n,keepalive:!0});if(r.ok)return t.debug&&console.log("[IndieBob] sent:",e.batch.length,"events"),!0;if(r.status>=400&&r.status<500)return t.debug&&console.warn("[IndieBob] 4xx error, not retrying:",r.status),!1}catch(r){t.debug&&console.warn("[IndieBob] fetch error (attempt",i+1,"):",r);}i<2&&(yield ot(et[i]));}return t.debug&&console.warn("[IndieBob] failed to send after",3,"attempts"),false})}var E="_ib_queue",nt=10,it=5e3,rt=50,m=class{constructor(t){this.queue=[];this.timer=null;this.flushing=false;this.config=t,this.startTimer(),this.setupVisibilityFlush(),this.setupOnlineFlush();}push(t){this.queue.push(t),this.queue.length>=nt&&this.flush();}flush(t=false){return a(this,null,function*(){if(this.flushing&&!t||this.queue.length===0)return;this.flushing=true;let o=this.queue.splice(0);(yield w({batch:o},this.config,t))||this.pushToOfflineQueue(o),this.flushing=false;})}flushOfflineQueue(){return a(this,null,function*(){let t=this.getOfflineQueue();if(t.length===0)return;this.clearOfflineQueue(),yield w({batch:t},this.config);})}destroy(){this.timer&&clearInterval(this.timer);}startTimer(){this.timer=setInterval(()=>{this.queue.length>0&&this.flush();},it);}setupVisibilityFlush(){typeof document!="undefined"&&document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush(true);},{passive:true});}setupOnlineFlush(){typeof window!="undefined"&&(window.addEventListener("online",()=>{this.flushOfflineQueue();}),navigator.onLine&&this.flushOfflineQueue());}pushToOfflineQueue(t){try{let i=[...this.getOfflineQueue(),...t].slice(-rt);localStorage.setItem(E,JSON.stringify(i));}catch(o){}}getOfflineQueue(){try{let t=localStorage.getItem(E);return t?JSON.parse(t):[]}catch(t){return []}}clearOfflineQueue(){try{localStorage.removeItem(E);}catch(t){}}};function Q(){if(typeof window=="undefined")return "desktop";let e=window.innerWidth;return e<=768?"mobile":e<=1024?"tablet":"desktop"}function V(){return typeof window=="undefined"?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}function T(e){var c,O,R,C;let t=typeof window=="undefined",o=(c=e==null?void 0:e.path)!=null?c:t?"":window.location.pathname+window.location.search,n=t?"":window.location.href,i=(O=e==null?void 0:e.title)!=null?O:t?"":document.title,r=(R=e==null?void 0:e.referrer)!=null?R:t?"":document.referrer,g=V(),l=Q(),s=t?void 0:F(window.location.search);return {page:{path:o,url:n,title:i,referrer:r},device:x(d({},g),{type:l}),utm:s,locale:t?"en":navigator.language,timezone:t?"UTC":(C=Intl.DateTimeFormat().resolvedOptions().timeZone)!=null?C:"UTC",sdkVersion:"0.1.0"}}function H(e){if(typeof window=="undefined")return ()=>{};let t=history.pushState.bind(history),o=history.replaceState.bind(history);return history.pushState=function(...n){t(...n),e();},history.replaceState=function(...n){o(...n);},window.addEventListener("popstate",e),()=>{history.pushState=t,history.replaceState=o,window.removeEventListener("popstate",e);}}var X=class e{constructor(t){this.cleanupHistory=null;this.config=t,this.queue=new m(t),typeof window!="undefined"&&B(window.location.search,document.referrer);}static init(t){let o=D(t);return new e(o)}page(t){f(this.config)&&this.queue.push(this.buildEvent("page",{context:T(t)}));}track(t,o){f(this.config)&&this.queue.push(this.buildEvent("track",{event:t,properties:o}));}identify(t,o){f(this.config)&&(this.userId=t,this.queue.push(this.buildEvent("identify",{traits:o,userId:t})));}revenue(t){var o;f(this.config)&&this.queue.push(this.buildEvent("revenue",{revenue:{amount:t.amount,currency:(o=t.currency)!=null?o:"USD",plan:t.plan,type:t.type}}));}optOut(){this.queue.flush(),j(),this.userId=void 0,this.config.debug&&console.log("[IndieBob] opted out");}optIn(){Y(),this.config.debug&&console.log("[IndieBob] opted in");}isOptedOut(){return K()}flush(){return a(this,null,function*(){yield this.queue.flush();})}getAnonymousId(){return v()}getSessionId(){return S()}getConfig(){return Object.freeze(d({},this.config))}enableAutoPage(){var t;return (t=this.cleanupHistory)==null||t.call(this),this.cleanupHistory=H(()=>this.page()),()=>{var o;(o=this.cleanupHistory)==null||o.call(this),this.cleanupHistory=null;}}buildEvent(t,o){var n,i;return {v:1,projectId:this.config.projectId,type:t,anonymousId:v(),userId:(n=this.userId)!=null?n:o.userId,sessionId:S(),timestamp:new Date().toISOString(),context:(i=o.context)!=null?i:T(),event:o.event,properties:o.properties,traits:o.traits,revenue:o.revenue}}};
2
+ exports.IndieBob=X;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Configuration for the IndieBob tracker.
3
+ */
4
+ interface TrackerConfig {
5
+ /** Your SDK project ID from the IndieBob dashboard. Format: 'proj_...' */
6
+ projectId: string;
7
+ /** Ingestion endpoint URL. Default: 'https://indiebob.com/api/collect' */
8
+ endpoint?: string;
9
+ /** Log all events to the browser console. Default: false */
10
+ debug?: boolean;
11
+ /** Honor the browser Do Not Track header. Default: true */
12
+ respectDnt?: boolean;
13
+ /** Honor Global Privacy Control signal (legally binding in CA/GDPR). Default: true */
14
+ respectGpc?: boolean;
15
+ }
16
+ /** Resolved config with all defaults applied. */
17
+ interface ResolvedConfig {
18
+ projectId: string;
19
+ endpoint: string;
20
+ debug: boolean;
21
+ respectDnt: boolean;
22
+ respectGpc: boolean;
23
+ }
24
+ /** The tracking event type. */
25
+ type EventType = 'page' | 'track' | 'identify' | 'revenue';
26
+ /** Device category. */
27
+ type DeviceType = 'desktop' | 'mobile' | 'tablet';
28
+ /** Revenue event type. */
29
+ type RevenueType = 'new' | 'renewal' | 'upgrade' | 'downgrade';
30
+ /** UTM attribution data. */
31
+ interface UtmData {
32
+ source?: string;
33
+ medium?: string;
34
+ campaign?: string;
35
+ term?: string;
36
+ content?: string;
37
+ }
38
+ /** Context attached to every event. */
39
+ interface EventContext {
40
+ page: {
41
+ path: string;
42
+ url: string;
43
+ title: string;
44
+ referrer: string;
45
+ };
46
+ device: {
47
+ width: number;
48
+ height: number;
49
+ type: DeviceType;
50
+ };
51
+ utm?: UtmData;
52
+ locale: string;
53
+ timezone: string;
54
+ sdkVersion: string;
55
+ }
56
+ /** A single tracking event in wire format (v1 schema). */
57
+ interface TrackerEvent {
58
+ /** Schema version. Always 1 for this SDK. Enables server-side backwards compat. */
59
+ v: 1;
60
+ projectId: string;
61
+ type: EventType;
62
+ anonymousId: string;
63
+ userId?: string;
64
+ sessionId: string;
65
+ timestamp: string;
66
+ context: EventContext;
67
+ event?: string;
68
+ properties?: Record<string, unknown>;
69
+ traits?: Record<string, unknown>;
70
+ revenue?: {
71
+ amount: number;
72
+ currency: string;
73
+ plan?: string;
74
+ type?: RevenueType;
75
+ };
76
+ }
77
+ /** Overrides for manual page() calls. */
78
+ interface PageOverrides {
79
+ path?: string;
80
+ title?: string;
81
+ referrer?: string;
82
+ }
83
+ /** Revenue data for bob.revenue(). */
84
+ interface RevenueData {
85
+ amount: number;
86
+ currency?: string;
87
+ plan?: string;
88
+ type?: RevenueType;
89
+ }
90
+
91
+ /**
92
+ * IndieBob Analytics Tracker.
93
+ *
94
+ * @example
95
+ * const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
96
+ * bob.page()
97
+ */
98
+ declare class IndieBob {
99
+ private config;
100
+ private queue;
101
+ private userId;
102
+ private cleanupHistory;
103
+ private constructor();
104
+ /**
105
+ * Create a tracker instance. Call once at app startup.
106
+ *
107
+ * @example
108
+ * const bob = IndieBob.init({ projectId: 'proj_your16charId' })
109
+ */
110
+ static init(config: TrackerConfig): IndieBob;
111
+ /**
112
+ * Track a page view. Call on every page load and SPA navigation.
113
+ *
114
+ * @example
115
+ * bob.page()
116
+ * bob.page({ path: '/pricing' })
117
+ */
118
+ page(overrides?: PageOverrides): void;
119
+ /**
120
+ * Track a custom event.
121
+ *
122
+ * @example
123
+ * bob.track('signup_started')
124
+ * bob.track('feature_used', { feature: 'csv_export' })
125
+ */
126
+ track(event: string, properties?: Record<string, unknown>): void;
127
+ /**
128
+ * Associate the current anonymous visitor with a known user ID.
129
+ * Call after login or signup.
130
+ *
131
+ * @example
132
+ * bob.identify('user_123', { email: 'jane@example.com', plan: 'pro' })
133
+ */
134
+ identify(userId: string, traits?: Record<string, unknown>): void;
135
+ /**
136
+ * Track a revenue event.
137
+ *
138
+ * @example
139
+ * bob.revenue({ amount: 29.00, currency: 'USD', plan: 'pro-monthly', type: 'new' })
140
+ */
141
+ revenue(data: RevenueData): void;
142
+ /**
143
+ * Stop all tracking and clear all stored identifiers.
144
+ * Respects user's privacy preferences.
145
+ */
146
+ optOut(): void;
147
+ /** Re-enable tracking after optOut(). */
148
+ optIn(): void;
149
+ /** Returns true if the user has explicitly opted out. */
150
+ isOptedOut(): boolean;
151
+ /** Force-send all queued events immediately. Useful in tests. */
152
+ flush(): Promise<void>;
153
+ /** Get the current anonymous ID. */
154
+ getAnonymousId(): string;
155
+ /** Get the current session ID. */
156
+ getSessionId(): string;
157
+ /** Get the resolved configuration (read-only). */
158
+ getConfig(): Readonly<ResolvedConfig>;
159
+ /**
160
+ * Enable auto-tracking of SPA navigation.
161
+ * Returns a cleanup function to remove the listeners.
162
+ *
163
+ * @example
164
+ * const cleanup = bob.enableAutoPage()
165
+ * // Later: cleanup()
166
+ */
167
+ enableAutoPage(): () => void;
168
+ private buildEvent;
169
+ }
170
+
171
+ export { type DeviceType, type EventContext, type EventType, IndieBob, type PageOverrides, type RevenueData, type RevenueType, type TrackerConfig, type TrackerEvent, type UtmData };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Configuration for the IndieBob tracker.
3
+ */
4
+ interface TrackerConfig {
5
+ /** Your SDK project ID from the IndieBob dashboard. Format: 'proj_...' */
6
+ projectId: string;
7
+ /** Ingestion endpoint URL. Default: 'https://indiebob.com/api/collect' */
8
+ endpoint?: string;
9
+ /** Log all events to the browser console. Default: false */
10
+ debug?: boolean;
11
+ /** Honor the browser Do Not Track header. Default: true */
12
+ respectDnt?: boolean;
13
+ /** Honor Global Privacy Control signal (legally binding in CA/GDPR). Default: true */
14
+ respectGpc?: boolean;
15
+ }
16
+ /** Resolved config with all defaults applied. */
17
+ interface ResolvedConfig {
18
+ projectId: string;
19
+ endpoint: string;
20
+ debug: boolean;
21
+ respectDnt: boolean;
22
+ respectGpc: boolean;
23
+ }
24
+ /** The tracking event type. */
25
+ type EventType = 'page' | 'track' | 'identify' | 'revenue';
26
+ /** Device category. */
27
+ type DeviceType = 'desktop' | 'mobile' | 'tablet';
28
+ /** Revenue event type. */
29
+ type RevenueType = 'new' | 'renewal' | 'upgrade' | 'downgrade';
30
+ /** UTM attribution data. */
31
+ interface UtmData {
32
+ source?: string;
33
+ medium?: string;
34
+ campaign?: string;
35
+ term?: string;
36
+ content?: string;
37
+ }
38
+ /** Context attached to every event. */
39
+ interface EventContext {
40
+ page: {
41
+ path: string;
42
+ url: string;
43
+ title: string;
44
+ referrer: string;
45
+ };
46
+ device: {
47
+ width: number;
48
+ height: number;
49
+ type: DeviceType;
50
+ };
51
+ utm?: UtmData;
52
+ locale: string;
53
+ timezone: string;
54
+ sdkVersion: string;
55
+ }
56
+ /** A single tracking event in wire format (v1 schema). */
57
+ interface TrackerEvent {
58
+ /** Schema version. Always 1 for this SDK. Enables server-side backwards compat. */
59
+ v: 1;
60
+ projectId: string;
61
+ type: EventType;
62
+ anonymousId: string;
63
+ userId?: string;
64
+ sessionId: string;
65
+ timestamp: string;
66
+ context: EventContext;
67
+ event?: string;
68
+ properties?: Record<string, unknown>;
69
+ traits?: Record<string, unknown>;
70
+ revenue?: {
71
+ amount: number;
72
+ currency: string;
73
+ plan?: string;
74
+ type?: RevenueType;
75
+ };
76
+ }
77
+ /** Overrides for manual page() calls. */
78
+ interface PageOverrides {
79
+ path?: string;
80
+ title?: string;
81
+ referrer?: string;
82
+ }
83
+ /** Revenue data for bob.revenue(). */
84
+ interface RevenueData {
85
+ amount: number;
86
+ currency?: string;
87
+ plan?: string;
88
+ type?: RevenueType;
89
+ }
90
+
91
+ /**
92
+ * IndieBob Analytics Tracker.
93
+ *
94
+ * @example
95
+ * const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
96
+ * bob.page()
97
+ */
98
+ declare class IndieBob {
99
+ private config;
100
+ private queue;
101
+ private userId;
102
+ private cleanupHistory;
103
+ private constructor();
104
+ /**
105
+ * Create a tracker instance. Call once at app startup.
106
+ *
107
+ * @example
108
+ * const bob = IndieBob.init({ projectId: 'proj_your16charId' })
109
+ */
110
+ static init(config: TrackerConfig): IndieBob;
111
+ /**
112
+ * Track a page view. Call on every page load and SPA navigation.
113
+ *
114
+ * @example
115
+ * bob.page()
116
+ * bob.page({ path: '/pricing' })
117
+ */
118
+ page(overrides?: PageOverrides): void;
119
+ /**
120
+ * Track a custom event.
121
+ *
122
+ * @example
123
+ * bob.track('signup_started')
124
+ * bob.track('feature_used', { feature: 'csv_export' })
125
+ */
126
+ track(event: string, properties?: Record<string, unknown>): void;
127
+ /**
128
+ * Associate the current anonymous visitor with a known user ID.
129
+ * Call after login or signup.
130
+ *
131
+ * @example
132
+ * bob.identify('user_123', { email: 'jane@example.com', plan: 'pro' })
133
+ */
134
+ identify(userId: string, traits?: Record<string, unknown>): void;
135
+ /**
136
+ * Track a revenue event.
137
+ *
138
+ * @example
139
+ * bob.revenue({ amount: 29.00, currency: 'USD', plan: 'pro-monthly', type: 'new' })
140
+ */
141
+ revenue(data: RevenueData): void;
142
+ /**
143
+ * Stop all tracking and clear all stored identifiers.
144
+ * Respects user's privacy preferences.
145
+ */
146
+ optOut(): void;
147
+ /** Re-enable tracking after optOut(). */
148
+ optIn(): void;
149
+ /** Returns true if the user has explicitly opted out. */
150
+ isOptedOut(): boolean;
151
+ /** Force-send all queued events immediately. Useful in tests. */
152
+ flush(): Promise<void>;
153
+ /** Get the current anonymous ID. */
154
+ getAnonymousId(): string;
155
+ /** Get the current session ID. */
156
+ getSessionId(): string;
157
+ /** Get the resolved configuration (read-only). */
158
+ getConfig(): Readonly<ResolvedConfig>;
159
+ /**
160
+ * Enable auto-tracking of SPA navigation.
161
+ * Returns a cleanup function to remove the listeners.
162
+ *
163
+ * @example
164
+ * const cleanup = bob.enableAutoPage()
165
+ * // Later: cleanup()
166
+ */
167
+ enableAutoPage(): () => void;
168
+ private buildEvent;
169
+ }
170
+
171
+ export { type DeviceType, type EventContext, type EventType, IndieBob, type PageOverrides, type RevenueData, type RevenueType, type TrackerConfig, type TrackerEvent, type UtmData };
@@ -0,0 +1,2 @@
1
+ var IndieBob=(function(exports){'use strict';var J=Object.defineProperty,z=Object.defineProperties;var G=Object.getOwnPropertyDescriptors;var k=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var A=(e,t,o)=>t in e?J(e,t,{enumerable:true,configurable:true,writable:true,value:o}):e[t]=o,d=(e,t)=>{for(var o in t||(t={}))W.call(t,o)&&A(e,o,t[o]);if(k)for(var o of k(t))Z.call(t,o)&&A(e,o,t[o]);return e},x=(e,t)=>z(e,G(t));var a=(e,t,o)=>new Promise((n,i)=>{var r=s=>{try{l(o.next(s));}catch(c){i(c);}},g=s=>{try{l(o.throw(s));}catch(c){i(c);}},l=s=>s.done?n(s.value):Promise.resolve(s.value).then(r,g);l((o=o.apply(e,t)).next());});var $="https://indiebob.com/api/collect";function D(e){var t,o,n,i;if(!e.projectId||!e.projectId.startsWith("proj_"))throw new Error(`[IndieBob] Invalid projectId "${e.projectId}". Must start with "proj_".`);return {projectId:e.projectId,endpoint:(t=e.endpoint)!=null?t:$,debug:(o=e.debug)!=null?o:false,respectDnt:(n=e.respectDnt)!=null?n:true,respectGpc:(i=e.respectGpc)!=null?i:true}}var u="_ib_anon";function h(){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-",t="";if(typeof crypto!="undefined"&&crypto.getRandomValues){let o=new Uint8Array(21);crypto.getRandomValues(o);for(let n of o)t+=e[n%e.length];}else for(let o=0;o<21;o++)t+=e[Math.floor(Math.random()*e.length)];return t}function v(){try{let e=localStorage.getItem(u);return e||(e=h(),localStorage.setItem(u,e)),e}catch(e){try{let t=sessionStorage.getItem(u);return t||(t=h(),sessionStorage.setItem(u,t)),t}catch(t){return h()}}}function P(){try{localStorage.removeItem(u);}catch(e){}try{sessionStorage.removeItem(u);}catch(e){}}var y="_ib_session",I="_ib_session_last";function N(){try{return crypto.randomUUID()}catch(e){return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}}function S(){var e;try{let t=Date.now(),o=parseInt((e=sessionStorage.getItem(I))!=null?e:"0",10),n=sessionStorage.getItem(y),i=o>0&&t-o>18e5;return (!n||i)&&(n=N(),sessionStorage.setItem(y,n)),sessionStorage.setItem(I,String(t)),n}catch(t){return N()}}function U(){try{sessionStorage.removeItem(y);}catch(e){}try{sessionStorage.removeItem(I);}catch(e){}}var b="_ib_first_utm",M="_ib_last_utm",_="_ib_referrer";function q(e){let t=new URLSearchParams(e),o={},n=false;return t.has("utm_source")&&(o.source=t.get("utm_source"),n=true),t.has("utm_medium")&&(o.medium=t.get("utm_medium"),n=true),t.has("utm_campaign")&&(o.campaign=t.get("utm_campaign"),n=true),t.has("utm_term")&&(o.term=t.get("utm_term"),n=true),t.has("utm_content")&&(o.content=t.get("utm_content"),n=true),n?o:null}function B(e,t){let o=q(e);try{o&&!localStorage.getItem(b)&&localStorage.setItem(b,JSON.stringify(o)),o&&localStorage.setItem(M,JSON.stringify(o)),t&&!localStorage.getItem(_)&&localStorage.setItem(_,t);}catch(n){}}function F(e){var t;return (t=q(e))!=null?t:void 0}function L(){try{localStorage.removeItem(b);}catch(e){}try{localStorage.removeItem(M);}catch(e){}try{localStorage.removeItem(_);}catch(e){}}var p="_ib_optout",tt="_ib_queue";function f(e){try{if(localStorage.getItem(p))return !1}catch(t){}return !(e.respectDnt&&typeof navigator!="undefined"&&(navigator.doNotTrack==="1"||navigator.msDoNotTrack==="1")||e.respectGpc&&typeof navigator!="undefined"&&navigator.globalPrivacyControl===true)}function j(){try{localStorage.setItem(p,"1");}catch(e){}P(),U(),L();try{localStorage.removeItem(tt);}catch(e){}}function Y(){try{localStorage.removeItem(p);}catch(e){}}function K(){try{return !!localStorage.getItem(p)}catch(e){return false}}var et=[1e3,2e3,4e3];function ot(e){return a(this,null,function*(){return new Promise(t=>setTimeout(t,e))})}function w(e,t,o=false){return a(this,null,function*(){let n=JSON.stringify(e);if(o&&typeof navigator!="undefined"&&navigator.sendBeacon){let i=new Blob([n],{type:"application/json"}),r=navigator.sendBeacon(t.endpoint,i);return t.debug&&console.log("[IndieBob] sendBeacon:",r,e.batch.length,"events"),r}for(let i=0;i<3;i++){try{let r=yield fetch(t.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:n,keepalive:!0});if(r.ok)return t.debug&&console.log("[IndieBob] sent:",e.batch.length,"events"),!0;if(r.status>=400&&r.status<500)return t.debug&&console.warn("[IndieBob] 4xx error, not retrying:",r.status),!1}catch(r){t.debug&&console.warn("[IndieBob] fetch error (attempt",i+1,"):",r);}i<2&&(yield ot(et[i]));}return t.debug&&console.warn("[IndieBob] failed to send after",3,"attempts"),false})}var E="_ib_queue",nt=10,it=5e3,rt=50,m=class{constructor(t){this.queue=[];this.timer=null;this.flushing=false;this.config=t,this.startTimer(),this.setupVisibilityFlush(),this.setupOnlineFlush();}push(t){this.queue.push(t),this.queue.length>=nt&&this.flush();}flush(t=false){return a(this,null,function*(){if(this.flushing&&!t||this.queue.length===0)return;this.flushing=true;let o=this.queue.splice(0);(yield w({batch:o},this.config,t))||this.pushToOfflineQueue(o),this.flushing=false;})}flushOfflineQueue(){return a(this,null,function*(){let t=this.getOfflineQueue();if(t.length===0)return;this.clearOfflineQueue(),yield w({batch:t},this.config);})}destroy(){this.timer&&clearInterval(this.timer);}startTimer(){this.timer=setInterval(()=>{this.queue.length>0&&this.flush();},it);}setupVisibilityFlush(){typeof document!="undefined"&&document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush(true);},{passive:true});}setupOnlineFlush(){typeof window!="undefined"&&(window.addEventListener("online",()=>{this.flushOfflineQueue();}),navigator.onLine&&this.flushOfflineQueue());}pushToOfflineQueue(t){try{let i=[...this.getOfflineQueue(),...t].slice(-rt);localStorage.setItem(E,JSON.stringify(i));}catch(o){}}getOfflineQueue(){try{let t=localStorage.getItem(E);return t?JSON.parse(t):[]}catch(t){return []}}clearOfflineQueue(){try{localStorage.removeItem(E);}catch(t){}}};function Q(){if(typeof window=="undefined")return "desktop";let e=window.innerWidth;return e<=768?"mobile":e<=1024?"tablet":"desktop"}function V(){return typeof window=="undefined"?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}function T(e){var c,O,R,C;let t=typeof window=="undefined",o=(c=e==null?void 0:e.path)!=null?c:t?"":window.location.pathname+window.location.search,n=t?"":window.location.href,i=(O=e==null?void 0:e.title)!=null?O:t?"":document.title,r=(R=e==null?void 0:e.referrer)!=null?R:t?"":document.referrer,g=V(),l=Q(),s=t?void 0:F(window.location.search);return {page:{path:o,url:n,title:i,referrer:r},device:x(d({},g),{type:l}),utm:s,locale:t?"en":navigator.language,timezone:t?"UTC":(C=Intl.DateTimeFormat().resolvedOptions().timeZone)!=null?C:"UTC",sdkVersion:"0.1.0"}}function H(e){if(typeof window=="undefined")return ()=>{};let t=history.pushState.bind(history),o=history.replaceState.bind(history);return history.pushState=function(...n){t(...n),e();},history.replaceState=function(...n){o(...n);},window.addEventListener("popstate",e),()=>{history.pushState=t,history.replaceState=o,window.removeEventListener("popstate",e);}}var X=class e{constructor(t){this.cleanupHistory=null;this.config=t,this.queue=new m(t),typeof window!="undefined"&&B(window.location.search,document.referrer);}static init(t){let o=D(t);return new e(o)}page(t){f(this.config)&&this.queue.push(this.buildEvent("page",{context:T(t)}));}track(t,o){f(this.config)&&this.queue.push(this.buildEvent("track",{event:t,properties:o}));}identify(t,o){f(this.config)&&(this.userId=t,this.queue.push(this.buildEvent("identify",{traits:o,userId:t})));}revenue(t){var o;f(this.config)&&this.queue.push(this.buildEvent("revenue",{revenue:{amount:t.amount,currency:(o=t.currency)!=null?o:"USD",plan:t.plan,type:t.type}}));}optOut(){this.queue.flush(),j(),this.userId=void 0,this.config.debug&&console.log("[IndieBob] opted out");}optIn(){Y(),this.config.debug&&console.log("[IndieBob] opted in");}isOptedOut(){return K()}flush(){return a(this,null,function*(){yield this.queue.flush();})}getAnonymousId(){return v()}getSessionId(){return S()}getConfig(){return Object.freeze(d({},this.config))}enableAutoPage(){var t;return (t=this.cleanupHistory)==null||t.call(this),this.cleanupHistory=H(()=>this.page()),()=>{var o;(o=this.cleanupHistory)==null||o.call(this),this.cleanupHistory=null;}}buildEvent(t,o){var n,i;return {v:1,projectId:this.config.projectId,type:t,anonymousId:v(),userId:(n=this.userId)!=null?n:o.userId,sessionId:S(),timestamp:new Date().toISOString(),context:(i=o.context)!=null?i:T(),event:o.event,properties:o.properties,traits:o.traits,revenue:o.revenue}}};
2
+ exports.IndieBob=X;return exports;})({});
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export{a as IndieBob}from'./chunk-XKQYO25W.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Configuration for the IndieBob tracker.
3
+ */
4
+ interface TrackerConfig {
5
+ /** Your SDK project ID from the IndieBob dashboard. Format: 'proj_...' */
6
+ projectId: string;
7
+ /** Ingestion endpoint URL. Default: 'https://indiebob.com/api/collect' */
8
+ endpoint?: string;
9
+ /** Log all events to the browser console. Default: false */
10
+ debug?: boolean;
11
+ /** Honor the browser Do Not Track header. Default: true */
12
+ respectDnt?: boolean;
13
+ /** Honor Global Privacy Control signal (legally binding in CA/GDPR). Default: true */
14
+ respectGpc?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Server-side tracker for Node.js environments.
19
+ * No browser APIs are used here.
20
+ */
21
+
22
+ /**
23
+ * Server-side IndieBob tracker. Use this in Node.js/Nuxt server routes.
24
+ *
25
+ * @example
26
+ * const bob = IndieBob.initServer({ projectId: 'proj_abc123def456ghij' })
27
+ * bob.track('server_event', { source: 'webhook' })
28
+ */
29
+ declare class IndieBobServer {
30
+ private config;
31
+ private constructor();
32
+ static initServer(config: TrackerConfig): IndieBobServer;
33
+ track(event: string, properties?: Record<string, unknown>, userId?: string): Promise<boolean>;
34
+ identify(userId: string, traits?: Record<string, unknown>): Promise<boolean>;
35
+ private buildEvent;
36
+ }
37
+ declare const IndieBob: typeof IndieBobServer;
38
+
39
+ export { IndieBob, IndieBobServer };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Configuration for the IndieBob tracker.
3
+ */
4
+ interface TrackerConfig {
5
+ /** Your SDK project ID from the IndieBob dashboard. Format: 'proj_...' */
6
+ projectId: string;
7
+ /** Ingestion endpoint URL. Default: 'https://indiebob.com/api/collect' */
8
+ endpoint?: string;
9
+ /** Log all events to the browser console. Default: false */
10
+ debug?: boolean;
11
+ /** Honor the browser Do Not Track header. Default: true */
12
+ respectDnt?: boolean;
13
+ /** Honor Global Privacy Control signal (legally binding in CA/GDPR). Default: true */
14
+ respectGpc?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Server-side tracker for Node.js environments.
19
+ * No browser APIs are used here.
20
+ */
21
+
22
+ /**
23
+ * Server-side IndieBob tracker. Use this in Node.js/Nuxt server routes.
24
+ *
25
+ * @example
26
+ * const bob = IndieBob.initServer({ projectId: 'proj_abc123def456ghij' })
27
+ * bob.track('server_event', { source: 'webhook' })
28
+ */
29
+ declare class IndieBobServer {
30
+ private config;
31
+ private constructor();
32
+ static initServer(config: TrackerConfig): IndieBobServer;
33
+ track(event: string, properties?: Record<string, unknown>, userId?: string): Promise<boolean>;
34
+ identify(userId: string, traits?: Record<string, unknown>): Promise<boolean>;
35
+ private buildEvent;
36
+ }
37
+ declare const IndieBob: typeof IndieBobServer;
38
+
39
+ export { IndieBob, IndieBobServer };
package/dist/server.js ADDED
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ // src/core/config.ts
4
+ var DEFAULT_ENDPOINT = "https://indiebob.com/api/collect";
5
+ function resolveConfig(input) {
6
+ if (!input.projectId || !input.projectId.startsWith("proj_")) {
7
+ throw new Error(
8
+ `[IndieBob] Invalid projectId "${input.projectId}". Must start with "proj_".`
9
+ );
10
+ }
11
+ return {
12
+ projectId: input.projectId,
13
+ endpoint: input.endpoint ?? DEFAULT_ENDPOINT,
14
+ debug: input.debug ?? false,
15
+ respectDnt: input.respectDnt ?? true,
16
+ respectGpc: input.respectGpc ?? true
17
+ };
18
+ }
19
+
20
+ // src/core/transport.ts
21
+ var MAX_RETRIES = 3;
22
+ var RETRY_DELAYS = [1e3, 2e3, 4e3];
23
+ async function sleep(ms) {
24
+ return new Promise((resolve) => setTimeout(resolve, ms));
25
+ }
26
+ async function sendBatchServer(payload, config) {
27
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
28
+ try {
29
+ const res = await fetch(config.endpoint, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify(payload)
33
+ });
34
+ if (res.ok) return true;
35
+ if (res.status >= 400 && res.status < 500) return false;
36
+ } catch {
37
+ }
38
+ if (attempt < MAX_RETRIES - 1) await sleep(RETRY_DELAYS[attempt]);
39
+ }
40
+ return false;
41
+ }
42
+
43
+ // src/integrations/server.ts
44
+ function generateId() {
45
+ try {
46
+ return crypto.randomUUID();
47
+ } catch {
48
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
49
+ }
50
+ }
51
+ var IndieBobServer = class _IndieBobServer {
52
+ constructor(config) {
53
+ this.config = config;
54
+ }
55
+ static initServer(config) {
56
+ return new _IndieBobServer(resolveConfig(config));
57
+ }
58
+ track(event, properties, userId) {
59
+ return sendBatchServer({
60
+ batch: [this.buildEvent("track", { event, properties, userId })]
61
+ }, this.config);
62
+ }
63
+ identify(userId, traits) {
64
+ return sendBatchServer({
65
+ batch: [this.buildEvent("identify", { traits, userId })]
66
+ }, this.config);
67
+ }
68
+ buildEvent(type, extras) {
69
+ return {
70
+ v: 1,
71
+ projectId: this.config.projectId,
72
+ type,
73
+ anonymousId: `server_${generateId()}`,
74
+ userId: extras.userId,
75
+ sessionId: `server_${generateId()}`,
76
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
77
+ context: {
78
+ page: { path: "", url: "", title: "", referrer: "" },
79
+ device: { width: 0, height: 0, type: "desktop" },
80
+ locale: "en",
81
+ timezone: "UTC",
82
+ sdkVersion: "0.1.0"
83
+ },
84
+ event: extras.event,
85
+ properties: extras.properties,
86
+ traits: extras.traits
87
+ };
88
+ }
89
+ };
90
+ var IndieBob = IndieBobServer;
91
+
92
+ exports.IndieBob = IndieBob;
93
+ exports.IndieBobServer = IndieBobServer;
@@ -0,0 +1,90 @@
1
+ // src/core/config.ts
2
+ var DEFAULT_ENDPOINT = "https://indiebob.com/api/collect";
3
+ function resolveConfig(input) {
4
+ if (!input.projectId || !input.projectId.startsWith("proj_")) {
5
+ throw new Error(
6
+ `[IndieBob] Invalid projectId "${input.projectId}". Must start with "proj_".`
7
+ );
8
+ }
9
+ return {
10
+ projectId: input.projectId,
11
+ endpoint: input.endpoint ?? DEFAULT_ENDPOINT,
12
+ debug: input.debug ?? false,
13
+ respectDnt: input.respectDnt ?? true,
14
+ respectGpc: input.respectGpc ?? true
15
+ };
16
+ }
17
+
18
+ // src/core/transport.ts
19
+ var MAX_RETRIES = 3;
20
+ var RETRY_DELAYS = [1e3, 2e3, 4e3];
21
+ async function sleep(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms));
23
+ }
24
+ async function sendBatchServer(payload, config) {
25
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
26
+ try {
27
+ const res = await fetch(config.endpoint, {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify(payload)
31
+ });
32
+ if (res.ok) return true;
33
+ if (res.status >= 400 && res.status < 500) return false;
34
+ } catch {
35
+ }
36
+ if (attempt < MAX_RETRIES - 1) await sleep(RETRY_DELAYS[attempt]);
37
+ }
38
+ return false;
39
+ }
40
+
41
+ // src/integrations/server.ts
42
+ function generateId() {
43
+ try {
44
+ return crypto.randomUUID();
45
+ } catch {
46
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
47
+ }
48
+ }
49
+ var IndieBobServer = class _IndieBobServer {
50
+ constructor(config) {
51
+ this.config = config;
52
+ }
53
+ static initServer(config) {
54
+ return new _IndieBobServer(resolveConfig(config));
55
+ }
56
+ track(event, properties, userId) {
57
+ return sendBatchServer({
58
+ batch: [this.buildEvent("track", { event, properties, userId })]
59
+ }, this.config);
60
+ }
61
+ identify(userId, traits) {
62
+ return sendBatchServer({
63
+ batch: [this.buildEvent("identify", { traits, userId })]
64
+ }, this.config);
65
+ }
66
+ buildEvent(type, extras) {
67
+ return {
68
+ v: 1,
69
+ projectId: this.config.projectId,
70
+ type,
71
+ anonymousId: `server_${generateId()}`,
72
+ userId: extras.userId,
73
+ sessionId: `server_${generateId()}`,
74
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
75
+ context: {
76
+ page: { path: "", url: "", title: "", referrer: "" },
77
+ device: { width: 0, height: 0, type: "desktop" },
78
+ locale: "en",
79
+ timezone: "UTC",
80
+ sdkVersion: "0.1.0"
81
+ },
82
+ event: extras.event,
83
+ properties: extras.properties,
84
+ traits: extras.traits
85
+ };
86
+ }
87
+ };
88
+ var IndieBob = IndieBobServer;
89
+
90
+ export { IndieBob, IndieBobServer };
package/dist/vue.cjs ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var z=Object.defineProperty,G=Object.defineProperties;var W=Object.getOwnPropertyDescriptors;var x=Object.getOwnPropertySymbols;var $=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var P=(t,e,o)=>e in t?z(t,e,{enumerable:true,configurable:true,writable:true,value:o}):t[e]=o,d=(t,e)=>{for(var o in e||(e={}))$.call(e,o)&&P(t,o,e[o]);if(x)for(var o of x(e))Z.call(e,o)&&P(t,o,e[o]);return t},D=(t,e)=>G(t,W(e));var a=(t,e,o)=>new Promise((n,r)=>{var i=s=>{try{l(o.next(s));}catch(u){r(u);}},h=s=>{try{l(o.throw(s));}catch(u){r(u);}},l=s=>s.done?n(s.value):Promise.resolve(s.value).then(i,h);l((o=o.apply(t,e)).next());});var ee="https://indiebob.com/api/collect";function N(t){var e,o,n,r;if(!t.projectId||!t.projectId.startsWith("proj_"))throw new Error(`[IndieBob] Invalid projectId "${t.projectId}". Must start with "proj_".`);return {projectId:t.projectId,endpoint:(e=t.endpoint)!=null?e:ee,debug:(o=t.debug)!=null?o:false,respectDnt:(n=t.respectDnt)!=null?n:true,respectGpc:(r=t.respectGpc)!=null?r:true}}var c="_ib_anon";function y(){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-",e="";if(typeof crypto!="undefined"&&crypto.getRandomValues){let o=new Uint8Array(21);crypto.getRandomValues(o);for(let n of o)e+=t[n%t.length];}else for(let o=0;o<21;o++)e+=t[Math.floor(Math.random()*t.length)];return e}function v(){try{let t=localStorage.getItem(c);return t||(t=y(),localStorage.setItem(c,t)),t}catch(t){try{let e=sessionStorage.getItem(c);return e||(e=y(),sessionStorage.setItem(c,e)),e}catch(e){return y()}}}function B(){try{localStorage.removeItem(c);}catch(t){}try{sessionStorage.removeItem(c);}catch(t){}}var I="_ib_session",S="_ib_session_last";function U(){try{return crypto.randomUUID()}catch(t){return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}}function b(){var t;try{let e=Date.now(),o=parseInt((t=sessionStorage.getItem(S))!=null?t:"0",10),n=sessionStorage.getItem(I),r=o>0&&e-o>18e5;return (!n||r)&&(n=U(),sessionStorage.setItem(I,n)),sessionStorage.setItem(S,String(e)),n}catch(e){return U()}}function M(){try{sessionStorage.removeItem(I);}catch(t){}try{sessionStorage.removeItem(S);}catch(t){}}var _="_ib_first_utm",q="_ib_last_utm",T="_ib_referrer";function F(t){let e=new URLSearchParams(t),o={},n=false;return e.has("utm_source")&&(o.source=e.get("utm_source"),n=true),e.has("utm_medium")&&(o.medium=e.get("utm_medium"),n=true),e.has("utm_campaign")&&(o.campaign=e.get("utm_campaign"),n=true),e.has("utm_term")&&(o.term=e.get("utm_term"),n=true),e.has("utm_content")&&(o.content=e.get("utm_content"),n=true),n?o:null}function L(t,e){let o=F(t);try{o&&!localStorage.getItem(_)&&localStorage.setItem(_,JSON.stringify(o)),o&&localStorage.setItem(q,JSON.stringify(o)),e&&!localStorage.getItem(T)&&localStorage.setItem(T,e);}catch(n){}}function j(t){var e;return (e=F(t))!=null?e:void 0}function Y(){try{localStorage.removeItem(_);}catch(t){}try{localStorage.removeItem(q);}catch(t){}try{localStorage.removeItem(T);}catch(t){}}var p="_ib_optout",te="_ib_queue";function f(t){try{if(localStorage.getItem(p))return !1}catch(e){}return !(t.respectDnt&&typeof navigator!="undefined"&&(navigator.doNotTrack==="1"||navigator.msDoNotTrack==="1")||t.respectGpc&&typeof navigator!="undefined"&&navigator.globalPrivacyControl===true)}function K(){try{localStorage.setItem(p,"1");}catch(t){}B(),M(),Y();try{localStorage.removeItem(te);}catch(t){}}function Q(){try{localStorage.removeItem(p);}catch(t){}}function V(){try{return !!localStorage.getItem(p)}catch(t){return false}}var oe=[1e3,2e3,4e3];function ne(t){return a(this,null,function*(){return new Promise(e=>setTimeout(e,t))})}function E(t,e,o=false){return a(this,null,function*(){let n=JSON.stringify(t);if(o&&typeof navigator!="undefined"&&navigator.sendBeacon){let r=new Blob([n],{type:"application/json"}),i=navigator.sendBeacon(e.endpoint,r);return e.debug&&console.log("[IndieBob] sendBeacon:",i,t.batch.length,"events"),i}for(let r=0;r<3;r++){try{let i=yield fetch(e.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:n,keepalive:!0});if(i.ok)return e.debug&&console.log("[IndieBob] sent:",t.batch.length,"events"),!0;if(i.status>=400&&i.status<500)return e.debug&&console.warn("[IndieBob] 4xx error, not retrying:",i.status),!1}catch(i){e.debug&&console.warn("[IndieBob] fetch error (attempt",r+1,"):",i);}r<2&&(yield ne(oe[r]));}return e.debug&&console.warn("[IndieBob] failed to send after",3,"attempts"),false})}var w="_ib_queue",re=10,ie=5e3,se=50,m=class{constructor(e){this.queue=[];this.timer=null;this.flushing=false;this.config=e,this.startTimer(),this.setupVisibilityFlush(),this.setupOnlineFlush();}push(e){this.queue.push(e),this.queue.length>=re&&this.flush();}flush(e=false){return a(this,null,function*(){if(this.flushing&&!e||this.queue.length===0)return;this.flushing=true;let o=this.queue.splice(0);(yield E({batch:o},this.config,e))||this.pushToOfflineQueue(o),this.flushing=false;})}flushOfflineQueue(){return a(this,null,function*(){let e=this.getOfflineQueue();if(e.length===0)return;this.clearOfflineQueue(),yield E({batch:e},this.config);})}destroy(){this.timer&&clearInterval(this.timer);}startTimer(){this.timer=setInterval(()=>{this.queue.length>0&&this.flush();},ie);}setupVisibilityFlush(){typeof document!="undefined"&&document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush(true);},{passive:true});}setupOnlineFlush(){typeof window!="undefined"&&(window.addEventListener("online",()=>{this.flushOfflineQueue();}),navigator.onLine&&this.flushOfflineQueue());}pushToOfflineQueue(e){try{let r=[...this.getOfflineQueue(),...e].slice(-se);localStorage.setItem(w,JSON.stringify(r));}catch(o){}}getOfflineQueue(){try{let e=localStorage.getItem(w);return e?JSON.parse(e):[]}catch(e){return []}}clearOfflineQueue(){try{localStorage.removeItem(w);}catch(e){}}};function H(){if(typeof window=="undefined")return "desktop";let t=window.innerWidth;return t<=768?"mobile":t<=1024?"tablet":"desktop"}function X(){return typeof window=="undefined"?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}function R(t){var u,C,k,A;let e=typeof window=="undefined",o=(u=t==null?void 0:t.path)!=null?u:e?"":window.location.pathname+window.location.search,n=e?"":window.location.href,r=(C=t==null?void 0:t.title)!=null?C:e?"":document.title,i=(k=t==null?void 0:t.referrer)!=null?k:e?"":document.referrer,h=X(),l=H(),s=e?void 0:j(window.location.search);return {page:{path:o,url:n,title:r,referrer:i},device:D(d({},h),{type:l}),utm:s,locale:e?"en":navigator.language,timezone:e?"UTC":(A=Intl.DateTimeFormat().resolvedOptions().timeZone)!=null?A:"UTC",sdkVersion:"0.1.0"}}function J(t){if(typeof window=="undefined")return ()=>{};let e=history.pushState.bind(history),o=history.replaceState.bind(history);return history.pushState=function(...n){e(...n),t();},history.replaceState=function(...n){o(...n);},window.addEventListener("popstate",t),()=>{history.pushState=e,history.replaceState=o,window.removeEventListener("popstate",t);}}var g=class t{constructor(e){this.cleanupHistory=null;this.config=e,this.queue=new m(e),typeof window!="undefined"&&L(window.location.search,document.referrer);}static init(e){let o=N(e);return new t(o)}page(e){f(this.config)&&this.queue.push(this.buildEvent("page",{context:R(e)}));}track(e,o){f(this.config)&&this.queue.push(this.buildEvent("track",{event:e,properties:o}));}identify(e,o){f(this.config)&&(this.userId=e,this.queue.push(this.buildEvent("identify",{traits:o,userId:e})));}revenue(e){var o;f(this.config)&&this.queue.push(this.buildEvent("revenue",{revenue:{amount:e.amount,currency:(o=e.currency)!=null?o:"USD",plan:e.plan,type:e.type}}));}optOut(){this.queue.flush(),K(),this.userId=void 0,this.config.debug&&console.log("[IndieBob] opted out");}optIn(){Q(),this.config.debug&&console.log("[IndieBob] opted in");}isOptedOut(){return V()}flush(){return a(this,null,function*(){yield this.queue.flush();})}getAnonymousId(){return v()}getSessionId(){return b()}getConfig(){return Object.freeze(d({},this.config))}enableAutoPage(){var e;return (e=this.cleanupHistory)==null||e.call(this),this.cleanupHistory=J(()=>this.page()),()=>{var o;(o=this.cleanupHistory)==null||o.call(this),this.cleanupHistory=null;}}buildEvent(e,o){var n,r;return {v:1,projectId:this.config.projectId,type:e,anonymousId:v(),userId:(n=this.userId)!=null?n:o.userId,sessionId:b(),timestamp:new Date().toISOString(),context:(r=o.context)!=null?r:R(),event:o.event,properties:o.properties,traits:o.traits,revenue:o.revenue}}};var ae=Symbol("indiebob"),O=null,Fe={install(t,e){let o=g.init(e);O=o,t.provide(ae,o),t.config.globalProperties.$bob=o;let n=t.config.globalProperties.$router;n&&n.afterEach(()=>{o.page();});}};function Le(){if(O)return O;throw new Error("[IndieBob] useTracker() called before IndieBobPlugin was installed.")}
2
+ exports.IndieBob=g;exports.IndieBobPlugin=Fe;exports.useTracker=Le;
package/dist/vue.d.mts ADDED
@@ -0,0 +1,29 @@
1
+ import { Plugin } from 'vue';
2
+ import { IndieBob } from './index.mjs';
3
+
4
+ /**
5
+ * Vue plugin for @indiebob/tracker.
6
+ *
7
+ * @example
8
+ * // main.ts
9
+ * app.use(IndieBobPlugin, { projectId: 'proj_abc123def456ghij' })
10
+ *
11
+ * @example
12
+ * // Nuxt plugin (plugins/tracker.client.ts)
13
+ * export default defineNuxtPlugin((nuxtApp) => {
14
+ * nuxtApp.vueApp.use(IndieBobPlugin, {
15
+ * projectId: useRuntimeConfig().public.trackerProjectId,
16
+ * })
17
+ * })
18
+ */
19
+ declare const IndieBobPlugin: Plugin;
20
+ /**
21
+ * Get the IndieBob tracker instance inside a Vue component or composable.
22
+ *
23
+ * @example
24
+ * const bob = useTracker()
25
+ * bob.track('button_clicked', { button: 'cta_hero' })
26
+ */
27
+ declare function useTracker(): IndieBob;
28
+
29
+ export { IndieBob, IndieBobPlugin, useTracker };
package/dist/vue.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { Plugin } from 'vue';
2
+ import { IndieBob } from './index.js';
3
+
4
+ /**
5
+ * Vue plugin for @indiebob/tracker.
6
+ *
7
+ * @example
8
+ * // main.ts
9
+ * app.use(IndieBobPlugin, { projectId: 'proj_abc123def456ghij' })
10
+ *
11
+ * @example
12
+ * // Nuxt plugin (plugins/tracker.client.ts)
13
+ * export default defineNuxtPlugin((nuxtApp) => {
14
+ * nuxtApp.vueApp.use(IndieBobPlugin, {
15
+ * projectId: useRuntimeConfig().public.trackerProjectId,
16
+ * })
17
+ * })
18
+ */
19
+ declare const IndieBobPlugin: Plugin;
20
+ /**
21
+ * Get the IndieBob tracker instance inside a Vue component or composable.
22
+ *
23
+ * @example
24
+ * const bob = useTracker()
25
+ * bob.track('button_clicked', { button: 'cta_hero' })
26
+ */
27
+ declare function useTracker(): IndieBob;
28
+
29
+ export { IndieBob, IndieBobPlugin, useTracker };
@@ -0,0 +1,2 @@
1
+ var IndieBob=(function(exports){'use strict';var z=Object.defineProperty,G=Object.defineProperties;var W=Object.getOwnPropertyDescriptors;var x=Object.getOwnPropertySymbols;var $=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var P=(t,e,o)=>e in t?z(t,e,{enumerable:true,configurable:true,writable:true,value:o}):t[e]=o,d=(t,e)=>{for(var o in e||(e={}))$.call(e,o)&&P(t,o,e[o]);if(x)for(var o of x(e))Z.call(e,o)&&P(t,o,e[o]);return t},D=(t,e)=>G(t,W(e));var a=(t,e,o)=>new Promise((n,r)=>{var i=s=>{try{l(o.next(s));}catch(u){r(u);}},h=s=>{try{l(o.throw(s));}catch(u){r(u);}},l=s=>s.done?n(s.value):Promise.resolve(s.value).then(i,h);l((o=o.apply(t,e)).next());});var ee="https://indiebob.com/api/collect";function N(t){var e,o,n,r;if(!t.projectId||!t.projectId.startsWith("proj_"))throw new Error(`[IndieBob] Invalid projectId "${t.projectId}". Must start with "proj_".`);return {projectId:t.projectId,endpoint:(e=t.endpoint)!=null?e:ee,debug:(o=t.debug)!=null?o:false,respectDnt:(n=t.respectDnt)!=null?n:true,respectGpc:(r=t.respectGpc)!=null?r:true}}var c="_ib_anon";function y(){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-",e="";if(typeof crypto!="undefined"&&crypto.getRandomValues){let o=new Uint8Array(21);crypto.getRandomValues(o);for(let n of o)e+=t[n%t.length];}else for(let o=0;o<21;o++)e+=t[Math.floor(Math.random()*t.length)];return e}function v(){try{let t=localStorage.getItem(c);return t||(t=y(),localStorage.setItem(c,t)),t}catch(t){try{let e=sessionStorage.getItem(c);return e||(e=y(),sessionStorage.setItem(c,e)),e}catch(e){return y()}}}function B(){try{localStorage.removeItem(c);}catch(t){}try{sessionStorage.removeItem(c);}catch(t){}}var I="_ib_session",S="_ib_session_last";function U(){try{return crypto.randomUUID()}catch(t){return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}}function b(){var t;try{let e=Date.now(),o=parseInt((t=sessionStorage.getItem(S))!=null?t:"0",10),n=sessionStorage.getItem(I),r=o>0&&e-o>18e5;return (!n||r)&&(n=U(),sessionStorage.setItem(I,n)),sessionStorage.setItem(S,String(e)),n}catch(e){return U()}}function M(){try{sessionStorage.removeItem(I);}catch(t){}try{sessionStorage.removeItem(S);}catch(t){}}var _="_ib_first_utm",q="_ib_last_utm",T="_ib_referrer";function F(t){let e=new URLSearchParams(t),o={},n=false;return e.has("utm_source")&&(o.source=e.get("utm_source"),n=true),e.has("utm_medium")&&(o.medium=e.get("utm_medium"),n=true),e.has("utm_campaign")&&(o.campaign=e.get("utm_campaign"),n=true),e.has("utm_term")&&(o.term=e.get("utm_term"),n=true),e.has("utm_content")&&(o.content=e.get("utm_content"),n=true),n?o:null}function L(t,e){let o=F(t);try{o&&!localStorage.getItem(_)&&localStorage.setItem(_,JSON.stringify(o)),o&&localStorage.setItem(q,JSON.stringify(o)),e&&!localStorage.getItem(T)&&localStorage.setItem(T,e);}catch(n){}}function j(t){var e;return (e=F(t))!=null?e:void 0}function Y(){try{localStorage.removeItem(_);}catch(t){}try{localStorage.removeItem(q);}catch(t){}try{localStorage.removeItem(T);}catch(t){}}var p="_ib_optout",te="_ib_queue";function f(t){try{if(localStorage.getItem(p))return !1}catch(e){}return !(t.respectDnt&&typeof navigator!="undefined"&&(navigator.doNotTrack==="1"||navigator.msDoNotTrack==="1")||t.respectGpc&&typeof navigator!="undefined"&&navigator.globalPrivacyControl===true)}function K(){try{localStorage.setItem(p,"1");}catch(t){}B(),M(),Y();try{localStorage.removeItem(te);}catch(t){}}function Q(){try{localStorage.removeItem(p);}catch(t){}}function V(){try{return !!localStorage.getItem(p)}catch(t){return false}}var oe=[1e3,2e3,4e3];function ne(t){return a(this,null,function*(){return new Promise(e=>setTimeout(e,t))})}function E(t,e,o=false){return a(this,null,function*(){let n=JSON.stringify(t);if(o&&typeof navigator!="undefined"&&navigator.sendBeacon){let r=new Blob([n],{type:"application/json"}),i=navigator.sendBeacon(e.endpoint,r);return e.debug&&console.log("[IndieBob] sendBeacon:",i,t.batch.length,"events"),i}for(let r=0;r<3;r++){try{let i=yield fetch(e.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:n,keepalive:!0});if(i.ok)return e.debug&&console.log("[IndieBob] sent:",t.batch.length,"events"),!0;if(i.status>=400&&i.status<500)return e.debug&&console.warn("[IndieBob] 4xx error, not retrying:",i.status),!1}catch(i){e.debug&&console.warn("[IndieBob] fetch error (attempt",r+1,"):",i);}r<2&&(yield ne(oe[r]));}return e.debug&&console.warn("[IndieBob] failed to send after",3,"attempts"),false})}var w="_ib_queue",re=10,ie=5e3,se=50,m=class{constructor(e){this.queue=[];this.timer=null;this.flushing=false;this.config=e,this.startTimer(),this.setupVisibilityFlush(),this.setupOnlineFlush();}push(e){this.queue.push(e),this.queue.length>=re&&this.flush();}flush(e=false){return a(this,null,function*(){if(this.flushing&&!e||this.queue.length===0)return;this.flushing=true;let o=this.queue.splice(0);(yield E({batch:o},this.config,e))||this.pushToOfflineQueue(o),this.flushing=false;})}flushOfflineQueue(){return a(this,null,function*(){let e=this.getOfflineQueue();if(e.length===0)return;this.clearOfflineQueue(),yield E({batch:e},this.config);})}destroy(){this.timer&&clearInterval(this.timer);}startTimer(){this.timer=setInterval(()=>{this.queue.length>0&&this.flush();},ie);}setupVisibilityFlush(){typeof document!="undefined"&&document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush(true);},{passive:true});}setupOnlineFlush(){typeof window!="undefined"&&(window.addEventListener("online",()=>{this.flushOfflineQueue();}),navigator.onLine&&this.flushOfflineQueue());}pushToOfflineQueue(e){try{let r=[...this.getOfflineQueue(),...e].slice(-se);localStorage.setItem(w,JSON.stringify(r));}catch(o){}}getOfflineQueue(){try{let e=localStorage.getItem(w);return e?JSON.parse(e):[]}catch(e){return []}}clearOfflineQueue(){try{localStorage.removeItem(w);}catch(e){}}};function H(){if(typeof window=="undefined")return "desktop";let t=window.innerWidth;return t<=768?"mobile":t<=1024?"tablet":"desktop"}function X(){return typeof window=="undefined"?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}function R(t){var u,C,k,A;let e=typeof window=="undefined",o=(u=t==null?void 0:t.path)!=null?u:e?"":window.location.pathname+window.location.search,n=e?"":window.location.href,r=(C=t==null?void 0:t.title)!=null?C:e?"":document.title,i=(k=t==null?void 0:t.referrer)!=null?k:e?"":document.referrer,h=X(),l=H(),s=e?void 0:j(window.location.search);return {page:{path:o,url:n,title:r,referrer:i},device:D(d({},h),{type:l}),utm:s,locale:e?"en":navigator.language,timezone:e?"UTC":(A=Intl.DateTimeFormat().resolvedOptions().timeZone)!=null?A:"UTC",sdkVersion:"0.1.0"}}function J(t){if(typeof window=="undefined")return ()=>{};let e=history.pushState.bind(history),o=history.replaceState.bind(history);return history.pushState=function(...n){e(...n),t();},history.replaceState=function(...n){o(...n);},window.addEventListener("popstate",t),()=>{history.pushState=e,history.replaceState=o,window.removeEventListener("popstate",t);}}var g=class t{constructor(e){this.cleanupHistory=null;this.config=e,this.queue=new m(e),typeof window!="undefined"&&L(window.location.search,document.referrer);}static init(e){let o=N(e);return new t(o)}page(e){f(this.config)&&this.queue.push(this.buildEvent("page",{context:R(e)}));}track(e,o){f(this.config)&&this.queue.push(this.buildEvent("track",{event:e,properties:o}));}identify(e,o){f(this.config)&&(this.userId=e,this.queue.push(this.buildEvent("identify",{traits:o,userId:e})));}revenue(e){var o;f(this.config)&&this.queue.push(this.buildEvent("revenue",{revenue:{amount:e.amount,currency:(o=e.currency)!=null?o:"USD",plan:e.plan,type:e.type}}));}optOut(){this.queue.flush(),K(),this.userId=void 0,this.config.debug&&console.log("[IndieBob] opted out");}optIn(){Q(),this.config.debug&&console.log("[IndieBob] opted in");}isOptedOut(){return V()}flush(){return a(this,null,function*(){yield this.queue.flush();})}getAnonymousId(){return v()}getSessionId(){return b()}getConfig(){return Object.freeze(d({},this.config))}enableAutoPage(){var e;return (e=this.cleanupHistory)==null||e.call(this),this.cleanupHistory=J(()=>this.page()),()=>{var o;(o=this.cleanupHistory)==null||o.call(this),this.cleanupHistory=null;}}buildEvent(e,o){var n,r;return {v:1,projectId:this.config.projectId,type:e,anonymousId:v(),userId:(n=this.userId)!=null?n:o.userId,sessionId:b(),timestamp:new Date().toISOString(),context:(r=o.context)!=null?r:R(),event:o.event,properties:o.properties,traits:o.traits,revenue:o.revenue}}};var ae=Symbol("indiebob"),O=null,Fe={install(t,e){let o=g.init(e);O=o,t.provide(ae,o),t.config.globalProperties.$bob=o;let n=t.config.globalProperties.$router;n&&n.afterEach(()=>{o.page();});}};function Le(){if(O)return O;throw new Error("[IndieBob] useTracker() called before IndieBobPlugin was installed.")}
2
+ exports.IndieBob=g;exports.IndieBobPlugin=Fe;exports.useTracker=Le;return exports;})({});
package/dist/vue.js ADDED
@@ -0,0 +1 @@
1
+ import {a}from'./chunk-XKQYO25W.js';export{a as IndieBob}from'./chunk-XKQYO25W.js';var l=Symbol("indiebob"),r=null,b={install(o,n){let e=a.init(n);r=e,o.provide(l,e),o.config.globalProperties.$bob=e;let t=o.config.globalProperties.$router;t&&t.afterEach(()=>{e.page();});}};function p(){if(r)return r;throw new Error("[IndieBob] useTracker() called before IndieBobPlugin was installed.")}export{b as IndieBobPlugin,p as useTracker};
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@indiebob/tracker",
3
+ "version": "0.1.0",
4
+ "description": "Privacy-first analytics SDK for web applications. <5KB gzipped. No cookies.",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "author": "IndieBob <hello@indiebob.com>",
8
+ "homepage": "https://indiebob.com/docs/sdk",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://gitlab.com/indiebob/indiebob-monorepo.git",
12
+ "directory": "packages/tracker"
13
+ },
14
+ "keywords": [
15
+ "analytics",
16
+ "tracking",
17
+ "privacy",
18
+ "sdk",
19
+ "web-analytics",
20
+ "event-tracking",
21
+ "utm",
22
+ "attribution",
23
+ "vue",
24
+ "nuxt"
25
+ ],
26
+ "sideEffects": false,
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "browser": "./dist/index.global.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs"
36
+ },
37
+ "./vue": {
38
+ "types": "./dist/vue.d.ts",
39
+ "import": "./dist/vue.js",
40
+ "require": "./dist/vue.cjs"
41
+ },
42
+ "./server": {
43
+ "types": "./dist/server.d.ts",
44
+ "import": "./dist/server.js",
45
+ "require": "./dist/server.cjs"
46
+ }
47
+ },
48
+ "files": [
49
+ "dist",
50
+ "README.md",
51
+ "CHANGELOG.md"
52
+ ],
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "dev": "tsup --watch",
56
+ "type-check": "tsc --noEmit",
57
+ "cdn:publish": "tsx scripts/publish-cdn.mts",
58
+ "prepublishOnly": "pnpm run build"
59
+ },
60
+ "devDependencies": {
61
+ "@aws-sdk/client-s3": "^3.1000.0",
62
+ "@types/node": "^22.0.0",
63
+ "dotenv": "^16.5.0",
64
+ "tsup": "^8.5.1",
65
+ "tsx": "^4.19.0",
66
+ "typescript": "^5.7.0",
67
+ "vue": "^3.5.0",
68
+ "vue-router": "^4.5.0"
69
+ },
70
+ "peerDependencies": {
71
+ "vue": ">=3.3.0",
72
+ "vue-router": ">=4.0.0"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "vue": {
76
+ "optional": true
77
+ },
78
+ "vue-router": {
79
+ "optional": true
80
+ }
81
+ }
82
+ }