@indiebob/tracker 0.2.4 → 0.4.1

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/dist/vue.cjs CHANGED
@@ -1,2 +1,997 @@
1
- 'use strict';var ft=Object.defineProperty,pt=Object.defineProperties;var gt=Object.getOwnPropertyDescriptors;var F=Object.getOwnPropertySymbols;var mt=Object.prototype.hasOwnProperty,ht=Object.prototype.propertyIsEnumerable;var U=(e,t,n)=>t in e?ft(e,t,{enumerable:true,configurable:true,writable:true,value:n}):e[t]=n,p=(e,t)=>{for(var n in t||(t={}))mt.call(t,n)&&U(e,n,t[n]);if(F)for(var n of F(t))ht.call(t,n)&&U(e,n,t[n]);return e},L=(e,t)=>pt(e,gt(t));var l=(e,t,n)=>new Promise((o,i)=>{var r=a=>{try{s(n.next(a));}catch(u){i(u);}},c=a=>{try{s(n.throw(a));}catch(u){i(u);}},s=a=>a.done?o(a.value):Promise.resolve(a.value).then(r,c);s((n=n.apply(e,t)).next());});var vt="https://www.indiebob.com/api/collect";function M(e){var t,n,o,i,r;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:vt,debug:(n=e.debug)!=null?n:false,respectDnt:(o=e.respectDnt)!=null?o:true,respectGpc:(i=e.respectGpc)!=null?i:true,autoDetect:(r=e.autoDetect)!=null?r:true}}var d="_ib_anon";function b(){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-",t="";if(typeof crypto!="undefined"&&crypto.getRandomValues){let n=new Uint8Array(21);crypto.getRandomValues(n);for(let o of n)t+=e[o%e.length];}else for(let n=0;n<21;n++)t+=e[Math.floor(Math.random()*e.length)];return t}function I(){try{let e=localStorage.getItem(d);return e||(e=b(),localStorage.setItem(d,e)),e}catch(e){try{let t=sessionStorage.getItem(d);return t||(t=b(),sessionStorage.setItem(d,t)),t}catch(t){return b()}}}function B(){try{localStorage.removeItem(d);}catch(e){}try{sessionStorage.removeItem(d);}catch(e){}}var S="_ib_session",E="_ib_session_last";function q(){try{return crypto.randomUUID()}catch(e){return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}}function w(){var e;try{let t=Date.now(),n=parseInt((e=sessionStorage.getItem(E))!=null?e:"0",10),o=sessionStorage.getItem(S),i=n>0&&t-n>18e5;return (!o||i)&&(o=q(),sessionStorage.setItem(S,o)),sessionStorage.setItem(E,String(t)),o}catch(t){return q()}}function j(){try{sessionStorage.removeItem(S);}catch(e){}try{sessionStorage.removeItem(E);}catch(e){}}var g="_ib_first_utm",Y="_ib_last_utm",m="_ib_referrer";function H(e){let t=new URLSearchParams(e),n={},o=false;return t.has("utm_source")?(n.source=t.get("utm_source"),o=true):t.get("ref")&&(n.source=t.get("ref"),o=true),t.has("utm_medium")&&(n.medium=t.get("utm_medium"),o=true),t.has("utm_campaign")&&(n.campaign=t.get("utm_campaign"),o=true),t.has("utm_term")&&(n.term=t.get("utm_term"),o=true),t.has("utm_content")&&(n.content=t.get("utm_content"),o=true),o?n:null}function K(e,t){let n=H(e);try{n&&!localStorage.getItem(g)&&localStorage.setItem(g,JSON.stringify(n)),n&&localStorage.setItem(Y,JSON.stringify(n)),t&&!localStorage.getItem(m)&&localStorage.setItem(m,t);}catch(o){}}function V(){try{let e=localStorage.getItem(g);return e?JSON.parse(e):void 0}catch(e){return}}function Q(){var e;try{return (e=localStorage.getItem(m))!=null?e:void 0}catch(t){return}}function z(e){var t;return (t=H(e))!=null?t:void 0}function X(){try{localStorage.removeItem(g);}catch(e){}try{localStorage.removeItem(Y);}catch(e){}try{localStorage.removeItem(m);}catch(e){}}var h="_ib_optout",yt="_ib_queue";function f(e){try{if(localStorage.getItem(h))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 $(){try{localStorage.setItem(h,"1");}catch(e){}B(),j(),X();try{localStorage.removeItem(yt);}catch(e){}}function J(){try{localStorage.removeItem(h);}catch(e){}}function G(){try{return !!localStorage.getItem(h)}catch(e){return false}}var bt=[1e3,2e3,4e3];function It(e){return l(this,null,function*(){return new Promise(t=>setTimeout(t,e))})}function _(e,t,n=false){return l(this,null,function*(){let o=JSON.stringify(e);if(n&&typeof navigator!="undefined"&&navigator.sendBeacon){let i=new Blob([o],{type:"text/plain;charset=UTF-8"}),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:o,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 It(bt[i]));}return t.debug&&console.warn("[IndieBob] failed to send after",3,"attempts"),false})}var T="_ib_queue",St=10,Et=5e3,wt=50,v=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>=St&&this.flush();}flush(t=false){return l(this,null,function*(){if(this.flushing&&!t||this.queue.length===0)return;this.flushing=true;let n=this.queue.splice(0);(yield _({batch:n},this.config,t))||this.pushToOfflineQueue(n),this.flushing=false;})}flushOfflineQueue(){return l(this,null,function*(){let t=this.getOfflineQueue();if(t.length===0)return;this.clearOfflineQueue(),yield _({batch:t},this.config);})}destroy(){this.timer&&clearInterval(this.timer);}startTimer(){this.timer=setInterval(()=>{this.queue.length>0&&this.flush();},Et);}setupVisibilityFlush(){typeof document!="undefined"&&(document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush(true);},{passive:true}),window.addEventListener("pagehide",()=>{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(-wt);localStorage.setItem(T,JSON.stringify(i));}catch(n){}}getOfflineQueue(){try{let t=localStorage.getItem(T);return t?JSON.parse(t):[]}catch(t){return []}}clearOfflineQueue(){try{localStorage.removeItem(T);}catch(t){}}};function W(){if(typeof window=="undefined")return "desktop";let e=window.innerWidth;return e<=768?"mobile":e<=1024?"tablet":"desktop"}function Z(){return typeof window=="undefined"?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}var _t=/^https?:\/\/[^/]*(stripe\.com|paypal\.com|paddle\.com|lemonsqueezy\.com|gumroad\.com)(\/|$)/i;function k(e){var x,A,P,C,D,N;let t=typeof window=="undefined",n=(x=e==null?void 0:e.path)!=null?x:t?"":window.location.pathname+window.location.search,o=t?"":window.location.href,i=(A=e==null?void 0:e.title)!=null?A:t?"":document.title,r=(P=e==null?void 0:e.referrer)!=null?P:t?"":document.referrer,c=!t&&(r===""||_t.test(r))&&(C=Q())!=null?C:r,s=Z(),a=W(),u=t?void 0:(D=z(window.location.search))!=null?D:V();return {page:{path:n,url:o,title:i,referrer:c},device:L(p({},s),{type:a}),utm:u,locale:t?"en":navigator.language,timezone:t?"UTC":(N=Intl.DateTimeFormat().resolvedOptions().timeZone)!=null?N:"UTC",sdkVersion:"0.2.4"}}function tt(e){if(typeof window=="undefined")return ()=>{};let t=history.pushState.bind(history),n=history.replaceState.bind(history);return history.pushState=function(...o){t(...o),e();},history.replaceState=function(...o){n(...o);},window.addEventListener("popstate",e),()=>{history.pushState=t,history.replaceState=n,window.removeEventListener("popstate",e);}}var Tt={signup:[/\/(sign-?up|register|join|create-?account|get-?started|onboard|registro|inscription|s-?inscrire|anmeldung|registrieren|cadastro|registrazione|aanmelden|inschrijven)/i],login:[/\/(log-?in|sign-?in|auth(\/login)?|iniciar-?sesion|connexion|se-?connecter|anmelden|einloggen|entrar|accesso|accedi|inloggen)/i],pricing:[/\/(pricing|plans|upgrade|subscribe|precios|tarifs|preise|precos|prezzi|prijzen)/i],checkout:[/\/(checkout|cart|basket|pay(ment)?|order|comprar|pagar|panier|caisse|warenkorb|kasse|carrinho|carrello|winkelwagen)/i],success:[/\/(success|thank-?you|thanks|confirmation|confirmed|complete[d]?|gracias|merci|danke|obrigado|grazie|bedankt)/i],onboarding:[/\/(onboarding|welcome|getting-?started|setup|bienvenido|bienvenue|willkommen|benvenuto|welkom)/i],contact:[/\/(contact|contacto|kontakt|contato|contatto|nous-?contacter)/i]},et={signup:"signup_start",login:"login_view",pricing:"pricing_view",checkout:"checkout_view",success:"success_view",onboarding:"onboarding_view",contact:"contact_view"};function kt(e,t){return e instanceof RegExp?e.test(t):t.includes(e)}function nt(e,t){var n;for(let o of Object.keys(et))if(((n=t==null?void 0:t[o])!=null?n:Tt[o]).some(r=>kt(r,e)))return et[o];return null}function ot(e){return e==="signup_start"?"signup_submit":e==="login_view"?"login_submit":e==="checkout_view"?"checkout_submit":null}function it(e,t){if(typeof document=="undefined")return ()=>{};let n=o=>{o.target instanceof HTMLFormElement&&t(e);};return document.addEventListener("submit",n,true),()=>document.removeEventListener("submit",n,true)}var R="ib_identified";function rt(){return typeof localStorage=="undefined"?true:!localStorage.getItem(R)}function st(){typeof localStorage!="undefined"&&localStorage.setItem(R,"1");}function at(){typeof localStorage!="undefined"&&localStorage.removeItem(R);}var ct="data-ib-event",ut="data-ib-";function lt(e){if(typeof document=="undefined")return ()=>{};let t=n=>{let o=n.target;for(;o&&o!==document.body;){let i=o.getAttribute(ct);if(i){let r={auto:true};for(let c of o.attributes)if(c.name.startsWith(ut)&&c.name!==ct){let s=c.name.slice(ut.length);r[s]=c.value;}e(i,r);return}o=o.parentElement;}};return document.addEventListener("click",t,true),()=>document.removeEventListener("click",t,true)}function dt(e){if(typeof document=="undefined")return ()=>{};let t=n=>{let o=n.target;for(;o&&o!==document.body;){if(o.tagName==="A"){let i=o.href;if(!i||!/^https?:\/\//.test(i))return;try{let r=new URL(i);r.hostname!==window.location.hostname&&e("outbound_click",{auto:!0,url:i,hostname:r.hostname});}catch(r){}return}o=o.parentElement;}};return document.addEventListener("click",t,true),()=>document.removeEventListener("click",t,true)}var y=class e{constructor(t){this.cleanupHistory=null;this.cleanupAttributes=null;this.cleanupOutbound=null;this.cleanupFormSubmit=null;if(this.config=t,this.queue=new v(t),typeof window!="undefined"&&(K(window.location.search,document.referrer),t.autoDetect!==false)){let n=typeof t.autoDetect=="object"?t.autoDetect:{};n.dataAttributes!==false&&(this.cleanupAttributes=lt((o,i)=>this.track(o,i))),n.outboundClicks!==false&&(this.cleanupOutbound=dt((o,i)=>this.track(o,i)));}}static init(t){let n=M(t);return new e(n)}page(t){var o;if(!f(this.config))return;let n=k(t);if(this.queue.push(this.buildEvent("page",{context:n})),this.config.autoDetect!==false){let i=n.page.path,r=typeof this.config.autoDetect=="object"?this.config.autoDetect:{},c={signup:r.signupPaths,login:r.loginPaths,pricing:r.pricingPaths,checkout:r.checkoutPaths,success:r.successPaths,onboarding:r.onboardingPaths,contact:r.contactPaths},s=nt(i,c);if(s&&(this.queue.push(this.buildEvent("track",{event:s,properties:{auto:true},context:n})),this.config.debug&&console.log(`[IndieBob] auto-detected: ${s} on ${i}`),r.formSubmit!==false)){(o=this.cleanupFormSubmit)==null||o.call(this);let a=ot(s);a&&(this.cleanupFormSubmit=it(a,u=>{this.queue.push(this.buildEvent("track",{event:u,properties:{auto:true}})),this.config.debug&&console.log(`[IndieBob] auto-detected: ${u}`);}));}}}track(t,n){f(this.config)&&this.queue.push(this.buildEvent("track",{event:t,properties:n}));}identify(t,n,o){if(f(this.config)&&(this.userId=t,this.queue.push(this.buildEvent("identify",{traits:n,userId:t})),this.config.autoDetect!==false)){let r=(typeof(o==null?void 0:o.isNew)=="boolean"?o.isNew:rt())?"signup":"login";this.queue.push(this.buildEvent("track",{event:r,properties:{auto:typeof(o==null?void 0:o.isNew)!="boolean"}})),st(),this.config.debug&&console.log(`[IndieBob] ${r} for ${t}`);}}revenue(t){var n;f(this.config)&&this.queue.push(this.buildEvent("revenue",{revenue:{amount:t.amount,currency:(n=t.currency)!=null?n:"USD",plan:t.plan,type:t.type}}));}optOut(){var t,n,o;this.queue.flush(),$(),at(),this.userId=void 0,(t=this.cleanupAttributes)==null||t.call(this),(n=this.cleanupOutbound)==null||n.call(this),(o=this.cleanupFormSubmit)==null||o.call(this),this.cleanupAttributes=null,this.cleanupOutbound=null,this.cleanupFormSubmit=null,this.config.debug&&console.log("[IndieBob] opted out");}optIn(){J(),this.config.debug&&console.log("[IndieBob] opted in");}isOptedOut(){return G()}flush(){return l(this,null,function*(){yield this.queue.flush();})}getAnonymousId(){return I()}getSessionId(){return w()}getConfig(){return Object.freeze(p({},this.config))}enableAutoPage(){var t;return (t=this.cleanupHistory)==null||t.call(this),this.cleanupHistory=tt(()=>this.page()),()=>{var n;(n=this.cleanupHistory)==null||n.call(this),this.cleanupHistory=null;}}buildEvent(t,n){var o,i;return {v:1,projectId:this.config.projectId,type:t,anonymousId:I(),userId:(o=this.userId)!=null?o:n.userId,sessionId:w(),timestamp:new Date().toISOString(),context:(i=n.context)!=null?i:k(),event:n.event,properties:n.properties,traits:n.traits,revenue:n.revenue}}};var Rt=Symbol("indiebob"),O=null,de={install(e,t){let n=y.init(t);O=n,e.provide(Rt,n),e.config.globalProperties.$bob=n;let o=e.config.globalProperties.$router;o&&o.afterEach(()=>{n.page();});}};function fe(){if(O)return O;throw new Error("[IndieBob] useTracker() called before IndieBobPlugin was installed.")}
2
- exports.IndieBob=y;exports.IndieBobPlugin=de;exports.useTracker=fe;
1
+ 'use strict';
2
+
3
+ // src/core/config.ts
4
+ var DEFAULT_ENDPOINT = "https://www.indiebob.com/api/collect";
5
+ var OPTIONS = "projectId endpoint debug respectDnt respectGpc autoDetect hashMode persistence";
6
+ var AUTO_OPTIONS = "signupPaths loginPaths pricingPaths checkoutPaths successPaths onboardingPaths contactPaths dataAttributes formSubmit outboundClicks fileDownloads";
7
+ function warnUnknown(obj, known, where) {
8
+ for (const k of Object.keys(obj)) {
9
+ if (!known.split(" ").includes(k)) console.warn(`[IndieBob] Unknown option ${where}"${k}" ignored`);
10
+ }
11
+ }
12
+ function resolveConfig(input) {
13
+ var _a, _b, _c, _d, _e, _f;
14
+ if (!input.projectId || !input.projectId.startsWith("proj_")) {
15
+ throw new Error(
16
+ `[IndieBob] Invalid projectId "${input.projectId}". Must start with "proj_".`
17
+ );
18
+ }
19
+ warnUnknown(input, OPTIONS, "");
20
+ if (typeof input.autoDetect === "object") warnUnknown(input.autoDetect, AUTO_OPTIONS, "autoDetect.");
21
+ if (input.persistence !== void 0 && input.persistence !== "local" && input.persistence !== "none") {
22
+ console.warn(`[IndieBob] persistence "${String(input.persistence)}" is not 'none' or 'local'; using 'none'`);
23
+ }
24
+ return {
25
+ projectId: input.projectId,
26
+ endpoint: (_a = input.endpoint) != null ? _a : DEFAULT_ENDPOINT,
27
+ debug: (_b = input.debug) != null ? _b : false,
28
+ respectDnt: (_c = input.respectDnt) != null ? _c : true,
29
+ respectGpc: (_d = input.respectGpc) != null ? _d : true,
30
+ autoDetect: (_e = input.autoDetect) != null ? _e : true,
31
+ hashMode: (_f = input.hashMode) != null ? _f : false,
32
+ persistence: input.persistence === "local" ? "local" : "none"
33
+ };
34
+ }
35
+
36
+ // src/core/identity.ts
37
+ var ANON_KEY = "_ib_anon";
38
+ function generateId() {
39
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
40
+ const bytes = new Uint8Array(21);
41
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(bytes);
42
+ else for (let i = 0; i < 21; i++) bytes[i] = Math.random() * 64;
43
+ let id = "";
44
+ for (const b of bytes) id += chars[b & 63];
45
+ return id;
46
+ }
47
+ function getAnonymousId() {
48
+ try {
49
+ let id = localStorage.getItem(ANON_KEY);
50
+ if (!id) {
51
+ id = generateId();
52
+ localStorage.setItem(ANON_KEY, id);
53
+ }
54
+ return id;
55
+ } catch (e) {
56
+ try {
57
+ let id = sessionStorage.getItem(ANON_KEY);
58
+ if (!id) {
59
+ id = generateId();
60
+ sessionStorage.setItem(ANON_KEY, id);
61
+ }
62
+ return id;
63
+ } catch (e2) {
64
+ return generateId();
65
+ }
66
+ }
67
+ }
68
+ function clearAnonymousId() {
69
+ try {
70
+ localStorage.removeItem(ANON_KEY);
71
+ } catch (e) {
72
+ }
73
+ try {
74
+ sessionStorage.removeItem(ANON_KEY);
75
+ } catch (e) {
76
+ }
77
+ }
78
+
79
+ // src/core/session.ts
80
+ var SESSION_KEY = "_ib_session";
81
+ var SESSION_LAST_KEY = "_ib_session_last";
82
+ var INACTIVITY_MS = 30 * 60 * 1e3;
83
+ function generateSessionId() {
84
+ try {
85
+ return crypto.randomUUID();
86
+ } catch (e) {
87
+ return generateId();
88
+ }
89
+ }
90
+ function getSessionId() {
91
+ var _a;
92
+ try {
93
+ const now = Date.now();
94
+ const lastActivity = parseInt((_a = sessionStorage.getItem(SESSION_LAST_KEY)) != null ? _a : "0", 10);
95
+ let sessionId = sessionStorage.getItem(SESSION_KEY);
96
+ const expired = lastActivity > 0 && now - lastActivity > INACTIVITY_MS;
97
+ if (!sessionId || expired) {
98
+ sessionId = generateSessionId();
99
+ sessionStorage.setItem(SESSION_KEY, sessionId);
100
+ }
101
+ sessionStorage.setItem(SESSION_LAST_KEY, String(now));
102
+ return sessionId;
103
+ } catch (e) {
104
+ return generateSessionId();
105
+ }
106
+ }
107
+ function clearSession() {
108
+ try {
109
+ sessionStorage.removeItem(SESSION_KEY);
110
+ } catch (e) {
111
+ }
112
+ try {
113
+ sessionStorage.removeItem(SESSION_LAST_KEY);
114
+ } catch (e) {
115
+ }
116
+ }
117
+
118
+ // src/core/attribution.ts
119
+ var FIRST_UTM_KEY = "_ib_first_utm";
120
+ var LAST_UTM_KEY = "_ib_last_utm";
121
+ var REFERRER_KEY = "_ib_referrer";
122
+ function parseUtm(search) {
123
+ const params = new URLSearchParams(search);
124
+ const utm = {};
125
+ let hasAny = false;
126
+ if (!params.has("utm_source") && params.get("ref")) {
127
+ utm.source = params.get("ref");
128
+ hasAny = true;
129
+ }
130
+ for (const key of ["source", "medium", "campaign", "term", "content"]) {
131
+ if (params.has("utm_" + key)) {
132
+ utm[key] = params.get("utm_" + key);
133
+ hasAny = true;
134
+ }
135
+ }
136
+ return hasAny ? utm : null;
137
+ }
138
+ function initAttribution(search, referrer) {
139
+ const utm = parseUtm(search);
140
+ try {
141
+ if (utm && !localStorage.getItem(FIRST_UTM_KEY)) {
142
+ localStorage.setItem(FIRST_UTM_KEY, JSON.stringify(utm));
143
+ }
144
+ if (utm) {
145
+ localStorage.setItem(LAST_UTM_KEY, JSON.stringify(utm));
146
+ }
147
+ if (referrer && !localStorage.getItem(REFERRER_KEY)) {
148
+ localStorage.setItem(REFERRER_KEY, referrer);
149
+ }
150
+ } catch (e) {
151
+ }
152
+ }
153
+ function getStoredFirstUtm() {
154
+ try {
155
+ const raw = localStorage.getItem(FIRST_UTM_KEY);
156
+ return raw ? JSON.parse(raw) : void 0;
157
+ } catch (e) {
158
+ return void 0;
159
+ }
160
+ }
161
+ function getStoredReferrer() {
162
+ var _a;
163
+ try {
164
+ return (_a = localStorage.getItem(REFERRER_KEY)) != null ? _a : void 0;
165
+ } catch (e) {
166
+ return void 0;
167
+ }
168
+ }
169
+ function clearAttribution() {
170
+ try {
171
+ localStorage.removeItem(FIRST_UTM_KEY);
172
+ } catch (e) {
173
+ }
174
+ try {
175
+ localStorage.removeItem(LAST_UTM_KEY);
176
+ } catch (e) {
177
+ }
178
+ try {
179
+ localStorage.removeItem(REFERRER_KEY);
180
+ } catch (e) {
181
+ }
182
+ }
183
+
184
+ // src/core/exclude.ts
185
+ var IGNORE_KEY = "ib_ignore";
186
+ function applyIgnore(search, store) {
187
+ const p = new URLSearchParams(search).get(IGNORE_KEY);
188
+ if (p === "1") store.setItem(IGNORE_KEY, "1");
189
+ else if (p === "0") store.removeItem(IGNORE_KEY);
190
+ return store.getItem(IGNORE_KEY) === "1";
191
+ }
192
+ function isIgnored() {
193
+ try {
194
+ return localStorage.getItem(IGNORE_KEY) === "1";
195
+ } catch (e) {
196
+ return false;
197
+ }
198
+ }
199
+ function isExcluded() {
200
+ return typeof navigator !== "undefined" && navigator.webdriver === true || isIgnored();
201
+ }
202
+
203
+ // src/core/consent.ts
204
+ var OPT_OUT_KEY = "_ib_optout";
205
+ var OFFLINE_QUEUE_KEY = "_ib_queue";
206
+ function isTrackingAllowed(config) {
207
+ if (isExcluded()) return false;
208
+ try {
209
+ if (localStorage.getItem(OPT_OUT_KEY)) return false;
210
+ } catch (e) {
211
+ }
212
+ if (config.respectDnt && typeof navigator !== "undefined") {
213
+ if (navigator.doNotTrack === "1" || navigator.msDoNotTrack === "1") {
214
+ return false;
215
+ }
216
+ }
217
+ if (config.respectGpc && typeof navigator !== "undefined") {
218
+ if (navigator.globalPrivacyControl === true) {
219
+ return false;
220
+ }
221
+ }
222
+ return true;
223
+ }
224
+ function optOut() {
225
+ try {
226
+ localStorage.setItem(OPT_OUT_KEY, "1");
227
+ } catch (e) {
228
+ }
229
+ clearAnonymousId();
230
+ clearSession();
231
+ clearAttribution();
232
+ try {
233
+ localStorage.removeItem(OFFLINE_QUEUE_KEY);
234
+ } catch (e) {
235
+ }
236
+ }
237
+ function optIn() {
238
+ try {
239
+ localStorage.removeItem(OPT_OUT_KEY);
240
+ } catch (e) {
241
+ }
242
+ }
243
+ function isOptedOut() {
244
+ try {
245
+ return !!localStorage.getItem(OPT_OUT_KEY);
246
+ } catch (e) {
247
+ return false;
248
+ }
249
+ }
250
+
251
+ // src/core/transport.ts
252
+ var MAX_RETRIES = 3;
253
+ function log(config, ...args) {
254
+ if (config.debug) console.log("[IndieBob]", ...args);
255
+ }
256
+ async function sendBatch(payload, config, isUnloading = false) {
257
+ const body = JSON.stringify(payload);
258
+ if (isUnloading && typeof navigator !== "undefined" && navigator.sendBeacon) {
259
+ const blob = new Blob([body], { type: "text/plain;charset=UTF-8" });
260
+ const ok = navigator.sendBeacon(config.endpoint, blob);
261
+ log(config, "sendBeacon:", ok, payload.batch.length, "events");
262
+ return ok;
263
+ }
264
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
265
+ try {
266
+ const res = await fetch(config.endpoint, {
267
+ method: "POST",
268
+ headers: { "Content-Type": "application/json" },
269
+ body,
270
+ keepalive: true
271
+ });
272
+ if (res.ok) {
273
+ log(config, "sent:", payload.batch.length, "events");
274
+ return true;
275
+ }
276
+ if (res.status >= 400 && res.status < 500) {
277
+ log(config, "4xx error, not retrying:", res.status);
278
+ return false;
279
+ }
280
+ } catch (err) {
281
+ log(config, "fetch error (attempt", attempt + 1, "):", err);
282
+ }
283
+ if (attempt < MAX_RETRIES - 1) {
284
+ await new Promise((resolve) => setTimeout(resolve, 1e3 << attempt));
285
+ }
286
+ }
287
+ log(config, "failed to send after", MAX_RETRIES, "attempts");
288
+ return false;
289
+ }
290
+
291
+ // src/core/queue.ts
292
+ var OFFLINE_KEY = "_ib_queue";
293
+ var MAX_BATCH = 10;
294
+ var FLUSH_INTERVAL_MS = 5e3;
295
+ var EventQueue = class {
296
+ /**
297
+ * @param beforeHide runs right before the hide flush, so an event that
298
+ * describes the page being left (engagement) rides the same beacon.
299
+ */
300
+ constructor(config, beforeHide) {
301
+ this.b = [];
302
+ /** Failed events awaiting a retry, when nothing may be stored ('none'). */
303
+ this.j = [];
304
+ this.k = false;
305
+ this.a = config;
306
+ setInterval(() => {
307
+ if (this.b.length > 0) void this.flush();
308
+ }, FLUSH_INTERVAL_MS);
309
+ if (typeof window === "undefined") return;
310
+ const hide = () => {
311
+ beforeHide == null ? void 0 : beforeHide();
312
+ void this.flush(true);
313
+ };
314
+ document.addEventListener("visibilitychange", () => {
315
+ if (document.visibilityState === "hidden") hide();
316
+ });
317
+ window.addEventListener("pagehide", hide);
318
+ window.addEventListener("online", () => {
319
+ void this.o();
320
+ });
321
+ if (navigator.onLine) {
322
+ void this.o();
323
+ }
324
+ }
325
+ push(event) {
326
+ this.b.push(event);
327
+ if (this.b.length >= MAX_BATCH) {
328
+ void this.flush();
329
+ }
330
+ }
331
+ async flush(isUnloading = false) {
332
+ if (this.k && !isUnloading) return;
333
+ if (this.b.length === 0) return;
334
+ this.k = true;
335
+ const batch = this.b.splice(0);
336
+ const payload = { batch };
337
+ const ok = await sendBatch(payload, this.a, isUnloading);
338
+ if (!ok) {
339
+ this.t([...this.p(), ...batch].slice(-50));
340
+ }
341
+ this.k = false;
342
+ }
343
+ async o() {
344
+ const stored = this.p();
345
+ if (stored.length === 0) return;
346
+ this.clearStored();
347
+ const payload = { batch: stored };
348
+ await sendBatch(payload, this.a);
349
+ }
350
+ /** Give events still waiting the ids they lacked (storage was just allowed). */
351
+ stamp(anonymousId, sessionId) {
352
+ for (const e of this.b) if (!e.anonymousId) Object.assign(e, { anonymousId, sessionId });
353
+ }
354
+ /** Drop the retry queue, wherever it is kept. */
355
+ clearStored() {
356
+ this.j = [];
357
+ try {
358
+ localStorage.removeItem(OFFLINE_KEY);
359
+ } catch (e) {
360
+ }
361
+ }
362
+ /** In localStorage only with persistence 'local'; in memory otherwise. */
363
+ t(events) {
364
+ if (this.a.persistence !== "local") {
365
+ this.j = events;
366
+ return;
367
+ }
368
+ try {
369
+ localStorage.setItem(OFFLINE_KEY, JSON.stringify(events));
370
+ } catch (e) {
371
+ }
372
+ }
373
+ p() {
374
+ if (this.a.persistence !== "local") return this.j;
375
+ try {
376
+ const raw = localStorage.getItem(OFFLINE_KEY);
377
+ if (!raw) return [];
378
+ return JSON.parse(raw);
379
+ } catch (e) {
380
+ return [];
381
+ }
382
+ }
383
+ };
384
+
385
+ // src/core/url.ts
386
+ var KEEP = /^(utm_\w+|ref)$/;
387
+ function cleanSearch(search) {
388
+ const out = new URLSearchParams();
389
+ new URLSearchParams(search).forEach((v, k) => {
390
+ if (KEEP.test(k)) out.append(k, v);
391
+ });
392
+ const s = out.toString();
393
+ return s ? "?" + s : "";
394
+ }
395
+ function cleanHash(hash) {
396
+ const route = hash.split("?")[0];
397
+ return route.includes("=") ? "" : route;
398
+ }
399
+ function cleanUrl(href, hashMode) {
400
+ try {
401
+ const u = new URL(href);
402
+ return u.origin + u.pathname + cleanSearch(u.search) + (hashMode ? cleanHash(u.hash) : "");
403
+ } catch (e) {
404
+ return "";
405
+ }
406
+ }
407
+ function cleanReferrer(ref) {
408
+ try {
409
+ const u = new URL(ref);
410
+ return u.origin + u.pathname;
411
+ } catch (e) {
412
+ return "";
413
+ }
414
+ }
415
+
416
+ // src/collectors/page.ts
417
+ function pagePath(loc, hashMode) {
418
+ return loc.pathname + cleanSearch(loc.search) + (hashMode ? cleanHash(loc.hash) : "");
419
+ }
420
+ function patchHistory(onNavigate, hashMode) {
421
+ if (typeof window === "undefined") return () => {
422
+ };
423
+ const originalPushState = history.pushState.bind(history);
424
+ const originalReplaceState = history.replaceState.bind(history);
425
+ history.pushState = function(...args) {
426
+ originalPushState(...args);
427
+ onNavigate();
428
+ };
429
+ history.replaceState = function(...args) {
430
+ originalReplaceState(...args);
431
+ };
432
+ window.addEventListener("popstate", onNavigate);
433
+ if (hashMode) window.addEventListener("hashchange", onNavigate);
434
+ return () => {
435
+ history.pushState = originalPushState;
436
+ history.replaceState = originalReplaceState;
437
+ window.removeEventListener("popstate", onNavigate);
438
+ window.removeEventListener("hashchange", onNavigate);
439
+ };
440
+ }
441
+
442
+ // src/collectors/context.ts
443
+ var PAYMENT_HOST = /^https?:\/\/[^/]*(stripe\.com|paypal\.com|paddle\.com|lemonsqueezy\.com|gumroad\.com)(\/|$)/i;
444
+ function buildContext(overrides, hashMode, local) {
445
+ var _a, _b, _c, _d, _e, _f, _g, _h;
446
+ const sdkVersion = "0.4.1" ;
447
+ if (typeof window === "undefined") {
448
+ return {
449
+ page: { path: (_a = overrides == null ? void 0 : overrides.path) != null ? _a : "", url: "", title: (_b = overrides == null ? void 0 : overrides.title) != null ? _b : "", referrer: (_c = overrides == null ? void 0 : overrides.referrer) != null ? _c : "" },
450
+ device: { width: 0, height: 0, type: "desktop" },
451
+ locale: "en",
452
+ timezone: "UTC",
453
+ sdkVersion
454
+ };
455
+ }
456
+ const liveReferrer = (_d = overrides == null ? void 0 : overrides.referrer) != null ? _d : document.referrer;
457
+ const width = innerWidth;
458
+ return {
459
+ page: {
460
+ path: (_e = overrides == null ? void 0 : overrides.path) != null ? _e : pagePath(location, hashMode),
461
+ url: cleanUrl(location.href, hashMode),
462
+ title: (_f = overrides == null ? void 0 : overrides.title) != null ? _f : document.title,
463
+ // First touch survives the round trip: a /thanks page reached from Stripe
464
+ // reports the referrer the visit STARTED with (stored with 'local'), and
465
+ // never checkout.stripe.com — otherwise every sale files itself under the
466
+ // payment host. Without storage the server's visit already holds it.
467
+ referrer: cleanReferrer(liveReferrer === "" || PAYMENT_HOST.test(liveReferrer) ? local && getStoredReferrer() || "" : liveReferrer)
468
+ },
469
+ // Device class from the viewport width; still required by /api/collect
470
+ device: { width, height: innerHeight, type: width <= 768 ? "mobile" : width <= 1024 ? "tablet" : "desktop" },
471
+ utm: (_g = parseUtm(location.search)) != null ? _g : local ? getStoredFirstUtm() : void 0,
472
+ locale: navigator.language,
473
+ timezone: (_h = Intl.DateTimeFormat().resolvedOptions().timeZone) != null ? _h : "UTC",
474
+ sdkVersion
475
+ };
476
+ }
477
+
478
+ // src/collectors/intent.ts
479
+ var INTENTS = [
480
+ [
481
+ "signup",
482
+ "signup_start",
483
+ /\/(sign-?up|register|join|create-?account|get-?started|onboard|registro|inscription|s-?inscrire|anmeldung|registrieren|cadastro|registrazione|aanmelden|inschrijven)/i
484
+ ],
485
+ [
486
+ "login",
487
+ "login_view",
488
+ /\/(log-?in|sign-?in|auth(\/login)?|iniciar-?sesion|connexion|se-?connecter|anmelden|einloggen|entrar|accesso|accedi|inloggen)/i
489
+ ],
490
+ [
491
+ "pricing",
492
+ "pricing_view",
493
+ /\/(pricing|plans|upgrade|subscribe|precios|tarifs|preise|precos|prezzi|prijzen)/i
494
+ ],
495
+ [
496
+ "checkout",
497
+ "checkout_view",
498
+ /\/(checkout|cart|basket|pay(ment)?|order|comprar|pagar|panier|caisse|warenkorb|kasse|carrinho|carrello|winkelwagen)/i
499
+ ],
500
+ [
501
+ "success",
502
+ "success_view",
503
+ /\/(success|thank-?you|thanks|confirmation|confirmed|complete[d]?|gracias|merci|danke|obrigado|grazie|bedankt)/i
504
+ ],
505
+ [
506
+ "onboarding",
507
+ "onboarding_view",
508
+ /\/(onboarding|welcome|getting-?started|setup|bienvenido|bienvenue|willkommen|benvenuto|welkom)/i
509
+ ],
510
+ [
511
+ "contact",
512
+ "contact_view",
513
+ /\/(contact|contacto|kontakt|contato|contatto|nous-?contacter)/i
514
+ ]
515
+ ];
516
+ function matchPattern(pattern, path) {
517
+ if (pattern instanceof RegExp) return pattern.test(path);
518
+ return path.includes(pattern);
519
+ }
520
+ function detectIntent(path, custom) {
521
+ var _a;
522
+ for (const [category, event, pattern] of INTENTS) {
523
+ const patterns = (_a = custom == null ? void 0 : custom[`${category}Paths`]) != null ? _a : [pattern];
524
+ if (patterns.some((p) => matchPattern(p, path))) return event;
525
+ }
526
+ return null;
527
+ }
528
+ function intentToSubmitEvent(intent) {
529
+ const m = /^(signup|login|checkout)_/.exec(intent);
530
+ return m ? m[1] + "_submit" : null;
531
+ }
532
+ function observeFormSubmits(submitEventName, onSubmit) {
533
+ if (typeof document === "undefined") return () => {
534
+ };
535
+ const handler = (e) => {
536
+ if (e.target instanceof HTMLFormElement) {
537
+ onSubmit(submitEventName);
538
+ }
539
+ };
540
+ document.addEventListener("submit", handler, true);
541
+ return () => document.removeEventListener("submit", handler, true);
542
+ }
543
+ var IDENTIFIED_KEY = "ib_identified";
544
+ function isFirstIdentify() {
545
+ if (typeof localStorage === "undefined") return true;
546
+ return !localStorage.getItem(IDENTIFIED_KEY);
547
+ }
548
+ function markIdentified() {
549
+ if (typeof localStorage === "undefined") return;
550
+ localStorage.setItem(IDENTIFIED_KEY, "1");
551
+ }
552
+ function clearIdentified() {
553
+ if (typeof localStorage === "undefined") return;
554
+ localStorage.removeItem(IDENTIFIED_KEY);
555
+ }
556
+
557
+ // src/collectors/attributes.ts
558
+ var ATTR_EVENT = "data-ib-event";
559
+ var ATTR_PREFIX = "data-ib-";
560
+ function attributeEvent(el) {
561
+ while (el && el !== document.body) {
562
+ const eventName = el.getAttribute(ATTR_EVENT);
563
+ if (eventName) {
564
+ const props = { auto: true };
565
+ for (const attr of el.attributes) {
566
+ if (attr.name.startsWith(ATTR_PREFIX) && attr.name !== ATTR_EVENT) {
567
+ props[attr.name.slice(ATTR_PREFIX.length)] = attr.value;
568
+ }
569
+ }
570
+ return [eventName, props];
571
+ }
572
+ el = el.parentElement;
573
+ }
574
+ return null;
575
+ }
576
+
577
+ // src/collectors/links.ts
578
+ var DOWNLOAD_EXT = /\.(pdf|zip|dmg|exe|msi|pkg|apk|ipa|deb|rpm|appimage|t?gz|bz2|xz|tar|7z|rar|csv|xlsx?|docx?|pptx?|txt|epub|mp3|wav|mp4|mov|iso)$/i;
579
+ function downloadExt(pathname) {
580
+ const m = DOWNLOAD_EXT.exec(pathname);
581
+ return m ? m[1].toLowerCase() : null;
582
+ }
583
+ function classifyLink(href, host, downloads, outbound) {
584
+ if (!/^https?:\/\//i.test(href)) return null;
585
+ try {
586
+ const u = new URL(href);
587
+ const ext = downloads && downloadExt(u.pathname);
588
+ if (ext) return ["file_download", { auto: true, url: u.origin + u.pathname, path: u.pathname, ext }];
589
+ if (outbound && u.hostname !== host) return ["outbound_click", { auto: true, url: href, hostname: u.hostname }];
590
+ } catch (e) {
591
+ }
592
+ return null;
593
+ }
594
+ function initClickTracking(onTrack, attrs, downloads, outbound) {
595
+ if (typeof document === "undefined") return () => {
596
+ };
597
+ const handler = (e) => {
598
+ var _a;
599
+ const el = e.target;
600
+ const a = (_a = el == null ? void 0 : el.closest) == null ? void 0 : _a.call(el, "a");
601
+ for (const hit of [
602
+ attrs && attributeEvent(el),
603
+ a && classifyLink(a.href, location.hostname, downloads, outbound)
604
+ ]) {
605
+ if (hit) onTrack(hit[0], hit[1]);
606
+ }
607
+ };
608
+ document.addEventListener("click", handler, true);
609
+ return () => document.removeEventListener("click", handler, true);
610
+ }
611
+
612
+ // src/collectors/engagement.ts
613
+ var ENGAGEMENT_MIN_MS = 3e3;
614
+ function shouldSendEngagement(ms, scroll, lastSentScroll) {
615
+ return scroll > lastSentScroll || ms >= ENGAGEMENT_MIN_MS;
616
+ }
617
+ function scrollPercent(top, viewport, docHeight) {
618
+ return docHeight > 0 ? Math.min(100, Math.round((top + viewport) / docHeight * 100)) : 0;
619
+ }
620
+ function trackEngagement() {
621
+ let ms = 0, since = 0, scroll = 0, sent = 0;
622
+ const tick = () => {
623
+ const now = Date.now();
624
+ if (since) ms += now - since;
625
+ since = document.visibilityState === "visible" && document.hasFocus() ? now : 0;
626
+ };
627
+ const sample = () => {
628
+ var _a, _b;
629
+ scroll = Math.max(scroll, scrollPercent(
630
+ scrollY,
631
+ innerHeight,
632
+ Math.max(document.documentElement.scrollHeight, (_b = (_a = document.body) == null ? void 0 : _a.scrollHeight) != null ? _b : 0)
633
+ ));
634
+ };
635
+ const reset = () => {
636
+ ms = scroll = sent = since = 0;
637
+ tick();
638
+ };
639
+ for (const t of ["focus", "blur"]) window.addEventListener(t, tick);
640
+ document.addEventListener("visibilitychange", tick);
641
+ window.addEventListener("scroll", sample);
642
+ reset();
643
+ return {
644
+ u: sample,
645
+ w: reset,
646
+ x(measure) {
647
+ if (measure) sample();
648
+ tick();
649
+ if (!shouldSendEngagement(ms, scroll, sent)) return null;
650
+ const r = { ms: Math.min(ms, 864e5), scroll };
651
+ ms = 0;
652
+ sent = scroll;
653
+ return r;
654
+ }
655
+ };
656
+ }
657
+
658
+ // src/index.ts
659
+ var instances = {};
660
+ function unseen() {
661
+ return typeof document !== "undefined" && (document.visibilityState === "hidden" || document.prerendering === true);
662
+ }
663
+ var IndieBob = class _IndieBob {
664
+ constructor(config) {
665
+ this.g = null;
666
+ this.m = null;
667
+ this.h = null;
668
+ /** Engagement meter for the current page (browser only). */
669
+ this.e = null;
670
+ /** Context of the page last counted — what an engagement event describes. */
671
+ this.n = null;
672
+ /** A first pageview waiting for the tab to become visible. */
673
+ this.i = false;
674
+ this.a = config;
675
+ this.b = new EventQueue(config, () => this.s(true));
676
+ if (typeof window !== "undefined") {
677
+ try {
678
+ if (applyIgnore(window.location.search, localStorage)) {
679
+ console.info("IndieBob: this browser is excluded from analytics (ib_ignore)");
680
+ }
681
+ } catch (e) {
682
+ }
683
+ if (this.c && isTrackingAllowed(config)) initAttribution(window.location.search, document.referrer);
684
+ this.e = trackEngagement();
685
+ window.addEventListener("pageshow", (e) => {
686
+ if (e.persisted && this.f !== void 0) {
687
+ this.f = void 0;
688
+ this.page(this.q);
689
+ }
690
+ });
691
+ if (config.autoDetect !== false) {
692
+ const opts = typeof config.autoDetect === "object" ? config.autoDetect : {};
693
+ this.m = initClickTracking(
694
+ (event, properties) => this.track(event, properties),
695
+ opts.dataAttributes !== false,
696
+ opts.fileDownloads !== false,
697
+ opts.outboundClicks !== false
698
+ );
699
+ }
700
+ }
701
+ }
702
+ /**
703
+ * Create a tracker instance. Call once at app startup.
704
+ *
705
+ * In the browser, calling it again for the same projectId returns the
706
+ * instance already running (its first config wins) — so a `data-project`
707
+ * script tag plus a manual init() never doubles the events.
708
+ *
709
+ * @example
710
+ * const bob = IndieBob.init({ projectId: 'proj_your16charId' })
711
+ */
712
+ static init(config) {
713
+ var _a, _b;
714
+ const resolved = resolveConfig(config);
715
+ return typeof window === "undefined" ? new _IndieBob(resolved) : (_b = instances[_a = resolved.projectId]) != null ? _b : instances[_a] = new _IndieBob(resolved);
716
+ }
717
+ /**
718
+ * Track a page view. Call on every page load and SPA navigation.
719
+ * Auto-detects signup/login/pricing pages and fires semantic events.
720
+ *
721
+ * @example
722
+ * bob.page()
723
+ * bob.page({ path: '/pricing' })
724
+ */
725
+ page(overrides) {
726
+ var _a;
727
+ if (!isTrackingAllowed(this.a)) return;
728
+ if (this.f === void 0 && unseen()) {
729
+ this.r = overrides;
730
+ if (!this.i) {
731
+ this.i = true;
732
+ const go = () => {
733
+ if (this.i && !unseen()) {
734
+ this.i = false;
735
+ this.page(this.r);
736
+ }
737
+ };
738
+ document.addEventListener("visibilitychange", go);
739
+ document.addEventListener("prerenderingchange", go);
740
+ }
741
+ return;
742
+ }
743
+ const ctx = buildContext(overrides, this.a.hashMode, this.c);
744
+ if (ctx.page.path === this.f) return;
745
+ this.s(false);
746
+ this.f = ctx.page.path;
747
+ this.q = overrides;
748
+ this.n = ctx;
749
+ if (this.e) {
750
+ this.e.w();
751
+ setTimeout(this.e.u, 1e3);
752
+ }
753
+ this.b.push(this.d("page", { context: ctx }));
754
+ if (this.a.autoDetect !== false) {
755
+ const path = ctx.page.path;
756
+ const opts = typeof this.a.autoDetect === "object" ? this.a.autoDetect : {};
757
+ const intent = detectIntent(path, opts);
758
+ if (intent) {
759
+ this.b.push(this.d("track", {
760
+ event: intent,
761
+ properties: { auto: true },
762
+ context: ctx
763
+ }));
764
+ log(this.a, `auto-detected: ${intent} on ${path}`);
765
+ if (opts.formSubmit !== false) {
766
+ (_a = this.h) == null ? void 0 : _a.call(this);
767
+ const submitEvent = intentToSubmitEvent(intent);
768
+ if (submitEvent) {
769
+ this.h = observeFormSubmits(submitEvent, (eventName) => {
770
+ this.b.push(this.d("track", {
771
+ event: eventName,
772
+ properties: { auto: true }
773
+ }));
774
+ log(this.a, `auto-detected: ${eventName}`);
775
+ });
776
+ }
777
+ }
778
+ }
779
+ }
780
+ }
781
+ /**
782
+ * Track a custom event.
783
+ *
784
+ * @example
785
+ * bob.track('signup_started')
786
+ * bob.track('feature_used', { feature: 'csv_export' })
787
+ */
788
+ track(event, properties) {
789
+ if (!isTrackingAllowed(this.a)) return;
790
+ this.b.push(this.d("track", { event, properties }));
791
+ }
792
+ /**
793
+ * Associate the current anonymous visitor with a known user ID.
794
+ * Call after login or signup.
795
+ *
796
+ * When autoDetect is enabled (default), this also fires:
797
+ * - 'signup' if this is the first time this visitor is identified (new user)
798
+ * - 'login' if this visitor was previously identified (returning user)
799
+ *
800
+ * PASS `isNew` WHEN YOUR SERVER KNOWS THE ANSWER. Without it the only
801
+ * evidence available is a localStorage flag this SDK wrote itself, and that
802
+ * flag cannot exist in a browser that has never run identify() before — so
803
+ * the FIRST identify in any browser looks like a signup. On a product that
804
+ * already has users, the day you install identify() every returning user
805
+ * files a phantom `signup` and the funnel's top step is wrong from then on.
806
+ * The server knows whether it just INSERTed a user row; tell us and the
807
+ * guess is skipped entirely.
808
+ *
809
+ * @example
810
+ * bob.identify('user_123', { plan: 'pro' }) // guess
811
+ * bob.identify('user_123', { plan: 'pro' }, { isNew: true }) // server truth
812
+ */
813
+ identify(userId, traits, options) {
814
+ if (!isTrackingAllowed(this.a)) return;
815
+ this.l = userId;
816
+ this.b.push(this.d("identify", { traits, userId }));
817
+ const known = typeof (options == null ? void 0 : options.isNew) === "boolean";
818
+ if (this.a.autoDetect !== false && (known || this.c)) {
819
+ const first = known ? options.isNew : isFirstIdentify();
820
+ const eventName = first ? "signup" : "login";
821
+ this.b.push(this.d("track", {
822
+ event: eventName,
823
+ // `auto` says where the verdict came from, so a funnel that looks
824
+ // wrong later can be traced to a guess rather than to the server.
825
+ properties: { auto: !known }
826
+ }));
827
+ if (this.c) markIdentified();
828
+ log(this.a, `${eventName} for ${userId}`);
829
+ }
830
+ }
831
+ /**
832
+ * Track a revenue event.
833
+ *
834
+ * @example
835
+ * bob.revenue({ amount: 29.00, currency: 'USD', plan: 'pro-monthly', type: 'new' })
836
+ */
837
+ revenue(data) {
838
+ var _a;
839
+ if (!isTrackingAllowed(this.a)) return;
840
+ this.b.push(this.d("revenue", {
841
+ revenue: {
842
+ amount: data.amount,
843
+ currency: (_a = data.currency) != null ? _a : "USD",
844
+ plan: data.plan,
845
+ type: data.type
846
+ }
847
+ }));
848
+ }
849
+ /**
850
+ * Stop all tracking and clear all stored identifiers.
851
+ * Respects user's privacy preferences.
852
+ */
853
+ optOut() {
854
+ var _a, _b;
855
+ void this.b.flush();
856
+ optOut();
857
+ clearIdentified();
858
+ this.l = void 0;
859
+ (_a = this.m) == null ? void 0 : _a.call(this);
860
+ (_b = this.h) == null ? void 0 : _b.call(this);
861
+ this.m = null;
862
+ this.h = null;
863
+ log(this.a, "opted out");
864
+ }
865
+ /** Re-enable tracking after optOut(). */
866
+ optIn() {
867
+ optIn();
868
+ log(this.a, "opted in");
869
+ }
870
+ /** Returns true if the user has explicitly opted out. */
871
+ isOptedOut() {
872
+ return isOptedOut();
873
+ }
874
+ /**
875
+ * Returns true if this browser excluded itself with `?ib_ignore=1`
876
+ * (the owner's own visits). `?ib_ignore=0` undoes it.
877
+ */
878
+ isIgnored() {
879
+ return isIgnored();
880
+ }
881
+ /** Force-send all queued events immediately. Useful in tests. */
882
+ async flush() {
883
+ await this.b.flush();
884
+ }
885
+ /** The stored anonymous ID with `persistence: 'local'`; '' without storage. */
886
+ getAnonymousId() {
887
+ return this.c ? getAnonymousId() : "";
888
+ }
889
+ /** The stored session ID with `persistence: 'local'`; '' without storage. */
890
+ getSessionId() {
891
+ return this.c ? getSessionId() : "";
892
+ }
893
+ /**
894
+ * Switch storage on or off — call it from your consent banner.
895
+ * `'local'` starts storing the visitor id and first-touch attribution from
896
+ * this page on; `'none'` removes everything the tracker stored.
897
+ *
898
+ * @example
899
+ * onConsent(() => bob.setPersistence('local'))
900
+ * onConsentWithdrawn(() => bob.setPersistence('none'))
901
+ */
902
+ setPersistence(mode) {
903
+ if (mode !== "local" && mode !== "none") return;
904
+ this.a.persistence = mode;
905
+ if (mode === "local") {
906
+ if (typeof window !== "undefined" && isTrackingAllowed(this.a)) {
907
+ initAttribution(location.search, document.referrer);
908
+ this.b.stamp(getAnonymousId(), getSessionId());
909
+ }
910
+ } else {
911
+ clearAnonymousId();
912
+ clearSession();
913
+ clearAttribution();
914
+ clearIdentified();
915
+ this.b.clearStored();
916
+ }
917
+ log(this.a, `persistence: ${mode}`);
918
+ }
919
+ get c() {
920
+ return this.a.persistence === "local";
921
+ }
922
+ /** Get the resolved configuration (read-only). */
923
+ getConfig() {
924
+ return Object.freeze({ ...this.a });
925
+ }
926
+ /**
927
+ * Enable auto-tracking of SPA navigation.
928
+ * Returns a cleanup function to remove the listeners.
929
+ *
930
+ * @example
931
+ * const cleanup = bob.enableAutoPage()
932
+ * // Later: cleanup()
933
+ */
934
+ enableAutoPage() {
935
+ var _a;
936
+ (_a = this.g) == null ? void 0 : _a.call(this);
937
+ this.g = patchHistory(() => this.page(), this.a.hashMode);
938
+ return () => {
939
+ var _a2;
940
+ (_a2 = this.g) == null ? void 0 : _a2.call(this);
941
+ this.g = null;
942
+ };
943
+ }
944
+ /**
945
+ * Queue an engagement event for the page last counted, if its reading is
946
+ * worth sending. `measure`: sample the scroll first (false on an SPA
947
+ * change, when the DOM already shows the next page).
948
+ */
949
+ s(measure) {
950
+ var _a;
951
+ const reading = this.n && ((_a = this.e) == null ? void 0 : _a.x(measure));
952
+ if (reading && isTrackingAllowed(this.a)) {
953
+ this.b.push(this.d("engagement", { context: this.n, engagement: reading }));
954
+ }
955
+ }
956
+ d(type, extras) {
957
+ var _a, _b;
958
+ return {
959
+ ...extras,
960
+ v: 1,
961
+ id: generateId(),
962
+ projectId: this.a.projectId,
963
+ type,
964
+ // Without storage the server derives both from its daily hash
965
+ ...this.c ? { anonymousId: getAnonymousId(), sessionId: getSessionId() } : {},
966
+ userId: (_a = this.l) != null ? _a : extras.userId,
967
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
968
+ context: (_b = extras.context) != null ? _b : buildContext(void 0, this.a.hashMode, this.c)
969
+ };
970
+ }
971
+ };
972
+
973
+ // src/integrations/vue.ts
974
+ var INJECT_KEY = /* @__PURE__ */ Symbol("indiebob");
975
+ var _instance = null;
976
+ var IndieBobPlugin = {
977
+ install(app, config) {
978
+ const bob = IndieBob.init(config);
979
+ _instance = bob;
980
+ app.provide(INJECT_KEY, bob);
981
+ app.config.globalProperties.$bob = bob;
982
+ const router = app.config.globalProperties.$router;
983
+ if (router) {
984
+ router.afterEach(() => {
985
+ bob.page();
986
+ });
987
+ }
988
+ }
989
+ };
990
+ function useTracker() {
991
+ if (_instance) return _instance;
992
+ throw new Error("[IndieBob] useTracker() called before IndieBobPlugin was installed.");
993
+ }
994
+
995
+ exports.IndieBob = IndieBob;
996
+ exports.IndieBobPlugin = IndieBobPlugin;
997
+ exports.useTracker = useTracker;