@pulsora/core 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/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # @pulsora/core
2
+
3
+ Privacy-first, cookieless analytics tracking library. Core package for Pulsora Analytics.
4
+
5
+ ## Features
6
+
7
+ - 🔒 **Privacy-First**: No cookies, no localStorage (by default)
8
+ - 🎯 **Lightweight**: <1KB gzipped
9
+ - 🚀 **High Performance**: Uses sendBeacon API, async processing
10
+ - 🔧 **Extensible**: Plugin system for additional features
11
+ - 📊 **Browser Fingerprinting**: Stable, anonymous visitor identification
12
+ - 🔄 **Retry Logic**: Automatic retry with exponential backoff
13
+ - 📱 **SPA Support**: Automatic pageview tracking on route changes
14
+ - 💪 **TypeScript**: Full type definitions included
15
+
16
+ ## Installation
17
+
18
+ ### NPM
19
+
20
+ ```bash
21
+ npm install @pulsora/core
22
+ ```
23
+
24
+ ### CDN
25
+
26
+ ```html
27
+ <script
28
+ async
29
+ src="https://cdn.pulsora.co/v1/pulsora.min.js"
30
+ data-token="YOUR_API_TOKEN"
31
+ ></script>
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### For NPM Users (Recommended for Apps)
37
+
38
+ Create and manage your own tracker instances:
39
+
40
+ ```typescript
41
+ import { Pulsora } from '@pulsora/core';
42
+
43
+ // Create a tracker instance
44
+ const tracker = new Pulsora();
45
+
46
+ // Initialize
47
+ tracker.init({
48
+ apiToken: 'your-api-token',
49
+ endpoint: 'https://api.pulsora.co/ingest', // optional
50
+ autoPageviews: true, // optional, default: true
51
+ debug: false, // optional, default: false
52
+ });
53
+
54
+ // Track pageview
55
+ tracker.pageview();
56
+
57
+ // Track custom event
58
+ tracker.event('button_click', {
59
+ button: 'signup',
60
+ location: 'header',
61
+ });
62
+
63
+ // Identify user
64
+ tracker.identify('user_123');
65
+
66
+ // Get visitor fingerprint (useful for server-side attribution)
67
+ const fingerprint = await tracker.getVisitorFingerprint();
68
+ const sessionId = tracker.getSessionId();
69
+ ```
70
+
71
+ #### Multiple Tracker Instances
72
+
73
+ Perfect for tracking multiple websites or separating different types of analytics:
74
+
75
+ ```typescript
76
+ import { Pulsora } from '@pulsora/core';
77
+
78
+ const mainSiteTracker = new Pulsora();
79
+ mainSiteTracker.init({ apiToken: 'token-1' });
80
+
81
+ const blogTracker = new Pulsora();
82
+ blogTracker.init({ apiToken: 'token-2' });
83
+
84
+ // Each tracker operates independently
85
+ mainSiteTracker.pageview();
86
+ blogTracker.pageview();
87
+ ```
88
+
89
+ ### For CDN Users (Recommended for Websites)
90
+
91
+ Add the script tag with your API token:
92
+
93
+ ```html
94
+ <!-- Auto-initializes and tracks pageviews -->
95
+ <script
96
+ async
97
+ src="https://cdn.pulsora.co/v1/pulsora.min.js"
98
+ data-token="YOUR_API_TOKEN"
99
+ data-debug="false"
100
+ ></script>
101
+
102
+ <script>
103
+ // Track custom event
104
+ window.pulsora.event('purchase', {
105
+ amount: 99.99,
106
+ currency: 'USD',
107
+ });
108
+
109
+ // Identify user
110
+ window.pulsora.identify('user_123');
111
+ </script>
112
+ ```
113
+
114
+ ## API Reference
115
+
116
+ ### `init(config: PulsoraConfig)`
117
+
118
+ Initialize the tracker.
119
+
120
+ ```typescript
121
+ interface PulsoraConfig {
122
+ apiToken: string; // Required: Your Pulsora API token
123
+ endpoint?: string; // Optional: API endpoint (default: https://api.pulsora.co/ingest)
124
+ autoPageviews?: boolean; // Optional: Auto-track pageviews (default: true)
125
+ debug?: boolean; // Optional: Enable debug logging (default: false)
126
+ maxRetries?: number; // Optional: Max retry attempts (default: 10)
127
+ retryBackoff?: number; // Optional: Initial retry backoff in ms (default: 1000)
128
+ }
129
+ ```
130
+
131
+ ### `pageview(options?: PageviewOptions)`
132
+
133
+ Track a pageview.
134
+
135
+ ```typescript
136
+ tracker.pageview(); // Current page
137
+
138
+ tracker.pageview({
139
+ url: 'https://example.com/page',
140
+ referrer: 'https://google.com',
141
+ title: 'Page Title',
142
+ });
143
+ ```
144
+
145
+ ### `event(eventName: string, eventData?: object)`
146
+
147
+ Track a custom event.
148
+
149
+ ```typescript
150
+ tracker.event('button_click');
151
+
152
+ tracker.event('purchase', {
153
+ product: 'Premium Plan',
154
+ amount: 99,
155
+ currency: 'USD',
156
+ });
157
+ ```
158
+
159
+ ### `identify(customerId: string)`
160
+
161
+ Identify a user. Links all previous anonymous activity to this user.
162
+
163
+ ```typescript
164
+ tracker.identify('user_123');
165
+ ```
166
+
167
+ ### `reset()`
168
+
169
+ Reset the session and clear user identification.
170
+
171
+ ```typescript
172
+ tracker.reset();
173
+ ```
174
+
175
+ ### `getVisitorFingerprint(): Promise<string>`
176
+
177
+ Get the current visitor's fingerprint. Useful for server-side revenue attribution.
178
+
179
+ ```typescript
180
+ const fingerprint = await tracker.getVisitorFingerprint();
181
+ // Send to your backend for revenue tracking
182
+ ```
183
+
184
+ ### `getSessionId(): string`
185
+
186
+ Get the current session ID.
187
+
188
+ ```typescript
189
+ const sessionId = tracker.getSessionId();
190
+ ```
191
+
192
+ ### `isIdentified(): boolean`
193
+
194
+ Check if the current visitor is identified.
195
+
196
+ ```typescript
197
+ if (tracker.isIdentified()) {
198
+ console.log('User is logged in');
199
+ }
200
+ ```
201
+
202
+ ### `use(extension: PulsoraExtension)`
203
+
204
+ Register an extension/plugin.
205
+
206
+ ```typescript
207
+ import { Pulsora } from '@pulsora/core';
208
+ import revenue from '@pulsora/revenue';
209
+
210
+ const tracker = new Pulsora();
211
+ tracker.use(revenue);
212
+ tracker.init({ apiToken: 'token' });
213
+ ```
214
+
215
+ ## TypeScript
216
+
217
+ Full TypeScript support with type definitions included:
218
+
219
+ ```typescript
220
+ import { Pulsora, PulsoraConfig, PageviewOptions } from '@pulsora/core';
221
+
222
+ const config: PulsoraConfig = {
223
+ apiToken: 'token',
224
+ debug: true,
225
+ };
226
+
227
+ const tracker = new Pulsora();
228
+ tracker.init(config);
229
+ ```
230
+
231
+ ## Privacy & GDPR Compliance
232
+
233
+ - **No cookies**: Pulsora doesn't use cookies
234
+ - **No localStorage**: All data is in-memory by default
235
+ - **Browser fingerprinting**: Uses non-PII technical characteristics
236
+ - **Anonymous by default**: Identification is opt-in via `identify()`
237
+ - **Transparent**: Open source and auditable
238
+
239
+ ## SPA Support
240
+
241
+ Automatic pageview tracking for Single Page Applications:
242
+
243
+ ```typescript
244
+ // Automatically tracks pageviews on:
245
+ // - history.pushState()
246
+ // - history.replaceState()
247
+ // - popstate events (back/forward buttons)
248
+
249
+ const tracker = new Pulsora();
250
+ tracker.init({
251
+ apiToken: 'token',
252
+ autoPageviews: true, // ← Enables SPA tracking
253
+ });
254
+ ```
255
+
256
+ ## Error Handling
257
+
258
+ Pulsora handles errors gracefully with automatic retry:
259
+
260
+ - Network failures: Exponential backoff retry
261
+ - Rate limiting: Respects Retry-After headers
262
+ - Max retries: Events dropped after max attempts
263
+ - Debug mode: Console warnings for troubleshooting
264
+
265
+ ## Browser Support
266
+
267
+ - Chrome/Edge (latest 2 versions)
268
+ - Firefox (latest 2 versions)
269
+ - Safari (latest 2 versions)
270
+ - Mobile browsers (iOS Safari, Chrome Mobile)
271
+
272
+ ## License
273
+
274
+ MIT
275
+
276
+ ## Links
277
+
278
+ - [Documentation](https://pulsora.co/docs)
279
+ - [GitHub](https://github.com/pulsora/javascript-sdk)
280
+ - [Website](https://pulsora.co)
@@ -0,0 +1,2 @@
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).pulsora=e()}(this,function(){"use strict";function t(){if("undefined"!=typeof crypto&&crypto.randomUUID)return crypto.randomUUID();let t=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const i=(t+16*Math.random())%16|0;return t=Math.floor(t/16),("x"===e?i:3&i|8).toString(16)})}function e(t){try{const e=new URL(t),i={};return e.searchParams.forEach((t,e)=>{i[e]=t}),{path:e.pathname,search:i}}catch(t){return{path:"/",search:{}}}}function i(t,e){"undefined"!=typeof console&&console.log}async function n(){return async function(t){if("undefined"!=typeof crypto&&crypto.subtle&&crypto.subtle.digest)try{const e=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(t));return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}catch(t){}let e=0;for(let i=0;i<t.length;i++)e=(e<<5)-e+t.charCodeAt(i),e&=e;return Math.abs(e).toString(16).padStart(8,"0")}([navigator.userAgent,navigator.language,(navigator.languages||[]).join(","),screen.width,screen.height,screen.colorDepth,window.devicePixelRatio||1,navigator.hardwareConcurrency||0,navigator.deviceMemory||0,navigator.maxTouchPoints||0,navigator.platform,(new Date).getTimezoneOffset(),s(),await o()].join("~"))}function s(){try{const t=document.createElement("canvas"),e=t.getContext("webgl")||t.getContext("experimental-webgl");if(!e)return"0";const i=e.getExtension("WEBGL_debug_renderer_info");if(!i)return"1";const n=e.getParameter(i.UNMASKED_VENDOR_WEBGL);return n+"~"+e.getParameter(i.UNMASKED_RENDERER_WEBGL)}catch(t){return"2"}}async function o(){try{const t=document.createElement("canvas");t.width=200,t.height=20;const e=t.getContext("2d");if(!e)return"0";e.textBaseline="top",e.font="14px 'PulsoraFont123'",e.textBaseline="alphabetic",e.fillStyle="#f60",e.fillRect(125,1,62,20),e.fillStyle="#069",e.fillText("Cwm fjord 🎨 glyph",2,15);return t.toDataURL().substring(100,200)}catch(t){return"1"}}class r{constructor(){this.customerId=null,this.sessionId=t(),this.startTime=Date.now()}getSessionId(){return this.sessionId}getDuration(){return Math.floor((Date.now()-this.startTime)/1e3)}setCustomerId(t){this.customerId=t}getCustomerId(){return this.customerId}isIdentified(){return null!==this.customerId}reset(){this.sessionId=t(),this.customerId=null,this.startTime=Date.now()}clearIdentification(){this.customerId=null}}class a{constructor(t){this.queue=new Map,this.retryTimeouts=new Map,this.config=t}async send(e){const i={id:t(),timestamp:Date.now(),attempts:0,data:e};await this.sendEvent(i)}async sendEvent(t){t.attempts++;try{const e={type:t.data.type,data:this.prepareEventData(t.data),token:this.config.apiToken};if(navigator.sendBeacon&&"identify"!==t.data.type){const n=new Blob([JSON.stringify(e)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,n))return this.config.debug&&i(0,t.data),void this.removeFromQueue(t.id)}const n=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(e),keepalive:!0});if(n.ok)return this.config.debug&&i(0,t.data),void this.removeFromQueue(t.id);if(429===n.status){const e=parseInt(n.headers.get("Retry-After")||"60",10);return this.config.debug&&i(),void this.scheduleRetry(t,1e3*e)}throw new Error(`HTTP ${n.status}`)}catch(e){if(this.config.debug&&i(),t.attempts<this.config.maxRetries){const e=Math.min(this.config.retryBackoff*Math.pow(2,t.attempts-1),3e4);this.scheduleRetry(t,e)}else this.config.debug&&i(t.attempts),this.removeFromQueue(t.id)}}prepareEventData(t){const{type:e,timestamp:i,...n}=t,s={};for(const[t,e]of Object.entries(n))void 0!==e&&(s[t]=e);return s}scheduleRetry(t,e){this.queue.set(t.id,t);const i=this.retryTimeouts.get(t.id);i&&clearTimeout(i);const n=setTimeout(()=>{this.retryTimeouts.delete(t.id);const e=this.queue.get(t.id);e&&this.sendEvent(e)},e);this.retryTimeouts.set(t.id,n)}removeFromQueue(t){this.queue.delete(t);const e=this.retryTimeouts.get(t);e&&(clearTimeout(e),this.retryTimeouts.delete(t))}flush(){const t=Array.from(this.queue.values());this.queue.clear();for(const t of this.retryTimeouts.values())clearTimeout(t);this.retryTimeouts.clear();for(const e of t)this.sendEvent(e)}get queueSize(){return this.queue.size}}const h=new class{constructor(){this.extensions=new Map,this.initialized=!1,this.sessionManager=new r}init(t){if(this.initialized)t.debug&&i();else{if("undefined"==typeof window)throw new Error("Browser environment required");this.config={endpoint:"https://api.pulsora.co/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...t},this.transport=new a({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),this.fingerprintPromise=n(),this.fingerprintPromise.then(t=>{var e;this.visitorFingerprint=t,(null===(e=this.config)||void 0===e?void 0:e.debug)&&i()}),this.initialized=!0,this.config.autoPageviews&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.pageview()):this.pageview(),this.setupSPATracking()),this.setupUnloadHandlers(),this.config.debug&&i(0,this.config)}}async pageview(t){if(!this.isReady())return;const i=(null==t?void 0:t.url)||location.href,n=(null==t?void 0:t.referrer)||document.referrer,s=(null==t?void 0:t.title)||document.title,o=e(i),r=function(t){if(t)try{const e=new URL(t).hostname.toLowerCase();if(e===location.hostname.toLowerCase())return;return e.replace(/^www\./,"")}catch(t){return}}(n),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),url:i,path:o.path,referrer:n||void 0,referrer_source:r,title:s,utm_source:o.search.utm_source,utm_medium:o.search.utm_medium,utm_campaign:o.search.utm_campaign,utm_term:o.search.utm_term,utm_content:o.search.utm_content,timestamp:Date.now()};this.transport.send(a)}async event(t,i){if(!this.isReady()||!t)return;const n=location.href,s=e(n),o={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),event_name:t,event_data:i,url:n,path:s.path,timestamp:Date.now()};this.transport.send(o)}async identify(t){var e;if(!this.isReady()||!t)return;this.sessionManager.setCustomerId(t);const n={type:"identify",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),customer_id:t,timestamp:Date.now()};this.transport.send(n),(null===(e=this.config)||void 0===e?void 0:e.debug)&&i()}reset(){var t;this.sessionManager.reset(),(null===(t=this.config)||void 0===t?void 0:t.debug)&&i()}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?(this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint):(this.fingerprintPromise=n(),this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint)}getSessionId(){return this.sessionManager.getSessionId()}isIdentified(){return this.sessionManager.isIdentified()}use(t){var e,n;this.extensions.has(t.name)?(null===(e=this.config)||void 0===e?void 0:e.debug)&&i(t.name):(this.extensions.set(t.name,t),t.init(this),(null===(n=this.config)||void 0===n?void 0:n.debug)&&i(t.name))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Not initialized"),!1)}setupSPATracking(){const t=history.pushState,e=history.replaceState;history.pushState=(...e)=>{t.apply(history,e),setTimeout(()=>this.pageview(),0)},history.replaceState=(...t)=>{e.apply(history,t),setTimeout(()=>this.pageview(),0)},addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const t=()=>{var t;return null===(t=this.transport)||void 0===t?void 0:t.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&t()}),addEventListener("pagehide",t),addEventListener("beforeunload",t)}};if("undefined"!=typeof window&&"undefined"!=typeof document&&(window.pulsora=h,document.currentScript)){const t=document.currentScript,e=t.getAttribute("data-token"),i=t.getAttribute("data-endpoint"),n="true"===t.getAttribute("data-debug");e&&h.init({apiToken:e,endpoint:i||void 0,debug:n})}return h});
2
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sources":["../src/utils.ts","../src/fingerprint.ts","../src/session.ts","../src/transport.ts","../src/browser.ts","../src/tracker.ts"],"sourcesContent":["/**\n * Generate a UUID v4\n */\nexport function generateUUID(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Compact fallback for older browsers\n let d = Date.now();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (d + Math.random() * 16) % 16 | 0;\n d = Math.floor(d / 16);\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n/**\n * SHA-256 hash function with fallback\n */\nexport async function sha256(str: string): Promise<string> {\n if (typeof crypto !== 'undefined' && crypto.subtle && crypto.subtle.digest) {\n try {\n const buf = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(str),\n );\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n } catch {}\n }\n\n // Fallback: simple hash (good enough for fingerprinting)\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash << 5) - hash + str.charCodeAt(i);\n hash = hash & hash;\n }\n return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\n/**\n * Parse URL and extract components\n */\nexport function parseUrl(url: string): {\n path: string;\n search: Record<string, string>;\n} {\n try {\n const u = new URL(url);\n const search: Record<string, string> = {};\n u.searchParams.forEach((v, k) => {\n search[k] = v;\n });\n return { path: u.pathname, search };\n } catch {\n return { path: '/', search: {} };\n }\n}\n\n/**\n * Get referrer source from URL\n * Returns the domain name for ANY external referrer\n */\nexport function getReferrerSource(referrer: string): string | undefined {\n if (!referrer) return;\n\n try {\n const referrerHost = new URL(referrer).hostname.toLowerCase();\n\n // Skip if same domain\n if (referrerHost === location.hostname.toLowerCase()) return;\n\n // Return the clean hostname (remove www. prefix if present)\n return referrerHost.replace(/^www\\./, '');\n } catch {\n return undefined;\n }\n}\n\n/**\n * Simple debounce function\n */\nexport function debounce<T extends (...args: any[]) => void>(\n fn: T,\n delay: number,\n): T {\n let timeout: any;\n return ((...args: any[]) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), delay);\n }) as T;\n}\n\n/**\n * Check if we're in a browser environment\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/**\n * Debug logging\n */\nexport function debugLog(message: string, data?: any): void {\n if (typeof console !== 'undefined' && console.log) {\n if (data !== undefined) {\n console.log(`[Pulsora] ${message}`, data);\n } else {\n console.log(`[Pulsora] ${message}`);\n }\n }\n}\n","import { sha256 } from './utils';\n\n/**\n * Generates a unique, stable browser fingerprint for tracking\n * Uses multiple browser characteristics to create a highly unique identifier\n * GDPR compliant - no PII is collected\n */\nexport async function generateFingerprint(): Promise<string> {\n const components = [\n // User agent and language (high entropy)\n navigator.userAgent,\n navigator.language,\n (navigator.languages || []).join(','),\n\n // Screen (very high entropy, cheap)\n screen.width,\n screen.height,\n screen.colorDepth,\n window.devicePixelRatio || 1,\n\n // Hardware\n navigator.hardwareConcurrency || 0,\n (navigator as any).deviceMemory || 0,\n navigator.maxTouchPoints || 0,\n\n // Browser/OS\n navigator.platform,\n new Date().getTimezoneOffset(),\n\n // WebGL (high entropy)\n getWebGLFingerprint(),\n\n // Canvas (high entropy, optimized)\n await getCanvasFingerprint(),\n ];\n\n return sha256(components.join('~'));\n}\n\n/**\n * WebGL fingerprinting - vendor and renderer info\n */\nfunction getWebGLFingerprint(): string {\n try {\n const canvas = document.createElement('canvas');\n const gl =\n canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) return '0';\n\n const debugInfo = (gl as any).getExtension('WEBGL_debug_renderer_info');\n if (!debugInfo) return '1';\n\n const vendor = (gl as any).getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);\n const renderer = (gl as any).getParameter(\n debugInfo.UNMASKED_RENDERER_WEBGL,\n );\n\n return vendor + '~' + renderer;\n } catch {\n return '2';\n }\n}\n\n/**\n * Canvas fingerprinting - optimized for size\n */\nasync function getCanvasFingerprint(): Promise<string> {\n try {\n const canvas = document.createElement('canvas');\n canvas.width = 200;\n canvas.height = 20;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return '0';\n\n // Use non-existent font to force system fallback (high entropy)\n ctx.textBaseline = 'top';\n ctx.font = \"14px 'PulsoraFont123'\";\n ctx.textBaseline = 'alphabetic';\n ctx.fillStyle = '#f60';\n ctx.fillRect(125, 1, 62, 20);\n ctx.fillStyle = '#069';\n\n // Text with emoji for extra entropy\n ctx.fillText('Cwm fjord 🎨 glyph', 2, 15);\n\n // Extract just a portion of the data URL to save space\n const dataUrl = canvas.toDataURL();\n // Take middle portion for better entropy\n return dataUrl.substring(100, 200);\n } catch {\n return '1';\n }\n}\n","import { generateUUID } from './utils';\n\n/**\n * Session manager for tracking user sessions\n * Handles session ID generation and customer identification\n */\nexport class SessionManager {\n private sessionId: string;\n private customerId: string | null = null;\n private startTime: number;\n\n constructor() {\n this.sessionId = generateUUID();\n this.startTime = Date.now();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n getDuration(): number {\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n setCustomerId(id: string): void {\n this.customerId = id;\n }\n\n getCustomerId(): string | null {\n return this.customerId;\n }\n\n isIdentified(): boolean {\n return this.customerId !== null;\n }\n\n reset(): void {\n this.sessionId = generateUUID();\n this.customerId = null;\n this.startTime = Date.now();\n }\n\n clearIdentification(): void {\n this.customerId = null;\n }\n}\n","import { QueuedEvent, TrackingData } from './types';\nimport { debugLog, generateUUID } from './utils';\n\nexport interface TransportConfig {\n endpoint: string;\n apiToken: string;\n maxRetries: number;\n retryBackoff: number;\n debug: boolean;\n}\n\n/**\n * Transport layer for sending events to the API\n * Handles retries, queuing, and network failures\n */\nexport class Transport {\n private config: TransportConfig;\n private queue = new Map<string, QueuedEvent>();\n private retryTimeouts = new Map<string, any>();\n\n constructor(config: TransportConfig) {\n this.config = config;\n }\n\n async send(data: TrackingData): Promise<void> {\n const event: QueuedEvent = {\n id: generateUUID(),\n timestamp: Date.now(),\n attempts: 0,\n data,\n };\n await this.sendEvent(event);\n }\n\n private async sendEvent(event: QueuedEvent): Promise<void> {\n event.attempts++;\n\n try {\n const payload = {\n type: event.data.type,\n data: this.prepareEventData(event.data),\n token: this.config.apiToken,\n };\n\n // Try sendBeacon first (except for identify)\n if (navigator.sendBeacon && event.data.type !== 'identify') {\n const blob = new Blob([JSON.stringify(payload)], {\n type: 'application/json',\n });\n\n if (navigator.sendBeacon(this.config.endpoint, blob)) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n }\n\n // Fallback to fetch\n const response = await fetch(this.config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Token': this.config.apiToken,\n },\n body: JSON.stringify(payload),\n keepalive: true,\n });\n\n if (response.ok) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n\n // Rate limit\n if (response.status === 429) {\n const retryAfter = parseInt(\n response.headers.get('Retry-After') || '60',\n 10,\n );\n this.config.debug &&\n debugLog(`Rate limited, retry after ${retryAfter}s`);\n this.scheduleRetry(event, retryAfter * 1000);\n return;\n }\n\n throw new Error(`HTTP ${response.status}`);\n } catch (error) {\n this.config.debug && debugLog('Send failed', { error, event });\n\n if (event.attempts < this.config.maxRetries) {\n const delay = Math.min(\n this.config.retryBackoff * Math.pow(2, event.attempts - 1),\n 30000,\n );\n this.scheduleRetry(event, delay);\n } else {\n this.config.debug &&\n debugLog(`Dropped after ${event.attempts} attempts`);\n this.removeFromQueue(event.id);\n }\n }\n }\n\n private prepareEventData(data: TrackingData): Record<string, any> {\n const { type, timestamp, ...rest } = data;\n const cleanData: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(rest)) {\n if (value !== undefined) {\n cleanData[key] = value;\n }\n }\n\n return cleanData;\n }\n\n private scheduleRetry(event: QueuedEvent, delay: number): void {\n this.queue.set(event.id, event);\n\n const existingTimeout = this.retryTimeouts.get(event.id);\n if (existingTimeout) {\n clearTimeout(existingTimeout);\n }\n\n const timeout = setTimeout(() => {\n this.retryTimeouts.delete(event.id);\n const queuedEvent = this.queue.get(event.id);\n if (queuedEvent) {\n this.sendEvent(queuedEvent);\n }\n }, delay);\n\n this.retryTimeouts.set(event.id, timeout);\n }\n\n private removeFromQueue(eventId: string): void {\n this.queue.delete(eventId);\n const timeout = this.retryTimeouts.get(eventId);\n if (timeout) {\n clearTimeout(timeout);\n this.retryTimeouts.delete(eventId);\n }\n }\n\n flush(): void {\n const events = Array.from(this.queue.values());\n this.queue.clear();\n\n for (const timeout of this.retryTimeouts.values()) {\n clearTimeout(timeout);\n }\n this.retryTimeouts.clear();\n\n for (const event of events) {\n this.sendEvent(event);\n }\n }\n\n get queueSize(): number {\n return this.queue.size;\n }\n}\n","// Browser/CDN entry point with singleton instance and auto-initialization\nimport { Tracker } from './tracker';\n\n// Create singleton instance\nconst pulsora = new Tracker();\n\n// Auto-initialize if data-token is present on script tag\nif (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Attach to window for global access\n (window as any).pulsora = pulsora;\n\n // Check for script tag with data attributes\n if (document.currentScript) {\n const script = document.currentScript as HTMLScriptElement;\n const token = script.getAttribute('data-token');\n const endpoint = script.getAttribute('data-endpoint');\n const debug = script.getAttribute('data-debug') === 'true';\n\n if (token) {\n pulsora.init({\n apiToken: token,\n endpoint: endpoint || undefined,\n debug,\n });\n }\n }\n}\n\n// Export for module systems (though typically used via window.pulsora)\nexport default pulsora;\n","import { generateFingerprint } from './fingerprint';\nimport { SessionManager } from './session';\nimport { Transport } from './transport';\nimport {\n EventData,\n PageviewOptions,\n PulsoraConfig,\n PulsoraCore,\n PulsoraExtension,\n TrackingData,\n} from './types';\nimport { debugLog, getReferrerSource, parseUrl } from './utils';\n\n/**\n * Main Pulsora tracker implementation\n * Handles pageview tracking, custom events, and user identification\n */\nexport class Tracker implements PulsoraCore {\n private config?: PulsoraConfig;\n private transport?: Transport;\n private sessionManager: SessionManager;\n private visitorFingerprint?: string;\n private fingerprintPromise?: Promise<string>;\n private extensions = new Map<string, PulsoraExtension>();\n private initialized = false;\n\n constructor() {\n this.sessionManager = new SessionManager();\n }\n\n init(config: PulsoraConfig): void {\n if (this.initialized) {\n config.debug && debugLog('Already initialized');\n return;\n }\n\n if (typeof window === 'undefined') {\n throw new Error('Browser environment required');\n }\n\n this.config = {\n endpoint: 'https://api.pulsora.co/ingest',\n autoPageviews: true,\n debug: false,\n maxRetries: 10,\n retryBackoff: 1000,\n ...config,\n };\n\n this.transport = new Transport({\n endpoint: this.config.endpoint!,\n apiToken: this.config.apiToken,\n maxRetries: this.config.maxRetries!,\n retryBackoff: this.config.retryBackoff!,\n debug: this.config.debug!,\n });\n\n // Start fingerprint generation\n this.fingerprintPromise = generateFingerprint();\n this.fingerprintPromise.then((fingerprint) => {\n this.visitorFingerprint = fingerprint;\n this.config?.debug && debugLog('Fingerprint ready', fingerprint);\n });\n\n this.initialized = true;\n\n // Auto pageviews\n if (this.config.autoPageviews) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => this.pageview());\n } else {\n this.pageview();\n }\n this.setupSPATracking();\n }\n\n // Flush on unload\n this.setupUnloadHandlers();\n\n this.config.debug && debugLog('Initialized', this.config);\n }\n\n async pageview(options?: PageviewOptions): Promise<void> {\n if (!this.isReady()) return;\n\n const url = options?.url || location.href;\n const referrer = options?.referrer || document.referrer;\n const title = options?.title || document.title;\n\n const parsed = parseUrl(url);\n const referrerSource = getReferrerSource(referrer);\n\n const data: TrackingData = {\n type: 'pageview',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n url,\n path: parsed.path,\n referrer: referrer || undefined,\n referrer_source: referrerSource,\n title,\n utm_source: parsed.search.utm_source,\n utm_medium: parsed.search.utm_medium,\n utm_campaign: parsed.search.utm_campaign,\n utm_term: parsed.search.utm_term,\n utm_content: parsed.search.utm_content,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async event(eventName: string, eventData?: EventData): Promise<void> {\n if (!this.isReady() || !eventName) return;\n\n const url = location.href;\n const parsed = parseUrl(url);\n\n const data: TrackingData = {\n type: 'event',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n event_name: eventName,\n event_data: eventData,\n url,\n path: parsed.path,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async identify(customerId: string): Promise<void> {\n if (!this.isReady() || !customerId) return;\n\n this.sessionManager.setCustomerId(customerId);\n\n const data: TrackingData = {\n type: 'identify',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n customer_id: customerId,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n this.config?.debug && debugLog('User identified', customerId);\n }\n\n reset(): void {\n this.sessionManager.reset();\n this.config?.debug && debugLog('Session reset');\n }\n\n async getVisitorFingerprint(): Promise<string> {\n if (this.visitorFingerprint) return this.visitorFingerprint;\n if (this.fingerprintPromise) {\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n this.fingerprintPromise = generateFingerprint();\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n getSessionId(): string {\n return this.sessionManager.getSessionId();\n }\n\n isIdentified(): boolean {\n return this.sessionManager.isIdentified();\n }\n\n use(extension: PulsoraExtension): void {\n if (this.extensions.has(extension.name)) {\n this.config?.debug &&\n debugLog(`Extension ${extension.name} already loaded`);\n return;\n }\n this.extensions.set(extension.name, extension);\n extension.init(this);\n this.config?.debug && debugLog(`Extension ${extension.name} loaded`);\n }\n\n private isReady(): boolean {\n if (!this.initialized) {\n console.warn('[Pulsora] Not initialized');\n return false;\n }\n return true;\n }\n\n private setupSPATracking(): void {\n // Intercept pushState/replaceState\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n history.pushState = (...args) => {\n originalPushState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n history.replaceState = (...args) => {\n originalReplaceState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Back/forward navigation\n addEventListener('popstate', () => {\n setTimeout(() => this.pageview(), 0);\n });\n }\n\n private setupUnloadHandlers(): void {\n const flush = () => this.transport?.flush();\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush();\n });\n\n addEventListener('pagehide', flush);\n addEventListener('beforeunload', flush);\n }\n}\n"],"names":["generateUUID","crypto","randomUUID","d","Date","now","replace","c","r","Math","random","floor","toString","parseUrl","url","u","URL","search","searchParams","forEach","v","k","path","pathname","_a","debugLog","message","data","console","log","async","generateFingerprint","str","subtle","digest","buf","TextEncoder","encode","Array","from","Uint8Array","map","b","padStart","join","hash","i","length","charCodeAt","abs","sha256","navigator","userAgent","language","languages","screen","width","height","colorDepth","window","devicePixelRatio","hardwareConcurrency","deviceMemory","maxTouchPoints","platform","getTimezoneOffset","getWebGLFingerprint","getCanvasFingerprint","canvas","document","createElement","gl","getContext","debugInfo","getExtension","vendor","getParameter","UNMASKED_VENDOR_WEBGL","UNMASKED_RENDERER_WEBGL","ctx","textBaseline","font","fillStyle","fillRect","fillText","toDataURL","substring","SessionManager","constructor","this","customerId","sessionId","startTime","getSessionId","getDuration","setCustomerId","id","getCustomerId","isIdentified","reset","clearIdentification","Transport","config","queue","Map","retryTimeouts","send","event","timestamp","attempts","sendEvent","payload","type","prepareEventData","token","apiToken","sendBeacon","blob","Blob","JSON","stringify","endpoint","debug","removeFromQueue","response","fetch","method","headers","body","keepalive","ok","status","retryAfter","parseInt","get","scheduleRetry","Error","error","maxRetries","delay","min","retryBackoff","pow","rest","cleanData","key","value","Object","entries","undefined","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","eventId","flush","events","values","clear","queueSize","size","pulsora","extensions","initialized","sessionManager","init","autoPageviews","transport","fingerprintPromise","then","fingerprint","visitorFingerprint","readyState","addEventListener","pageview","setupSPATracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","parsed","referrerSource","referrerHost","hostname","toLowerCase","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","utm_source","utm_medium","utm_campaign","utm_term","utm_content","eventName","eventData","event_name","event_data","identify","customer_id","use","extension","has","name","_b","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","visibilityState","currentScript","script","getAttribute"],"mappings":"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"}
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";function t(){if("undefined"!=typeof crypto&&crypto.randomUUID)return crypto.randomUUID();let t=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const i=(t+16*Math.random())%16|0;return t=Math.floor(t/16),("x"===e?i:3&i|8).toString(16)})}function e(t){try{const e=new URL(t),i={};return e.searchParams.forEach((t,e)=>{i[e]=t}),{path:e.pathname,search:i}}catch(t){return{path:"/",search:{}}}}function i(t,e){"undefined"!=typeof console&&console.log&&(void 0!==e?console.log(`[Pulsora] ${t}`,e):console.log(`[Pulsora] ${t}`))}async function n(){return async function(t){if("undefined"!=typeof crypto&&crypto.subtle&&crypto.subtle.digest)try{const e=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(t));return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}catch(t){}let e=0;for(let i=0;i<t.length;i++)e=(e<<5)-e+t.charCodeAt(i),e&=e;return Math.abs(e).toString(16).padStart(8,"0")}([navigator.userAgent,navigator.language,(navigator.languages||[]).join(","),screen.width,screen.height,screen.colorDepth,window.devicePixelRatio||1,navigator.hardwareConcurrency||0,navigator.deviceMemory||0,navigator.maxTouchPoints||0,navigator.platform,(new Date).getTimezoneOffset(),s(),await r()].join("~"))}function s(){try{const t=document.createElement("canvas"),e=t.getContext("webgl")||t.getContext("experimental-webgl");if(!e)return"0";const i=e.getExtension("WEBGL_debug_renderer_info");if(!i)return"1";const n=e.getParameter(i.UNMASKED_VENDOR_WEBGL);return n+"~"+e.getParameter(i.UNMASKED_RENDERER_WEBGL)}catch(t){return"2"}}async function r(){try{const t=document.createElement("canvas");t.width=200,t.height=20;const e=t.getContext("2d");if(!e)return"0";e.textBaseline="top",e.font="14px 'PulsoraFont123'",e.textBaseline="alphabetic",e.fillStyle="#f60",e.fillRect(125,1,62,20),e.fillStyle="#069",e.fillText("Cwm fjord 🎨 glyph",2,15);return t.toDataURL().substring(100,200)}catch(t){return"1"}}class o{constructor(){this.customerId=null,this.sessionId=t(),this.startTime=Date.now()}getSessionId(){return this.sessionId}getDuration(){return Math.floor((Date.now()-this.startTime)/1e3)}setCustomerId(t){this.customerId=t}getCustomerId(){return this.customerId}isIdentified(){return null!==this.customerId}reset(){this.sessionId=t(),this.customerId=null,this.startTime=Date.now()}clearIdentification(){this.customerId=null}}class a{constructor(t){this.queue=new Map,this.retryTimeouts=new Map,this.config=t}async send(e){const i={id:t(),timestamp:Date.now(),attempts:0,data:e};await this.sendEvent(i)}async sendEvent(t){t.attempts++;try{const e={type:t.data.type,data:this.prepareEventData(t.data),token:this.config.apiToken};if(navigator.sendBeacon&&"identify"!==t.data.type){const n=new Blob([JSON.stringify(e)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,n))return this.config.debug&&i("Event sent",t.data),void this.removeFromQueue(t.id)}const n=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(e),keepalive:!0});if(n.ok)return this.config.debug&&i("Event sent",t.data),void this.removeFromQueue(t.id);if(429===n.status){const e=parseInt(n.headers.get("Retry-After")||"60",10);return this.config.debug&&i(`Rate limited, retry after ${e}s`),void this.scheduleRetry(t,1e3*e)}throw new Error(`HTTP ${n.status}`)}catch(e){if(this.config.debug&&i("Send failed",{error:e,event:t}),t.attempts<this.config.maxRetries){const e=Math.min(this.config.retryBackoff*Math.pow(2,t.attempts-1),3e4);this.scheduleRetry(t,e)}else this.config.debug&&i(`Dropped after ${t.attempts} attempts`),this.removeFromQueue(t.id)}}prepareEventData(t){const{type:e,timestamp:i,...n}=t,s={};for(const[t,e]of Object.entries(n))void 0!==e&&(s[t]=e);return s}scheduleRetry(t,e){this.queue.set(t.id,t);const i=this.retryTimeouts.get(t.id);i&&clearTimeout(i);const n=setTimeout(()=>{this.retryTimeouts.delete(t.id);const e=this.queue.get(t.id);e&&this.sendEvent(e)},e);this.retryTimeouts.set(t.id,n)}removeFromQueue(t){this.queue.delete(t);const e=this.retryTimeouts.get(t);e&&(clearTimeout(e),this.retryTimeouts.delete(t))}flush(){const t=Array.from(this.queue.values());this.queue.clear();for(const t of this.retryTimeouts.values())clearTimeout(t);this.retryTimeouts.clear();for(const e of t)this.sendEvent(e)}get queueSize(){return this.queue.size}}exports.Pulsora=class{constructor(){this.extensions=new Map,this.initialized=!1,this.sessionManager=new o}init(t){if(this.initialized)t.debug&&i("Already initialized");else{if("undefined"==typeof window)throw new Error("Browser environment required");this.config={endpoint:"https://api.pulsora.co/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...t},this.transport=new a({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),this.fingerprintPromise=n(),this.fingerprintPromise.then(t=>{var e;this.visitorFingerprint=t,(null===(e=this.config)||void 0===e?void 0:e.debug)&&i("Fingerprint ready",t)}),this.initialized=!0,this.config.autoPageviews&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.pageview()):this.pageview(),this.setupSPATracking()),this.setupUnloadHandlers(),this.config.debug&&i("Initialized",this.config)}}async pageview(t){if(!this.isReady())return;const i=(null==t?void 0:t.url)||location.href,n=(null==t?void 0:t.referrer)||document.referrer,s=(null==t?void 0:t.title)||document.title,r=e(i),o=function(t){if(t)try{const e=new URL(t).hostname.toLowerCase();if(e===location.hostname.toLowerCase())return;return e.replace(/^www\./,"")}catch(t){return}}(n),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),url:i,path:r.path,referrer:n||void 0,referrer_source:o,title:s,utm_source:r.search.utm_source,utm_medium:r.search.utm_medium,utm_campaign:r.search.utm_campaign,utm_term:r.search.utm_term,utm_content:r.search.utm_content,timestamp:Date.now()};this.transport.send(a)}async event(t,i){if(!this.isReady()||!t)return;const n=location.href,s=e(n),r={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),event_name:t,event_data:i,url:n,path:s.path,timestamp:Date.now()};this.transport.send(r)}async identify(t){var e;if(!this.isReady()||!t)return;this.sessionManager.setCustomerId(t);const n={type:"identify",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),customer_id:t,timestamp:Date.now()};this.transport.send(n),(null===(e=this.config)||void 0===e?void 0:e.debug)&&i("User identified",t)}reset(){var t;this.sessionManager.reset(),(null===(t=this.config)||void 0===t?void 0:t.debug)&&i("Session reset")}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?(this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint):(this.fingerprintPromise=n(),this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint)}getSessionId(){return this.sessionManager.getSessionId()}isIdentified(){return this.sessionManager.isIdentified()}use(t){var e,n;this.extensions.has(t.name)?(null===(e=this.config)||void 0===e?void 0:e.debug)&&i(`Extension ${t.name} already loaded`):(this.extensions.set(t.name,t),t.init(this),(null===(n=this.config)||void 0===n?void 0:n.debug)&&i(`Extension ${t.name} loaded`))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Not initialized"),!1)}setupSPATracking(){const t=history.pushState,e=history.replaceState;history.pushState=(...e)=>{t.apply(history,e),setTimeout(()=>this.pageview(),0)},history.replaceState=(...t)=>{e.apply(history,t),setTimeout(()=>this.pageview(),0)},addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const t=()=>{var t;return null===(t=this.transport)||void 0===t?void 0:t.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&t()}),addEventListener("pagehide",t),addEventListener("beforeunload",t)}};
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/utils.ts","../src/fingerprint.ts","../src/session.ts","../src/transport.ts","../src/tracker.ts"],"sourcesContent":["/**\n * Generate a UUID v4\n */\nexport function generateUUID(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Compact fallback for older browsers\n let d = Date.now();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (d + Math.random() * 16) % 16 | 0;\n d = Math.floor(d / 16);\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n/**\n * SHA-256 hash function with fallback\n */\nexport async function sha256(str: string): Promise<string> {\n if (typeof crypto !== 'undefined' && crypto.subtle && crypto.subtle.digest) {\n try {\n const buf = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(str),\n );\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n } catch {}\n }\n\n // Fallback: simple hash (good enough for fingerprinting)\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash << 5) - hash + str.charCodeAt(i);\n hash = hash & hash;\n }\n return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\n/**\n * Parse URL and extract components\n */\nexport function parseUrl(url: string): {\n path: string;\n search: Record<string, string>;\n} {\n try {\n const u = new URL(url);\n const search: Record<string, string> = {};\n u.searchParams.forEach((v, k) => {\n search[k] = v;\n });\n return { path: u.pathname, search };\n } catch {\n return { path: '/', search: {} };\n }\n}\n\n/**\n * Get referrer source from URL\n * Returns the domain name for ANY external referrer\n */\nexport function getReferrerSource(referrer: string): string | undefined {\n if (!referrer) return;\n\n try {\n const referrerHost = new URL(referrer).hostname.toLowerCase();\n\n // Skip if same domain\n if (referrerHost === location.hostname.toLowerCase()) return;\n\n // Return the clean hostname (remove www. prefix if present)\n return referrerHost.replace(/^www\\./, '');\n } catch {\n return undefined;\n }\n}\n\n/**\n * Simple debounce function\n */\nexport function debounce<T extends (...args: any[]) => void>(\n fn: T,\n delay: number,\n): T {\n let timeout: any;\n return ((...args: any[]) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), delay);\n }) as T;\n}\n\n/**\n * Check if we're in a browser environment\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/**\n * Debug logging\n */\nexport function debugLog(message: string, data?: any): void {\n if (typeof console !== 'undefined' && console.log) {\n if (data !== undefined) {\n console.log(`[Pulsora] ${message}`, data);\n } else {\n console.log(`[Pulsora] ${message}`);\n }\n }\n}\n","import { sha256 } from './utils';\n\n/**\n * Generates a unique, stable browser fingerprint for tracking\n * Uses multiple browser characteristics to create a highly unique identifier\n * GDPR compliant - no PII is collected\n */\nexport async function generateFingerprint(): Promise<string> {\n const components = [\n // User agent and language (high entropy)\n navigator.userAgent,\n navigator.language,\n (navigator.languages || []).join(','),\n\n // Screen (very high entropy, cheap)\n screen.width,\n screen.height,\n screen.colorDepth,\n window.devicePixelRatio || 1,\n\n // Hardware\n navigator.hardwareConcurrency || 0,\n (navigator as any).deviceMemory || 0,\n navigator.maxTouchPoints || 0,\n\n // Browser/OS\n navigator.platform,\n new Date().getTimezoneOffset(),\n\n // WebGL (high entropy)\n getWebGLFingerprint(),\n\n // Canvas (high entropy, optimized)\n await getCanvasFingerprint(),\n ];\n\n return sha256(components.join('~'));\n}\n\n/**\n * WebGL fingerprinting - vendor and renderer info\n */\nfunction getWebGLFingerprint(): string {\n try {\n const canvas = document.createElement('canvas');\n const gl =\n canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) return '0';\n\n const debugInfo = (gl as any).getExtension('WEBGL_debug_renderer_info');\n if (!debugInfo) return '1';\n\n const vendor = (gl as any).getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);\n const renderer = (gl as any).getParameter(\n debugInfo.UNMASKED_RENDERER_WEBGL,\n );\n\n return vendor + '~' + renderer;\n } catch {\n return '2';\n }\n}\n\n/**\n * Canvas fingerprinting - optimized for size\n */\nasync function getCanvasFingerprint(): Promise<string> {\n try {\n const canvas = document.createElement('canvas');\n canvas.width = 200;\n canvas.height = 20;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return '0';\n\n // Use non-existent font to force system fallback (high entropy)\n ctx.textBaseline = 'top';\n ctx.font = \"14px 'PulsoraFont123'\";\n ctx.textBaseline = 'alphabetic';\n ctx.fillStyle = '#f60';\n ctx.fillRect(125, 1, 62, 20);\n ctx.fillStyle = '#069';\n\n // Text with emoji for extra entropy\n ctx.fillText('Cwm fjord 🎨 glyph', 2, 15);\n\n // Extract just a portion of the data URL to save space\n const dataUrl = canvas.toDataURL();\n // Take middle portion for better entropy\n return dataUrl.substring(100, 200);\n } catch {\n return '1';\n }\n}\n","import { generateUUID } from './utils';\n\n/**\n * Session manager for tracking user sessions\n * Handles session ID generation and customer identification\n */\nexport class SessionManager {\n private sessionId: string;\n private customerId: string | null = null;\n private startTime: number;\n\n constructor() {\n this.sessionId = generateUUID();\n this.startTime = Date.now();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n getDuration(): number {\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n setCustomerId(id: string): void {\n this.customerId = id;\n }\n\n getCustomerId(): string | null {\n return this.customerId;\n }\n\n isIdentified(): boolean {\n return this.customerId !== null;\n }\n\n reset(): void {\n this.sessionId = generateUUID();\n this.customerId = null;\n this.startTime = Date.now();\n }\n\n clearIdentification(): void {\n this.customerId = null;\n }\n}\n","import { QueuedEvent, TrackingData } from './types';\nimport { debugLog, generateUUID } from './utils';\n\nexport interface TransportConfig {\n endpoint: string;\n apiToken: string;\n maxRetries: number;\n retryBackoff: number;\n debug: boolean;\n}\n\n/**\n * Transport layer for sending events to the API\n * Handles retries, queuing, and network failures\n */\nexport class Transport {\n private config: TransportConfig;\n private queue = new Map<string, QueuedEvent>();\n private retryTimeouts = new Map<string, any>();\n\n constructor(config: TransportConfig) {\n this.config = config;\n }\n\n async send(data: TrackingData): Promise<void> {\n const event: QueuedEvent = {\n id: generateUUID(),\n timestamp: Date.now(),\n attempts: 0,\n data,\n };\n await this.sendEvent(event);\n }\n\n private async sendEvent(event: QueuedEvent): Promise<void> {\n event.attempts++;\n\n try {\n const payload = {\n type: event.data.type,\n data: this.prepareEventData(event.data),\n token: this.config.apiToken,\n };\n\n // Try sendBeacon first (except for identify)\n if (navigator.sendBeacon && event.data.type !== 'identify') {\n const blob = new Blob([JSON.stringify(payload)], {\n type: 'application/json',\n });\n\n if (navigator.sendBeacon(this.config.endpoint, blob)) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n }\n\n // Fallback to fetch\n const response = await fetch(this.config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Token': this.config.apiToken,\n },\n body: JSON.stringify(payload),\n keepalive: true,\n });\n\n if (response.ok) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n\n // Rate limit\n if (response.status === 429) {\n const retryAfter = parseInt(\n response.headers.get('Retry-After') || '60',\n 10,\n );\n this.config.debug &&\n debugLog(`Rate limited, retry after ${retryAfter}s`);\n this.scheduleRetry(event, retryAfter * 1000);\n return;\n }\n\n throw new Error(`HTTP ${response.status}`);\n } catch (error) {\n this.config.debug && debugLog('Send failed', { error, event });\n\n if (event.attempts < this.config.maxRetries) {\n const delay = Math.min(\n this.config.retryBackoff * Math.pow(2, event.attempts - 1),\n 30000,\n );\n this.scheduleRetry(event, delay);\n } else {\n this.config.debug &&\n debugLog(`Dropped after ${event.attempts} attempts`);\n this.removeFromQueue(event.id);\n }\n }\n }\n\n private prepareEventData(data: TrackingData): Record<string, any> {\n const { type, timestamp, ...rest } = data;\n const cleanData: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(rest)) {\n if (value !== undefined) {\n cleanData[key] = value;\n }\n }\n\n return cleanData;\n }\n\n private scheduleRetry(event: QueuedEvent, delay: number): void {\n this.queue.set(event.id, event);\n\n const existingTimeout = this.retryTimeouts.get(event.id);\n if (existingTimeout) {\n clearTimeout(existingTimeout);\n }\n\n const timeout = setTimeout(() => {\n this.retryTimeouts.delete(event.id);\n const queuedEvent = this.queue.get(event.id);\n if (queuedEvent) {\n this.sendEvent(queuedEvent);\n }\n }, delay);\n\n this.retryTimeouts.set(event.id, timeout);\n }\n\n private removeFromQueue(eventId: string): void {\n this.queue.delete(eventId);\n const timeout = this.retryTimeouts.get(eventId);\n if (timeout) {\n clearTimeout(timeout);\n this.retryTimeouts.delete(eventId);\n }\n }\n\n flush(): void {\n const events = Array.from(this.queue.values());\n this.queue.clear();\n\n for (const timeout of this.retryTimeouts.values()) {\n clearTimeout(timeout);\n }\n this.retryTimeouts.clear();\n\n for (const event of events) {\n this.sendEvent(event);\n }\n }\n\n get queueSize(): number {\n return this.queue.size;\n }\n}\n","import { generateFingerprint } from './fingerprint';\nimport { SessionManager } from './session';\nimport { Transport } from './transport';\nimport {\n EventData,\n PageviewOptions,\n PulsoraConfig,\n PulsoraCore,\n PulsoraExtension,\n TrackingData,\n} from './types';\nimport { debugLog, getReferrerSource, parseUrl } from './utils';\n\n/**\n * Main Pulsora tracker implementation\n * Handles pageview tracking, custom events, and user identification\n */\nexport class Tracker implements PulsoraCore {\n private config?: PulsoraConfig;\n private transport?: Transport;\n private sessionManager: SessionManager;\n private visitorFingerprint?: string;\n private fingerprintPromise?: Promise<string>;\n private extensions = new Map<string, PulsoraExtension>();\n private initialized = false;\n\n constructor() {\n this.sessionManager = new SessionManager();\n }\n\n init(config: PulsoraConfig): void {\n if (this.initialized) {\n config.debug && debugLog('Already initialized');\n return;\n }\n\n if (typeof window === 'undefined') {\n throw new Error('Browser environment required');\n }\n\n this.config = {\n endpoint: 'https://api.pulsora.co/ingest',\n autoPageviews: true,\n debug: false,\n maxRetries: 10,\n retryBackoff: 1000,\n ...config,\n };\n\n this.transport = new Transport({\n endpoint: this.config.endpoint!,\n apiToken: this.config.apiToken,\n maxRetries: this.config.maxRetries!,\n retryBackoff: this.config.retryBackoff!,\n debug: this.config.debug!,\n });\n\n // Start fingerprint generation\n this.fingerprintPromise = generateFingerprint();\n this.fingerprintPromise.then((fingerprint) => {\n this.visitorFingerprint = fingerprint;\n this.config?.debug && debugLog('Fingerprint ready', fingerprint);\n });\n\n this.initialized = true;\n\n // Auto pageviews\n if (this.config.autoPageviews) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => this.pageview());\n } else {\n this.pageview();\n }\n this.setupSPATracking();\n }\n\n // Flush on unload\n this.setupUnloadHandlers();\n\n this.config.debug && debugLog('Initialized', this.config);\n }\n\n async pageview(options?: PageviewOptions): Promise<void> {\n if (!this.isReady()) return;\n\n const url = options?.url || location.href;\n const referrer = options?.referrer || document.referrer;\n const title = options?.title || document.title;\n\n const parsed = parseUrl(url);\n const referrerSource = getReferrerSource(referrer);\n\n const data: TrackingData = {\n type: 'pageview',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n url,\n path: parsed.path,\n referrer: referrer || undefined,\n referrer_source: referrerSource,\n title,\n utm_source: parsed.search.utm_source,\n utm_medium: parsed.search.utm_medium,\n utm_campaign: parsed.search.utm_campaign,\n utm_term: parsed.search.utm_term,\n utm_content: parsed.search.utm_content,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async event(eventName: string, eventData?: EventData): Promise<void> {\n if (!this.isReady() || !eventName) return;\n\n const url = location.href;\n const parsed = parseUrl(url);\n\n const data: TrackingData = {\n type: 'event',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n event_name: eventName,\n event_data: eventData,\n url,\n path: parsed.path,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async identify(customerId: string): Promise<void> {\n if (!this.isReady() || !customerId) return;\n\n this.sessionManager.setCustomerId(customerId);\n\n const data: TrackingData = {\n type: 'identify',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n customer_id: customerId,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n this.config?.debug && debugLog('User identified', customerId);\n }\n\n reset(): void {\n this.sessionManager.reset();\n this.config?.debug && debugLog('Session reset');\n }\n\n async getVisitorFingerprint(): Promise<string> {\n if (this.visitorFingerprint) return this.visitorFingerprint;\n if (this.fingerprintPromise) {\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n this.fingerprintPromise = generateFingerprint();\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n getSessionId(): string {\n return this.sessionManager.getSessionId();\n }\n\n isIdentified(): boolean {\n return this.sessionManager.isIdentified();\n }\n\n use(extension: PulsoraExtension): void {\n if (this.extensions.has(extension.name)) {\n this.config?.debug &&\n debugLog(`Extension ${extension.name} already loaded`);\n return;\n }\n this.extensions.set(extension.name, extension);\n extension.init(this);\n this.config?.debug && debugLog(`Extension ${extension.name} loaded`);\n }\n\n private isReady(): boolean {\n if (!this.initialized) {\n console.warn('[Pulsora] Not initialized');\n return false;\n }\n return true;\n }\n\n private setupSPATracking(): void {\n // Intercept pushState/replaceState\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n history.pushState = (...args) => {\n originalPushState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n history.replaceState = (...args) => {\n originalReplaceState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Back/forward navigation\n addEventListener('popstate', () => {\n setTimeout(() => this.pageview(), 0);\n });\n }\n\n private setupUnloadHandlers(): void {\n const flush = () => this.transport?.flush();\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush();\n });\n\n addEventListener('pagehide', flush);\n addEventListener('beforeunload', flush);\n }\n}\n"],"names":["generateUUID","crypto","randomUUID","d","Date","now","replace","c","r","Math","random","floor","toString","parseUrl","url","u","URL","search","searchParams","forEach","v","k","path","pathname","_a","debugLog","message","data","console","log","undefined","async","generateFingerprint","str","subtle","digest","buf","TextEncoder","encode","Array","from","Uint8Array","map","b","padStart","join","hash","i","length","charCodeAt","abs","sha256","navigator","userAgent","language","languages","screen","width","height","colorDepth","window","devicePixelRatio","hardwareConcurrency","deviceMemory","maxTouchPoints","platform","getTimezoneOffset","getWebGLFingerprint","getCanvasFingerprint","canvas","document","createElement","gl","getContext","debugInfo","getExtension","vendor","getParameter","UNMASKED_VENDOR_WEBGL","UNMASKED_RENDERER_WEBGL","ctx","textBaseline","font","fillStyle","fillRect","fillText","toDataURL","substring","SessionManager","constructor","this","customerId","sessionId","startTime","getSessionId","getDuration","setCustomerId","id","getCustomerId","isIdentified","reset","clearIdentification","Transport","config","queue","Map","retryTimeouts","send","event","timestamp","attempts","sendEvent","payload","type","prepareEventData","token","apiToken","sendBeacon","blob","Blob","JSON","stringify","endpoint","debug","removeFromQueue","response","fetch","method","headers","body","keepalive","ok","status","retryAfter","parseInt","get","scheduleRetry","Error","error","maxRetries","delay","min","retryBackoff","pow","rest","cleanData","key","value","Object","entries","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","eventId","flush","events","values","clear","queueSize","size","extensions","initialized","sessionManager","init","autoPageviews","transport","fingerprintPromise","then","fingerprint","visitorFingerprint","readyState","addEventListener","pageview","setupSPATracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","parsed","referrerSource","referrerHost","hostname","toLowerCase","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","utm_source","utm_medium","utm_campaign","utm_term","utm_content","eventName","eventData","event_name","event_data","identify","customer_id","use","extension","has","name","_b","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","visibilityState"],"mappings":"sBAGgBA,IACd,GAAsB,oBAAXC,QAA0BA,OAAOC,WAC1C,OAAOD,OAAOC,aAIhB,IAAIC,EAAIC,KAAKC,MACb,MAAO,uCAAuCC,QAAQ,QAAUC,IAC9D,MAAMC,GAAKL,EAAoB,GAAhBM,KAAKC,UAAiB,GAAK,EAE1C,OADAP,EAAIM,KAAKE,MAAMR,EAAI,KACL,MAANI,EAAYC,EAAS,EAAJA,EAAW,GAAKI,SAAS,KAEtD,CA8BM,SAAUC,EAASC,GAIvB,IACE,MAAMC,EAAI,IAAIC,IAAIF,GACZG,EAAiC,CAAA,EAIvC,OAHAF,EAAEG,aAAaC,QAAQ,CAACC,EAAGC,KACzBJ,EAAOI,GAAKD,IAEP,CAAEE,KAAMP,EAAEQ,SAAUN,SAC7B,CAAE,MAAAO,GACA,MAAO,CAAEF,KAAM,IAAKL,OAAQ,CAAA,EAC9B,CACF,CA8CM,SAAUQ,EAASC,EAAiBC,GACjB,oBAAZC,SAA2BA,QAAQC,WAC/BC,IAATH,EACFC,QAAQC,IAAI,aAAaH,IAAWC,GAEpCC,QAAQC,IAAI,aAAaH,KAG/B,CC1GOK,eAAeC,IA6BpB,ODhBKD,eAAsBE,GAC3B,GAAsB,oBAAXhC,QAA0BA,OAAOiC,QAAUjC,OAAOiC,OAAOC,OAClE,IACE,MAAMC,QAAYnC,OAAOiC,OAAOC,OAC9B,WACA,IAAIE,aAAcC,OAAOL,IAE3B,OAAOM,MAAMC,KAAK,IAAIC,WAAWL,IAC9BM,IAAKC,GAAMA,EAAE/B,SAAS,IAAIgC,SAAS,EAAG,MACtCC,KAAK,GACV,CAAE,MAAArB,GAAO,CAIX,IAAIsB,EAAO,EACX,IAAK,IAAIC,EAAI,EAAGA,EAAId,EAAIe,OAAQD,IAC9BD,GAAQA,GAAQ,GAAKA,EAAOb,EAAIgB,WAAWF,GAC3CD,GAAcA,EAEhB,OAAOrC,KAAKyC,IAAIJ,GAAMlC,SAAS,IAAIgC,SAAS,EAAG,IACjD,CCJSO,CA5BY,CAEjBC,UAAUC,UACVD,UAAUE,UACTF,UAAUG,WAAa,IAAIV,KAAK,KAGjCW,OAAOC,MACPD,OAAOE,OACPF,OAAOG,WACPC,OAAOC,kBAAoB,EAG3BT,UAAUU,qBAAuB,EAChCV,UAAkBW,cAAgB,EACnCX,UAAUY,gBAAkB,EAG5BZ,UAAUa,UACV,IAAI7D,MAAO8D,oBAGXC,UAGMC,KAGiBvB,KAAK,KAChC,CAKA,SAASsB,IACP,IACE,MAAME,EAASC,SAASC,cAAc,UAChCC,EACJH,EAAOI,WAAW,UAAYJ,EAAOI,WAAW,sBAClD,IAAKD,EAAI,MAAO,IAEhB,MAAME,EAAaF,EAAWG,aAAa,6BAC3C,IAAKD,EAAW,MAAO,IAEvB,MAAME,EAAUJ,EAAWK,aAAaH,EAAUI,uBAKlD,OAAOF,EAAS,IAJEJ,EAAWK,aAC3BH,EAAUK,wBAId,CAAE,MAAAvD,GACA,MAAO,GACT,CACF,CAKAO,eAAeqC,IACb,IACE,MAAMC,EAASC,SAASC,cAAc,UACtCF,EAAOZ,MAAQ,IACfY,EAAOX,OAAS,GAEhB,MAAMsB,EAAMX,EAAOI,WAAW,MAC9B,IAAKO,EAAK,MAAO,IAGjBA,EAAIC,aAAe,MACnBD,EAAIE,KAAO,wBACXF,EAAIC,aAAe,aACnBD,EAAIG,UAAY,OAChBH,EAAII,SAAS,IAAK,EAAG,GAAI,IACzBJ,EAAIG,UAAY,OAGhBH,EAAIK,SAAS,qBAAsB,EAAG,IAKtC,OAFgBhB,EAAOiB,YAERC,UAAU,IAAK,IAChC,CAAE,MAAA/D,GACA,MAAO,GACT,CACF,OCvFagE,EAKX,WAAAC,GAHQC,KAAAC,WAA4B,KAIlCD,KAAKE,UAAY5F,IACjB0F,KAAKG,UAAYzF,KAAKC,KACxB,CAEA,YAAAyF,GACE,OAAOJ,KAAKE,SACd,CAEA,WAAAG,GACE,OAAOtF,KAAKE,OAAOP,KAAKC,MAAQqF,KAAKG,WAAa,IACpD,CAEA,aAAAG,CAAcC,GACZP,KAAKC,WAAaM,CACpB,CAEA,aAAAC,GACE,OAAOR,KAAKC,UACd,CAEA,YAAAQ,GACE,OAA2B,OAApBT,KAAKC,UACd,CAEA,KAAAS,GACEV,KAAKE,UAAY5F,IACjB0F,KAAKC,WAAa,KAClBD,KAAKG,UAAYzF,KAAKC,KACxB,CAEA,mBAAAgG,GACEX,KAAKC,WAAa,IACpB,QC7BWW,EAKX,WAAAb,CAAYc,GAHJb,KAAAc,MAAQ,IAAIC,IACZf,KAAAgB,cAAgB,IAAID,IAG1Bf,KAAKa,OAASA,CAChB,CAEA,UAAMI,CAAKhF,GACT,MAAMiF,EAAqB,CACzBX,GAAIjG,IACJ6G,UAAWzG,KAAKC,MAChByG,SAAU,EACVnF,cAEI+D,KAAKqB,UAAUH,EACvB,CAEQ,eAAMG,CAAUH,GACtBA,EAAME,WAEN,IACE,MAAME,EAAU,CACdC,KAAML,EAAMjF,KAAKsF,KACjBtF,KAAM+D,KAAKwB,iBAAiBN,EAAMjF,MAClCwF,MAAOzB,KAAKa,OAAOa,UAIrB,GAAIhE,UAAUiE,YAAkC,aAApBT,EAAMjF,KAAKsF,KAAqB,CAC1D,MAAMK,EAAO,IAAIC,KAAK,CAACC,KAAKC,UAAUT,IAAW,CAC/CC,KAAM,qBAGR,GAAI7D,UAAUiE,WAAW3B,KAAKa,OAAOmB,SAAUJ,GAG7C,OAFA5B,KAAKa,OAAOoB,OAASlG,EAAS,aAAcmF,EAAMjF,WAClD+D,KAAKkC,gBAAgBhB,EAAMX,GAG/B,CAGA,MAAM4B,QAAiBC,MAAMpC,KAAKa,OAAOmB,SAAU,CACjDK,OAAQ,OACRC,QAAS,CACP,eAAgB,mBAChB,cAAetC,KAAKa,OAAOa,UAE7Ba,KAAMT,KAAKC,UAAUT,GACrBkB,WAAW,IAGb,GAAIL,EAASM,GAGX,OAFAzC,KAAKa,OAAOoB,OAASlG,EAAS,aAAcmF,EAAMjF,WAClD+D,KAAKkC,gBAAgBhB,EAAMX,IAK7B,GAAwB,MAApB4B,EAASO,OAAgB,CAC3B,MAAMC,EAAaC,SACjBT,EAASG,QAAQO,IAAI,gBAAkB,KACvC,IAKF,OAHA7C,KAAKa,OAAOoB,OACVlG,EAAS,6BAA6B4G,WACxC3C,KAAK8C,cAAc5B,EAAoB,IAAbyB,EAE5B,CAEA,MAAM,IAAII,MAAM,QAAQZ,EAASO,SACnC,CAAE,MAAOM,GAGP,GAFAhD,KAAKa,OAAOoB,OAASlG,EAAS,cAAe,CAAEiH,QAAO9B,UAElDA,EAAME,SAAWpB,KAAKa,OAAOoC,WAAY,CAC3C,MAAMC,EAAQnI,KAAKoI,IACjBnD,KAAKa,OAAOuC,aAAerI,KAAKsI,IAAI,EAAGnC,EAAME,SAAW,GACxD,KAEFpB,KAAK8C,cAAc5B,EAAOgC,EAC5B,MACElD,KAAKa,OAAOoB,OACVlG,EAAS,iBAAiBmF,EAAME,qBAClCpB,KAAKkC,gBAAgBhB,EAAMX,GAE/B,CACF,CAEQ,gBAAAiB,CAAiBvF,GACvB,MAAMsF,KAAEA,EAAIJ,UAAEA,KAAcmC,GAASrH,EAC/BsH,EAAiC,CAAA,EAEvC,IAAK,MAAOC,EAAKC,KAAUC,OAAOC,QAAQL,QAC1BlH,IAAVqH,IACFF,EAAUC,GAAOC,GAIrB,OAAOF,CACT,CAEQ,aAAAT,CAAc5B,EAAoBgC,GACxClD,KAAKc,MAAM8C,IAAI1C,EAAMX,GAAIW,GAEzB,MAAM2C,EAAkB7D,KAAKgB,cAAc6B,IAAI3B,EAAMX,IACjDsD,GACFC,aAAaD,GAGf,MAAME,EAAUC,WAAW,KACzBhE,KAAKgB,cAAciD,OAAO/C,EAAMX,IAChC,MAAM2D,EAAclE,KAAKc,MAAM+B,IAAI3B,EAAMX,IACrC2D,GACFlE,KAAKqB,UAAU6C,IAEhBhB,GAEHlD,KAAKgB,cAAc4C,IAAI1C,EAAMX,GAAIwD,EACnC,CAEQ,eAAA7B,CAAgBiC,GACtBnE,KAAKc,MAAMmD,OAAOE,GAClB,MAAMJ,EAAU/D,KAAKgB,cAAc6B,IAAIsB,GACnCJ,IACFD,aAAaC,GACb/D,KAAKgB,cAAciD,OAAOE,GAE9B,CAEA,KAAAC,GACE,MAAMC,EAASxH,MAAMC,KAAKkD,KAAKc,MAAMwD,UACrCtE,KAAKc,MAAMyD,QAEX,IAAK,MAAMR,KAAW/D,KAAKgB,cAAcsD,SACvCR,aAAaC,GAEf/D,KAAKgB,cAAcuD,QAEnB,IAAK,MAAMrD,KAASmD,EAClBrE,KAAKqB,UAAUH,EAEnB,CAEA,aAAIsD,GACF,OAAOxE,KAAKc,MAAM2D,IACpB,wBCvIA,WAAA1E,GAHQC,KAAA0E,WAAa,IAAI3D,IACjBf,KAAA2E,aAAc,EAGpB3E,KAAK4E,eAAiB,IAAI9E,CAC5B,CAEA,IAAA+E,CAAKhE,GACH,GAAIb,KAAK2E,YACP9D,EAAOoB,OAASlG,EAAS,2BAD3B,CAKA,GAAsB,oBAAXmC,OACT,MAAM,IAAI6E,MAAM,gCAGlB/C,KAAKa,OAAS,CACZmB,SAAU,gCACV8C,eAAe,EACf7C,OAAO,EACPgB,WAAY,GACZG,aAAc,OACXvC,GAGLb,KAAK+E,UAAY,IAAInE,EAAU,CAC7BoB,SAAUhC,KAAKa,OAAOmB,SACtBN,SAAU1B,KAAKa,OAAOa,SACtBuB,WAAYjD,KAAKa,OAAOoC,WACxBG,aAAcpD,KAAKa,OAAOuC,aAC1BnB,MAAOjC,KAAKa,OAAOoB,QAIrBjC,KAAKgF,mBAAqB1I,IAC1B0D,KAAKgF,mBAAmBC,KAAMC,UAC5BlF,KAAKmF,mBAAqBD,GACf,QAAXpJ,EAAAkE,KAAKa,cAAM,IAAA/E,OAAA,EAAAA,EAAEmG,QAASlG,EAAS,oBAAqBmJ,KAGtDlF,KAAK2E,aAAc,EAGf3E,KAAKa,OAAOiE,gBACc,YAAxBlG,SAASwG,WACXxG,SAASyG,iBAAiB,mBAAoB,IAAMrF,KAAKsF,YAEzDtF,KAAKsF,WAEPtF,KAAKuF,oBAIPvF,KAAKwF,sBAELxF,KAAKa,OAAOoB,OAASlG,EAAS,cAAeiE,KAAKa,OA7ClD,CA8CF,CAEA,cAAMyE,CAASG,GACb,IAAKzF,KAAK0F,UAAW,OAErB,MAAMtK,GAAMqK,aAAO,EAAPA,EAASrK,MAAOuK,SAASC,KAC/BC,GAAWJ,aAAO,EAAPA,EAASI,WAAYjH,SAASiH,SACzCC,GAAQL,aAAO,EAAPA,EAASK,QAASlH,SAASkH,MAEnCC,EAAS5K,EAASC,GAClB4K,EJzBJ,SAA4BH,GAChC,GAAKA,EAEL,IACE,MAAMI,EAAe,IAAI3K,IAAIuK,GAAUK,SAASC,cAGhD,GAAIF,IAAiBN,SAASO,SAASC,cAAe,OAGtD,OAAOF,EAAarL,QAAQ,SAAU,GACxC,CAAE,MAAAkB,GACA,MACF,CACF,CIW2BsK,CAAkBP,GAEnC5J,EAAqB,CACzBsF,KAAM,WACN8E,0BAA2BrG,KAAKsG,wBAChCC,WAAYvG,KAAK4E,eAAexE,eAChChF,MACAQ,KAAMmK,EAAOnK,KACbiK,SAAUA,QAAYzJ,EACtBoK,gBAAiBR,EACjBF,QACAW,WAAYV,EAAOxK,OAAOkL,WAC1BC,WAAYX,EAAOxK,OAAOmL,WAC1BC,aAAcZ,EAAOxK,OAAOoL,aAC5BC,SAAUb,EAAOxK,OAAOqL,SACxBC,YAAad,EAAOxK,OAAOsL,YAC3B1F,UAAWzG,KAAKC,OAGlBqF,KAAK+E,UAAW9D,KAAKhF,EACvB,CAEA,WAAMiF,CAAM4F,EAAmBC,GAC7B,IAAK/G,KAAK0F,YAAcoB,EAAW,OAEnC,MAAM1L,EAAMuK,SAASC,KACfG,EAAS5K,EAASC,GAElBa,EAAqB,CACzBsF,KAAM,QACN8E,0BAA2BrG,KAAKsG,wBAChCC,WAAYvG,KAAK4E,eAAexE,eAChC4G,WAAYF,EACZG,WAAYF,EACZ3L,MACAQ,KAAMmK,EAAOnK,KACbuF,UAAWzG,KAAKC,OAGlBqF,KAAK+E,UAAW9D,KAAKhF,EACvB,CAEA,cAAMiL,CAASjH,SACb,IAAKD,KAAK0F,YAAczF,EAAY,OAEpCD,KAAK4E,eAAetE,cAAcL,GAElC,MAAMhE,EAAqB,CACzBsF,KAAM,WACN8E,0BAA2BrG,KAAKsG,wBAChCC,WAAYvG,KAAK4E,eAAexE,eAChC+G,YAAalH,EACbkB,UAAWzG,KAAKC,OAGlBqF,KAAK+E,UAAW9D,KAAKhF,IACV,QAAXH,EAAAkE,KAAKa,cAAM,IAAA/E,OAAA,EAAAA,EAAEmG,QAASlG,EAAS,kBAAmBkE,EACpD,CAEA,KAAAS,SACEV,KAAK4E,eAAelE,SACT,QAAX5E,EAAAkE,KAAKa,cAAM,IAAA/E,OAAA,EAAAA,EAAEmG,QAASlG,EAAS,gBACjC,CAEA,2BAAMuK,GACJ,OAAItG,KAAKmF,mBAA2BnF,KAAKmF,mBACrCnF,KAAKgF,oBACPhF,KAAKmF,yBAA2BnF,KAAKgF,mBAC9BhF,KAAKmF,qBAEdnF,KAAKgF,mBAAqB1I,IAC1B0D,KAAKmF,yBAA2BnF,KAAKgF,mBAC9BhF,KAAKmF,mBACd,CAEA,YAAA/E,GACE,OAAOJ,KAAK4E,eAAexE,cAC7B,CAEA,YAAAK,GACE,OAAOT,KAAK4E,eAAenE,cAC7B,CAEA,GAAA2G,CAAIC,WACErH,KAAK0E,WAAW4C,IAAID,EAAUE,eAChCzL,EAAAkE,KAAKa,6BAAQoB,QACXlG,EAAS,aAAasL,EAAUE,wBAGpCvH,KAAK0E,WAAWd,IAAIyD,EAAUE,KAAMF,GACpCA,EAAUxC,KAAK7E,eACfwH,EAAAxH,KAAKa,6BAAQoB,QAASlG,EAAS,aAAasL,EAAUE,eACxD,CAEQ,OAAA7B,GACN,QAAK1F,KAAK2E,cACRzI,QAAQuL,KAAK,8BACN,EAGX,CAEQ,gBAAAlC,GAEN,MAAMmC,EAAoBC,QAAQC,UAC5BC,EAAuBF,QAAQG,aAErCH,QAAQC,UAAY,IAAIG,KACtBL,EAAkBM,MAAML,QAASI,GACjC/D,WAAW,IAAMhE,KAAKsF,WAAY,IAGpCqC,QAAQG,aAAe,IAAIC,KACzBF,EAAqBG,MAAML,QAASI,GACpC/D,WAAW,IAAMhE,KAAKsF,WAAY,IAIpCD,iBAAiB,WAAY,KAC3BrB,WAAW,IAAMhE,KAAKsF,WAAY,IAEtC,CAEQ,mBAAAE,GACN,MAAMpB,EAAQ,WAAM,OAAc,QAAdtI,EAAAkE,KAAK+E,iBAAS,IAAAjJ,OAAA,EAAAA,EAAEsI,SAEpCxF,SAASyG,iBAAiB,mBAAoB,KACX,WAA7BzG,SAASqJ,iBAA8B7D,MAG7CiB,iBAAiB,WAAYjB,GAC7BiB,iBAAiB,eAAgBjB,EACnC"}
@@ -0,0 +1,86 @@
1
+ interface PulsoraConfig {
2
+ apiToken: string;
3
+ endpoint?: string;
4
+ autoPageviews?: boolean;
5
+ debug?: boolean;
6
+ maxRetries?: number;
7
+ retryBackoff?: number;
8
+ }
9
+ interface PageviewOptions {
10
+ url?: string;
11
+ referrer?: string;
12
+ title?: string;
13
+ }
14
+ interface EventData {
15
+ [key: string]: any;
16
+ }
17
+ interface PulsoraExtension {
18
+ name: string;
19
+ init(core: PulsoraCore): void;
20
+ }
21
+ interface PulsoraCore {
22
+ init(config: PulsoraConfig): void;
23
+ pageview(options?: PageviewOptions): void;
24
+ event(eventName: string, eventData?: EventData): void;
25
+ identify(customerId: string): void;
26
+ reset(): void;
27
+ getVisitorFingerprint(): Promise<string>;
28
+ getSessionId(): string;
29
+ isIdentified(): boolean;
30
+ use(extension: PulsoraExtension): void;
31
+ }
32
+ interface QueuedEvent {
33
+ id: string;
34
+ timestamp: number;
35
+ attempts: number;
36
+ data: TrackingData;
37
+ }
38
+ interface TrackingData {
39
+ type: 'pageview' | 'event' | 'identify';
40
+ visitor_fingerprint: string;
41
+ session_id: string;
42
+ url?: string;
43
+ path?: string;
44
+ referrer?: string;
45
+ referrer_source?: string;
46
+ title?: string;
47
+ event_name?: string;
48
+ event_data?: EventData;
49
+ customer_id?: string;
50
+ utm_source?: string;
51
+ utm_medium?: string;
52
+ utm_campaign?: string;
53
+ utm_term?: string;
54
+ utm_content?: string;
55
+ timestamp: number;
56
+ }
57
+
58
+ /**
59
+ * Main Pulsora tracker implementation
60
+ * Handles pageview tracking, custom events, and user identification
61
+ */
62
+ declare class Tracker implements PulsoraCore {
63
+ private config?;
64
+ private transport?;
65
+ private sessionManager;
66
+ private visitorFingerprint?;
67
+ private fingerprintPromise?;
68
+ private extensions;
69
+ private initialized;
70
+ constructor();
71
+ init(config: PulsoraConfig): void;
72
+ pageview(options?: PageviewOptions): Promise<void>;
73
+ event(eventName: string, eventData?: EventData): Promise<void>;
74
+ identify(customerId: string): Promise<void>;
75
+ reset(): void;
76
+ getVisitorFingerprint(): Promise<string>;
77
+ getSessionId(): string;
78
+ isIdentified(): boolean;
79
+ use(extension: PulsoraExtension): void;
80
+ private isReady;
81
+ private setupSPATracking;
82
+ private setupUnloadHandlers;
83
+ }
84
+
85
+ export { Tracker as Pulsora };
86
+ export type { EventData, PageviewOptions, PulsoraConfig, PulsoraCore, PulsoraExtension, QueuedEvent, TrackingData };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ function t(){if("undefined"!=typeof crypto&&crypto.randomUUID)return crypto.randomUUID();let t=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const i=(t+16*Math.random())%16|0;return t=Math.floor(t/16),("x"===e?i:3&i|8).toString(16)})}function e(t){try{const e=new URL(t),i={};return e.searchParams.forEach((t,e)=>{i[e]=t}),{path:e.pathname,search:i}}catch(t){return{path:"/",search:{}}}}function i(t,e){"undefined"!=typeof console&&console.log&&(void 0!==e?console.log(`[Pulsora] ${t}`,e):console.log(`[Pulsora] ${t}`))}async function n(){return async function(t){if("undefined"!=typeof crypto&&crypto.subtle&&crypto.subtle.digest)try{const e=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(t));return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}catch(t){}let e=0;for(let i=0;i<t.length;i++)e=(e<<5)-e+t.charCodeAt(i),e&=e;return Math.abs(e).toString(16).padStart(8,"0")}([navigator.userAgent,navigator.language,(navigator.languages||[]).join(","),screen.width,screen.height,screen.colorDepth,window.devicePixelRatio||1,navigator.hardwareConcurrency||0,navigator.deviceMemory||0,navigator.maxTouchPoints||0,navigator.platform,(new Date).getTimezoneOffset(),s(),await r()].join("~"))}function s(){try{const t=document.createElement("canvas"),e=t.getContext("webgl")||t.getContext("experimental-webgl");if(!e)return"0";const i=e.getExtension("WEBGL_debug_renderer_info");if(!i)return"1";const n=e.getParameter(i.UNMASKED_VENDOR_WEBGL);return n+"~"+e.getParameter(i.UNMASKED_RENDERER_WEBGL)}catch(t){return"2"}}async function r(){try{const t=document.createElement("canvas");t.width=200,t.height=20;const e=t.getContext("2d");if(!e)return"0";e.textBaseline="top",e.font="14px 'PulsoraFont123'",e.textBaseline="alphabetic",e.fillStyle="#f60",e.fillRect(125,1,62,20),e.fillStyle="#069",e.fillText("Cwm fjord 🎨 glyph",2,15);return t.toDataURL().substring(100,200)}catch(t){return"1"}}class o{constructor(){this.customerId=null,this.sessionId=t(),this.startTime=Date.now()}getSessionId(){return this.sessionId}getDuration(){return Math.floor((Date.now()-this.startTime)/1e3)}setCustomerId(t){this.customerId=t}getCustomerId(){return this.customerId}isIdentified(){return null!==this.customerId}reset(){this.sessionId=t(),this.customerId=null,this.startTime=Date.now()}clearIdentification(){this.customerId=null}}class a{constructor(t){this.queue=new Map,this.retryTimeouts=new Map,this.config=t}async send(e){const i={id:t(),timestamp:Date.now(),attempts:0,data:e};await this.sendEvent(i)}async sendEvent(t){t.attempts++;try{const e={type:t.data.type,data:this.prepareEventData(t.data),token:this.config.apiToken};if(navigator.sendBeacon&&"identify"!==t.data.type){const n=new Blob([JSON.stringify(e)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,n))return this.config.debug&&i("Event sent",t.data),void this.removeFromQueue(t.id)}const n=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(e),keepalive:!0});if(n.ok)return this.config.debug&&i("Event sent",t.data),void this.removeFromQueue(t.id);if(429===n.status){const e=parseInt(n.headers.get("Retry-After")||"60",10);return this.config.debug&&i(`Rate limited, retry after ${e}s`),void this.scheduleRetry(t,1e3*e)}throw new Error(`HTTP ${n.status}`)}catch(e){if(this.config.debug&&i("Send failed",{error:e,event:t}),t.attempts<this.config.maxRetries){const e=Math.min(this.config.retryBackoff*Math.pow(2,t.attempts-1),3e4);this.scheduleRetry(t,e)}else this.config.debug&&i(`Dropped after ${t.attempts} attempts`),this.removeFromQueue(t.id)}}prepareEventData(t){const{type:e,timestamp:i,...n}=t,s={};for(const[t,e]of Object.entries(n))void 0!==e&&(s[t]=e);return s}scheduleRetry(t,e){this.queue.set(t.id,t);const i=this.retryTimeouts.get(t.id);i&&clearTimeout(i);const n=setTimeout(()=>{this.retryTimeouts.delete(t.id);const e=this.queue.get(t.id);e&&this.sendEvent(e)},e);this.retryTimeouts.set(t.id,n)}removeFromQueue(t){this.queue.delete(t);const e=this.retryTimeouts.get(t);e&&(clearTimeout(e),this.retryTimeouts.delete(t))}flush(){const t=Array.from(this.queue.values());this.queue.clear();for(const t of this.retryTimeouts.values())clearTimeout(t);this.retryTimeouts.clear();for(const e of t)this.sendEvent(e)}get queueSize(){return this.queue.size}}class d{constructor(){this.extensions=new Map,this.initialized=!1,this.sessionManager=new o}init(t){if(this.initialized)t.debug&&i("Already initialized");else{if("undefined"==typeof window)throw new Error("Browser environment required");this.config={endpoint:"https://api.pulsora.co/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...t},this.transport=new a({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),this.fingerprintPromise=n(),this.fingerprintPromise.then(t=>{var e;this.visitorFingerprint=t,(null===(e=this.config)||void 0===e?void 0:e.debug)&&i("Fingerprint ready",t)}),this.initialized=!0,this.config.autoPageviews&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.pageview()):this.pageview(),this.setupSPATracking()),this.setupUnloadHandlers(),this.config.debug&&i("Initialized",this.config)}}async pageview(t){if(!this.isReady())return;const i=(null==t?void 0:t.url)||location.href,n=(null==t?void 0:t.referrer)||document.referrer,s=(null==t?void 0:t.title)||document.title,r=e(i),o=function(t){if(t)try{const e=new URL(t).hostname.toLowerCase();if(e===location.hostname.toLowerCase())return;return e.replace(/^www\./,"")}catch(t){return}}(n),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),url:i,path:r.path,referrer:n||void 0,referrer_source:o,title:s,utm_source:r.search.utm_source,utm_medium:r.search.utm_medium,utm_campaign:r.search.utm_campaign,utm_term:r.search.utm_term,utm_content:r.search.utm_content,timestamp:Date.now()};this.transport.send(a)}async event(t,i){if(!this.isReady()||!t)return;const n=location.href,s=e(n),r={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),event_name:t,event_data:i,url:n,path:s.path,timestamp:Date.now()};this.transport.send(r)}async identify(t){var e;if(!this.isReady()||!t)return;this.sessionManager.setCustomerId(t);const n={type:"identify",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),customer_id:t,timestamp:Date.now()};this.transport.send(n),(null===(e=this.config)||void 0===e?void 0:e.debug)&&i("User identified",t)}reset(){var t;this.sessionManager.reset(),(null===(t=this.config)||void 0===t?void 0:t.debug)&&i("Session reset")}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?(this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint):(this.fingerprintPromise=n(),this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint)}getSessionId(){return this.sessionManager.getSessionId()}isIdentified(){return this.sessionManager.isIdentified()}use(t){var e,n;this.extensions.has(t.name)?(null===(e=this.config)||void 0===e?void 0:e.debug)&&i(`Extension ${t.name} already loaded`):(this.extensions.set(t.name,t),t.init(this),(null===(n=this.config)||void 0===n?void 0:n.debug)&&i(`Extension ${t.name} loaded`))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Not initialized"),!1)}setupSPATracking(){const t=history.pushState,e=history.replaceState;history.pushState=(...e)=>{t.apply(history,e),setTimeout(()=>this.pageview(),0)},history.replaceState=(...t)=>{e.apply(history,t),setTimeout(()=>this.pageview(),0)},addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const t=()=>{var t;return null===(t=this.transport)||void 0===t?void 0:t.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&t()}),addEventListener("pagehide",t),addEventListener("beforeunload",t)}}export{d as Pulsora};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/utils.ts","../src/fingerprint.ts","../src/session.ts","../src/transport.ts","../src/tracker.ts"],"sourcesContent":["/**\n * Generate a UUID v4\n */\nexport function generateUUID(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Compact fallback for older browsers\n let d = Date.now();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (d + Math.random() * 16) % 16 | 0;\n d = Math.floor(d / 16);\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n/**\n * SHA-256 hash function with fallback\n */\nexport async function sha256(str: string): Promise<string> {\n if (typeof crypto !== 'undefined' && crypto.subtle && crypto.subtle.digest) {\n try {\n const buf = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(str),\n );\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n } catch {}\n }\n\n // Fallback: simple hash (good enough for fingerprinting)\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash << 5) - hash + str.charCodeAt(i);\n hash = hash & hash;\n }\n return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\n/**\n * Parse URL and extract components\n */\nexport function parseUrl(url: string): {\n path: string;\n search: Record<string, string>;\n} {\n try {\n const u = new URL(url);\n const search: Record<string, string> = {};\n u.searchParams.forEach((v, k) => {\n search[k] = v;\n });\n return { path: u.pathname, search };\n } catch {\n return { path: '/', search: {} };\n }\n}\n\n/**\n * Get referrer source from URL\n * Returns the domain name for ANY external referrer\n */\nexport function getReferrerSource(referrer: string): string | undefined {\n if (!referrer) return;\n\n try {\n const referrerHost = new URL(referrer).hostname.toLowerCase();\n\n // Skip if same domain\n if (referrerHost === location.hostname.toLowerCase()) return;\n\n // Return the clean hostname (remove www. prefix if present)\n return referrerHost.replace(/^www\\./, '');\n } catch {\n return undefined;\n }\n}\n\n/**\n * Simple debounce function\n */\nexport function debounce<T extends (...args: any[]) => void>(\n fn: T,\n delay: number,\n): T {\n let timeout: any;\n return ((...args: any[]) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), delay);\n }) as T;\n}\n\n/**\n * Check if we're in a browser environment\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/**\n * Debug logging\n */\nexport function debugLog(message: string, data?: any): void {\n if (typeof console !== 'undefined' && console.log) {\n if (data !== undefined) {\n console.log(`[Pulsora] ${message}`, data);\n } else {\n console.log(`[Pulsora] ${message}`);\n }\n }\n}\n","import { sha256 } from './utils';\n\n/**\n * Generates a unique, stable browser fingerprint for tracking\n * Uses multiple browser characteristics to create a highly unique identifier\n * GDPR compliant - no PII is collected\n */\nexport async function generateFingerprint(): Promise<string> {\n const components = [\n // User agent and language (high entropy)\n navigator.userAgent,\n navigator.language,\n (navigator.languages || []).join(','),\n\n // Screen (very high entropy, cheap)\n screen.width,\n screen.height,\n screen.colorDepth,\n window.devicePixelRatio || 1,\n\n // Hardware\n navigator.hardwareConcurrency || 0,\n (navigator as any).deviceMemory || 0,\n navigator.maxTouchPoints || 0,\n\n // Browser/OS\n navigator.platform,\n new Date().getTimezoneOffset(),\n\n // WebGL (high entropy)\n getWebGLFingerprint(),\n\n // Canvas (high entropy, optimized)\n await getCanvasFingerprint(),\n ];\n\n return sha256(components.join('~'));\n}\n\n/**\n * WebGL fingerprinting - vendor and renderer info\n */\nfunction getWebGLFingerprint(): string {\n try {\n const canvas = document.createElement('canvas');\n const gl =\n canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) return '0';\n\n const debugInfo = (gl as any).getExtension('WEBGL_debug_renderer_info');\n if (!debugInfo) return '1';\n\n const vendor = (gl as any).getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);\n const renderer = (gl as any).getParameter(\n debugInfo.UNMASKED_RENDERER_WEBGL,\n );\n\n return vendor + '~' + renderer;\n } catch {\n return '2';\n }\n}\n\n/**\n * Canvas fingerprinting - optimized for size\n */\nasync function getCanvasFingerprint(): Promise<string> {\n try {\n const canvas = document.createElement('canvas');\n canvas.width = 200;\n canvas.height = 20;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return '0';\n\n // Use non-existent font to force system fallback (high entropy)\n ctx.textBaseline = 'top';\n ctx.font = \"14px 'PulsoraFont123'\";\n ctx.textBaseline = 'alphabetic';\n ctx.fillStyle = '#f60';\n ctx.fillRect(125, 1, 62, 20);\n ctx.fillStyle = '#069';\n\n // Text with emoji for extra entropy\n ctx.fillText('Cwm fjord 🎨 glyph', 2, 15);\n\n // Extract just a portion of the data URL to save space\n const dataUrl = canvas.toDataURL();\n // Take middle portion for better entropy\n return dataUrl.substring(100, 200);\n } catch {\n return '1';\n }\n}\n","import { generateUUID } from './utils';\n\n/**\n * Session manager for tracking user sessions\n * Handles session ID generation and customer identification\n */\nexport class SessionManager {\n private sessionId: string;\n private customerId: string | null = null;\n private startTime: number;\n\n constructor() {\n this.sessionId = generateUUID();\n this.startTime = Date.now();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n getDuration(): number {\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n setCustomerId(id: string): void {\n this.customerId = id;\n }\n\n getCustomerId(): string | null {\n return this.customerId;\n }\n\n isIdentified(): boolean {\n return this.customerId !== null;\n }\n\n reset(): void {\n this.sessionId = generateUUID();\n this.customerId = null;\n this.startTime = Date.now();\n }\n\n clearIdentification(): void {\n this.customerId = null;\n }\n}\n","import { QueuedEvent, TrackingData } from './types';\nimport { debugLog, generateUUID } from './utils';\n\nexport interface TransportConfig {\n endpoint: string;\n apiToken: string;\n maxRetries: number;\n retryBackoff: number;\n debug: boolean;\n}\n\n/**\n * Transport layer for sending events to the API\n * Handles retries, queuing, and network failures\n */\nexport class Transport {\n private config: TransportConfig;\n private queue = new Map<string, QueuedEvent>();\n private retryTimeouts = new Map<string, any>();\n\n constructor(config: TransportConfig) {\n this.config = config;\n }\n\n async send(data: TrackingData): Promise<void> {\n const event: QueuedEvent = {\n id: generateUUID(),\n timestamp: Date.now(),\n attempts: 0,\n data,\n };\n await this.sendEvent(event);\n }\n\n private async sendEvent(event: QueuedEvent): Promise<void> {\n event.attempts++;\n\n try {\n const payload = {\n type: event.data.type,\n data: this.prepareEventData(event.data),\n token: this.config.apiToken,\n };\n\n // Try sendBeacon first (except for identify)\n if (navigator.sendBeacon && event.data.type !== 'identify') {\n const blob = new Blob([JSON.stringify(payload)], {\n type: 'application/json',\n });\n\n if (navigator.sendBeacon(this.config.endpoint, blob)) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n }\n\n // Fallback to fetch\n const response = await fetch(this.config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Token': this.config.apiToken,\n },\n body: JSON.stringify(payload),\n keepalive: true,\n });\n\n if (response.ok) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n\n // Rate limit\n if (response.status === 429) {\n const retryAfter = parseInt(\n response.headers.get('Retry-After') || '60',\n 10,\n );\n this.config.debug &&\n debugLog(`Rate limited, retry after ${retryAfter}s`);\n this.scheduleRetry(event, retryAfter * 1000);\n return;\n }\n\n throw new Error(`HTTP ${response.status}`);\n } catch (error) {\n this.config.debug && debugLog('Send failed', { error, event });\n\n if (event.attempts < this.config.maxRetries) {\n const delay = Math.min(\n this.config.retryBackoff * Math.pow(2, event.attempts - 1),\n 30000,\n );\n this.scheduleRetry(event, delay);\n } else {\n this.config.debug &&\n debugLog(`Dropped after ${event.attempts} attempts`);\n this.removeFromQueue(event.id);\n }\n }\n }\n\n private prepareEventData(data: TrackingData): Record<string, any> {\n const { type, timestamp, ...rest } = data;\n const cleanData: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(rest)) {\n if (value !== undefined) {\n cleanData[key] = value;\n }\n }\n\n return cleanData;\n }\n\n private scheduleRetry(event: QueuedEvent, delay: number): void {\n this.queue.set(event.id, event);\n\n const existingTimeout = this.retryTimeouts.get(event.id);\n if (existingTimeout) {\n clearTimeout(existingTimeout);\n }\n\n const timeout = setTimeout(() => {\n this.retryTimeouts.delete(event.id);\n const queuedEvent = this.queue.get(event.id);\n if (queuedEvent) {\n this.sendEvent(queuedEvent);\n }\n }, delay);\n\n this.retryTimeouts.set(event.id, timeout);\n }\n\n private removeFromQueue(eventId: string): void {\n this.queue.delete(eventId);\n const timeout = this.retryTimeouts.get(eventId);\n if (timeout) {\n clearTimeout(timeout);\n this.retryTimeouts.delete(eventId);\n }\n }\n\n flush(): void {\n const events = Array.from(this.queue.values());\n this.queue.clear();\n\n for (const timeout of this.retryTimeouts.values()) {\n clearTimeout(timeout);\n }\n this.retryTimeouts.clear();\n\n for (const event of events) {\n this.sendEvent(event);\n }\n }\n\n get queueSize(): number {\n return this.queue.size;\n }\n}\n","import { generateFingerprint } from './fingerprint';\nimport { SessionManager } from './session';\nimport { Transport } from './transport';\nimport {\n EventData,\n PageviewOptions,\n PulsoraConfig,\n PulsoraCore,\n PulsoraExtension,\n TrackingData,\n} from './types';\nimport { debugLog, getReferrerSource, parseUrl } from './utils';\n\n/**\n * Main Pulsora tracker implementation\n * Handles pageview tracking, custom events, and user identification\n */\nexport class Tracker implements PulsoraCore {\n private config?: PulsoraConfig;\n private transport?: Transport;\n private sessionManager: SessionManager;\n private visitorFingerprint?: string;\n private fingerprintPromise?: Promise<string>;\n private extensions = new Map<string, PulsoraExtension>();\n private initialized = false;\n\n constructor() {\n this.sessionManager = new SessionManager();\n }\n\n init(config: PulsoraConfig): void {\n if (this.initialized) {\n config.debug && debugLog('Already initialized');\n return;\n }\n\n if (typeof window === 'undefined') {\n throw new Error('Browser environment required');\n }\n\n this.config = {\n endpoint: 'https://api.pulsora.co/ingest',\n autoPageviews: true,\n debug: false,\n maxRetries: 10,\n retryBackoff: 1000,\n ...config,\n };\n\n this.transport = new Transport({\n endpoint: this.config.endpoint!,\n apiToken: this.config.apiToken,\n maxRetries: this.config.maxRetries!,\n retryBackoff: this.config.retryBackoff!,\n debug: this.config.debug!,\n });\n\n // Start fingerprint generation\n this.fingerprintPromise = generateFingerprint();\n this.fingerprintPromise.then((fingerprint) => {\n this.visitorFingerprint = fingerprint;\n this.config?.debug && debugLog('Fingerprint ready', fingerprint);\n });\n\n this.initialized = true;\n\n // Auto pageviews\n if (this.config.autoPageviews) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => this.pageview());\n } else {\n this.pageview();\n }\n this.setupSPATracking();\n }\n\n // Flush on unload\n this.setupUnloadHandlers();\n\n this.config.debug && debugLog('Initialized', this.config);\n }\n\n async pageview(options?: PageviewOptions): Promise<void> {\n if (!this.isReady()) return;\n\n const url = options?.url || location.href;\n const referrer = options?.referrer || document.referrer;\n const title = options?.title || document.title;\n\n const parsed = parseUrl(url);\n const referrerSource = getReferrerSource(referrer);\n\n const data: TrackingData = {\n type: 'pageview',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n url,\n path: parsed.path,\n referrer: referrer || undefined,\n referrer_source: referrerSource,\n title,\n utm_source: parsed.search.utm_source,\n utm_medium: parsed.search.utm_medium,\n utm_campaign: parsed.search.utm_campaign,\n utm_term: parsed.search.utm_term,\n utm_content: parsed.search.utm_content,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async event(eventName: string, eventData?: EventData): Promise<void> {\n if (!this.isReady() || !eventName) return;\n\n const url = location.href;\n const parsed = parseUrl(url);\n\n const data: TrackingData = {\n type: 'event',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n event_name: eventName,\n event_data: eventData,\n url,\n path: parsed.path,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async identify(customerId: string): Promise<void> {\n if (!this.isReady() || !customerId) return;\n\n this.sessionManager.setCustomerId(customerId);\n\n const data: TrackingData = {\n type: 'identify',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n customer_id: customerId,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n this.config?.debug && debugLog('User identified', customerId);\n }\n\n reset(): void {\n this.sessionManager.reset();\n this.config?.debug && debugLog('Session reset');\n }\n\n async getVisitorFingerprint(): Promise<string> {\n if (this.visitorFingerprint) return this.visitorFingerprint;\n if (this.fingerprintPromise) {\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n this.fingerprintPromise = generateFingerprint();\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n getSessionId(): string {\n return this.sessionManager.getSessionId();\n }\n\n isIdentified(): boolean {\n return this.sessionManager.isIdentified();\n }\n\n use(extension: PulsoraExtension): void {\n if (this.extensions.has(extension.name)) {\n this.config?.debug &&\n debugLog(`Extension ${extension.name} already loaded`);\n return;\n }\n this.extensions.set(extension.name, extension);\n extension.init(this);\n this.config?.debug && debugLog(`Extension ${extension.name} loaded`);\n }\n\n private isReady(): boolean {\n if (!this.initialized) {\n console.warn('[Pulsora] Not initialized');\n return false;\n }\n return true;\n }\n\n private setupSPATracking(): void {\n // Intercept pushState/replaceState\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n history.pushState = (...args) => {\n originalPushState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n history.replaceState = (...args) => {\n originalReplaceState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Back/forward navigation\n addEventListener('popstate', () => {\n setTimeout(() => this.pageview(), 0);\n });\n }\n\n private setupUnloadHandlers(): void {\n const flush = () => this.transport?.flush();\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush();\n });\n\n addEventListener('pagehide', flush);\n addEventListener('beforeunload', flush);\n }\n}\n"],"names":["generateUUID","crypto","randomUUID","d","Date","now","replace","c","r","Math","random","floor","toString","parseUrl","url","u","URL","search","searchParams","forEach","v","k","path","pathname","_a","debugLog","message","data","console","log","undefined","async","generateFingerprint","str","subtle","digest","buf","TextEncoder","encode","Array","from","Uint8Array","map","b","padStart","join","hash","i","length","charCodeAt","abs","sha256","navigator","userAgent","language","languages","screen","width","height","colorDepth","window","devicePixelRatio","hardwareConcurrency","deviceMemory","maxTouchPoints","platform","getTimezoneOffset","getWebGLFingerprint","getCanvasFingerprint","canvas","document","createElement","gl","getContext","debugInfo","getExtension","vendor","getParameter","UNMASKED_VENDOR_WEBGL","UNMASKED_RENDERER_WEBGL","ctx","textBaseline","font","fillStyle","fillRect","fillText","toDataURL","substring","SessionManager","constructor","this","customerId","sessionId","startTime","getSessionId","getDuration","setCustomerId","id","getCustomerId","isIdentified","reset","clearIdentification","Transport","config","queue","Map","retryTimeouts","send","event","timestamp","attempts","sendEvent","payload","type","prepareEventData","token","apiToken","sendBeacon","blob","Blob","JSON","stringify","endpoint","debug","removeFromQueue","response","fetch","method","headers","body","keepalive","ok","status","retryAfter","parseInt","get","scheduleRetry","Error","error","maxRetries","delay","min","retryBackoff","pow","rest","cleanData","key","value","Object","entries","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","eventId","flush","events","values","clear","queueSize","size","Tracker","extensions","initialized","sessionManager","init","autoPageviews","transport","fingerprintPromise","then","fingerprint","visitorFingerprint","readyState","addEventListener","pageview","setupSPATracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","parsed","referrerSource","referrerHost","hostname","toLowerCase","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","utm_source","utm_medium","utm_campaign","utm_term","utm_content","eventName","eventData","event_name","event_data","identify","customer_id","use","extension","has","name","_b","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","visibilityState"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ !function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).Pulsora={})}(this,function(t){"use strict";function i(){return"undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const i=16*Math.random()|0;return("x"===t?i:3&i|8).toString(16)})}async function e(t){if("undefined"!=typeof crypto&&crypto.subtle&&crypto.subtle.digest)try{const i=(new TextEncoder).encode(t),e=await crypto.subtle.digest("SHA-256",i);return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}catch(t){}let i=0;for(let e=0;e<t.length;e++){i=(i<<5)-i+t.charCodeAt(e),i&=i}return Math.abs(i).toString(16).padStart(8,"0")}function n(t){try{const i=new URL(t),e=new URLSearchParams(i.search);return{path:i.pathname,utm_source:e.get("utm_source")||void 0,utm_medium:e.get("utm_medium")||void 0,utm_campaign:e.get("utm_campaign")||void 0,utm_term:e.get("utm_term")||void 0,utm_content:e.get("utm_content")||void 0}}catch(t){return{path:"/"}}}function o(t,i){"undefined"!=typeof console&&console.log}async function s(){const t=await async function(){return{userAgent:navigator.userAgent||"",language:navigator.language||"",languages:(navigator.languages||[]).join(","),platform:navigator.platform||"",screenResolution:`${screen.width}x${screen.height}`,screenColorDepth:screen.colorDepth||0,timezoneOffset:(new Date).getTimezoneOffset(),hardwareConcurrency:navigator.hardwareConcurrency||0,deviceMemory:navigator.deviceMemory,maxTouchPoints:navigator.maxTouchPoints||0,canvas:await r(),webgl:a(),audio:u(),fonts:c()}}();return e(Object.values(t).join("|"))}async function r(){try{const t=document.createElement("canvas"),i=t.getContext("2d");if(!i)return"";t.width=280,t.height=60,i.fillStyle="#f60",i.fillRect(125,1,62,20),i.fillStyle="#069",i.font="11pt Arial",i.fillText("Pulsora Analytics 🚀",2,15),i.fillStyle="rgba(102, 204, 0, 0.7)",i.font="18pt Arial",i.fillText("Analytics",4,45),i.globalCompositeOperation="multiply",i.fillStyle="rgb(255,0,255)",i.beginPath(),i.arc(50,50,50,0,2*Math.PI,!0),i.closePath(),i.fill(),i.fillStyle="rgb(0,255,255)",i.beginPath(),i.arc(100,50,50,0,2*Math.PI,!0),i.closePath(),i.fill(),i.fillStyle="rgb(255,255,0)",i.beginPath(),i.arc(75,100,50,0,2*Math.PI,!0),i.closePath(),i.fill();const n=t.toDataURL();return await e(n)}catch(t){return""}}function a(){try{const t=document.createElement("canvas"),i=t.getContext("webgl")||t.getContext("experimental-webgl");if(!i)return"";const e=i.getExtension("WEBGL_debug_renderer_info");if(!e)return"";const n=i.getParameter(e.UNMASKED_VENDOR_WEBGL)||"";return`${n}~${i.getParameter(e.UNMASKED_RENDERER_WEBGL)||""}`}catch(t){return""}}function u(){var t;try{const i=window.AudioContext||window.webkitAudioContext;if(!i)return"";const e=new i,n=e.createOscillator(),o=e.createAnalyser(),s=e.createGain(),r=e.createScriptProcessor(4096,1,1);n.type="triangle",n.frequency.value=1e4,s.gain.value=0,n.connect(o),o.connect(r),r.connect(s),s.connect(e.destination),n.start(0),null===(t=e.startRendering)||void 0===t||t.call(e);const a=[e.sampleRate,e.destination.maxChannelCount,o.frequencyBinCount].join("~");return n.stop(),e.close(),a}catch(t){return""}}function c(){try{const t="mmmmmmmmmmlli",i="72px",e=["monospace","sans-serif","serif"],n=document.createElement("canvas").getContext("2d");if(!n)return"";const o={};for(const s of e)n.font=`${i} ${s}`,o[s]=n.measureText(t).width;const s=["Arial","Verdana","Times New Roman","Courier New","Georgia","Palatino","Garamond","Bookman","Comic Sans MS","Trebuchet MS","Arial Black","Impact","Lucida Sans","Tahoma","Helvetica","Century Gothic","Lucida Console","Futura","Roboto","Ubuntu"],r=[];for(const a of s){let s=!1;for(const r of e){n.font=`${i} '${a}', ${r}`;if(n.measureText(t).width!==o[r]){s=!0;break}}s&&r.push(a)}return r.join(",")}catch(t){return""}}class h{constructor(){this.t=null,this.i=i(),this.o=Date.now()}get sessionId(){return this.i}get duration(){return Math.floor((Date.now()-this.o)/1e3)}setCustomerId(t){this.t=t}get customerId(){return this.t}isIdentified(){return null!==this.t}reset(){this.i=i(),this.t=null,this.o=Date.now()}clearIdentification(){this.t=null}}class d{constructor(t){this.queue=new Map,this.retryTimeouts=new Map,this.config=t}async send(t){const e={id:i(),timestamp:Date.now(),attempts:0,data:t};await this.sendEvent(e)}async sendEvent(t){t.attempts++;try{const i={type:t.data.type,data:this.prepareEventData(t.data),token:this.config.apiToken};if(navigator.sendBeacon&&"identify"!==t.data.type){const e=new Blob([JSON.stringify(i)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,e))return this.config.debug&&o(0,t.data),void this.removeFromQueue(t.id)}const e=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(i),keepalive:!0});if(e.ok)return this.config.debug&&o(0,t.data),void this.removeFromQueue(t.id);if(429===e.status){const i=parseInt(e.headers.get("Retry-After")||"60",10);return this.config.debug&&o(),void this.scheduleRetry(t,1e3*i)}throw new Error(`HTTP ${e.status}: ${e.statusText}`)}catch(i){if(this.config.debug&&o(),t.attempts<this.config.maxRetries){const i=this.calculateBackoff(t.attempts);this.scheduleRetry(t,i)}else this.config.debug&&o(t.attempts,t.data),this.removeFromQueue(t.id)}}prepareEventData(t){const{type:i,timestamp:e,...n}=t,o={};for(const[t,i]of Object.entries(n))void 0!==i&&(o[t]=i);return o}scheduleRetry(t,i){this.queue.set(t.id,t);const e=this.retryTimeouts.get(t.id);e&&clearTimeout(e);const n=setTimeout(()=>{this.retryTimeouts.delete(t.id);const i=this.queue.get(t.id);i&&this.sendEvent(i)},i);this.retryTimeouts.set(t.id,n)}calculateBackoff(t){const i=this.config.retryBackoff,e=Math.min(i*Math.pow(2,t-1),3e4),n=.3*Math.random()*e;return Math.floor(e+n)}removeFromQueue(t){this.queue.delete(t);const i=this.retryTimeouts.get(t);i&&(clearTimeout(i),this.retryTimeouts.delete(t))}flush(){const t=Array.from(this.queue.values());this.queue.clear();for(const t of this.retryTimeouts.values())clearTimeout(t);this.retryTimeouts.clear();for(const i of t)this.sendEvent(i)}get queueSize(){return this.queue.size}}class l{constructor(){this.config=null,this.transport=null,this.visitorFingerprint=null,this.extensions=new Map,this.initialized=!1,this.fingerprintPromise=null,this.session=new h}init(t){if(this.initialized)t.debug&&o();else{if("undefined"==typeof window||"undefined"==typeof document)throw new Error("Pulsora can only be initialized in a browser environment");this.config={endpoint:"https://api.pulsora.co/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...t},this.transport=new d({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),this.fingerprintPromise=s(),this.fingerprintPromise.then(t=>{var i;this.visitorFingerprint=t,(null===(i=this.config)||void 0===i?void 0:i.debug)&&o()}),this.initialized=!0,this.config.autoPageviews&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.pageview()):this.pageview(),this.setupHistoryTracking()),this.setupUnloadHandlers(),this.config.debug&&o(0,this.config)}}async pageview(t){if(!this.isReady())return;const i=(null==t?void 0:t.url)||window.location.href,e=(null==t?void 0:t.referrer)||document.referrer,o=(null==t?void 0:t.title)||document.title,s=n(i),r=function(t){if(t)try{const i=new URL(t).hostname.toLowerCase();if(i===window.location.hostname.toLowerCase())return;const e={"google.":"Google","bing.":"Bing","yahoo.":"Yahoo","duckduckgo.":"DuckDuckGo","facebook.":"Facebook","twitter.":"Twitter","x.com":"Twitter","t.co":"Twitter","linkedin.":"LinkedIn","reddit.":"Reddit","youtube.":"YouTube","instagram.":"Instagram","pinterest.":"Pinterest","tiktok.":"TikTok","github.":"GitHub"};for(const[t,n]of Object.entries(e))if(i.includes(t))return n;return i}catch(t){return}}(e),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.session.sessionId,url:i,path:s.path,referrer:e||void 0,referrer_source:r,title:o,utm_source:s.utm_source,utm_medium:s.utm_medium,utm_campaign:s.utm_campaign,utm_term:s.utm_term,utm_content:s.utm_content,timestamp:Date.now()};this.transport.send(a)}async event(t,i){var e;if(!this.isReady())return;if(!t||"string"!=typeof t)return void((null===(e=this.config)||void 0===e?void 0:e.debug)&&o());const s=window.location.href,r=n(s),a={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.session.sessionId,event_name:t,event_data:i,url:s,path:r.path,timestamp:Date.now()};this.transport.send(a)}async identify(t){var i,e;if(!this.isReady())return;if(!t||"string"!=typeof t)return void((null===(i=this.config)||void 0===i?void 0:i.debug)&&o());this.session.setCustomerId(t);const n={type:"identify",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.session.sessionId,customer_id:t,timestamp:Date.now()};this.transport.send(n),(null===(e=this.config)||void 0===e?void 0:e.debug)&&o()}reset(){var t;this.session.reset(),(null===(t=this.config)||void 0===t?void 0:t.debug)&&o()}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?(this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint):(this.fingerprintPromise=s(),this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint)}getSessionId(){return this.session.sessionId}isIdentified(){return this.session.isIdentified()}use(t){var i,e;this.extensions.has(t.name)?(null===(i=this.config)||void 0===i?void 0:i.debug)&&o(t.name):(this.extensions.set(t.name,t),t.init(this),(null===(e=this.config)||void 0===e?void 0:e.debug)&&o(t.name))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Tracker not initialized. Call init() first."),!1)}setupHistoryTracking(){const t=history.pushState,i=history.replaceState;history.pushState=(...i)=>{t.apply(history,i),setTimeout(()=>this.pageview(),0)},history.replaceState=(...t)=>{i.apply(history,t),setTimeout(()=>this.pageview(),0)},window.addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const t=()=>{this.transport&&this.transport.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&t()}),window.addEventListener("pagehide",t),window.addEventListener("beforeunload",t)}}const f=new l;if("undefined"!=typeof window&&(window.Pulsora=f,document.currentScript)){const t=document.currentScript,i=t.getAttribute("data-token"),e=t.getAttribute("data-endpoint"),n="true"===t.getAttribute("data-debug");i&&f.init({apiToken:i,endpoint:e||void 0,debug:n})}t.Tracker=l,t.default=f,Object.defineProperty(t,"u",{value:!0})});
2
+ //# sourceMappingURL=index.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.js","sources":["../src/utils.ts","../src/fingerprint.ts","../src/session.ts","../src/transport.ts","../src/tracker.ts","../src/index.ts"],"sourcesContent":["/**\n * Generate a UUID v4\n * Uses crypto.randomUUID() if available, otherwise falls back to manual generation\n */\nexport function generateUUID(): string {\n // Use native crypto.randomUUID if available\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Fallback to manual UUID v4 generation\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Simple SHA-256 hash function using Web Crypto API\n * Falls back to a simple hash function if Web Crypto is not available\n */\nexport async function sha256(message: string): Promise<string> {\n // Use Web Crypto API if available\n if (typeof crypto !== 'undefined' && crypto.subtle && crypto.subtle.digest) {\n try {\n const msgBuffer = new TextEncoder().encode(message);\n const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n } catch (e) {\n // Fall through to simple hash\n }\n }\n\n // Fallback to simple hash function (not cryptographically secure, but good enough for fingerprinting)\n let hash = 0;\n for (let i = 0; i < message.length; i++) {\n const char = message.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash; // Convert to 32-bit integer\n }\n return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\n/**\n * Parse URL and extract components\n */\nexport function parseUrl(url: string): {\n path: string;\n utm_source?: string;\n utm_medium?: string;\n utm_campaign?: string;\n utm_term?: string;\n utm_content?: string;\n} {\n try {\n const urlObj = new URL(url);\n const params = new URLSearchParams(urlObj.search);\n\n return {\n path: urlObj.pathname,\n utm_source: params.get('utm_source') || undefined,\n utm_medium: params.get('utm_medium') || undefined,\n utm_campaign: params.get('utm_campaign') || undefined,\n utm_term: params.get('utm_term') || undefined,\n utm_content: params.get('utm_content') || undefined,\n };\n } catch {\n return { path: '/' };\n }\n}\n\n/**\n * Extract referrer source from referrer URL\n */\nexport function getReferrerSource(referrer: string): string | undefined {\n if (!referrer) return undefined;\n\n try {\n const url = new URL(referrer);\n const hostname = url.hostname.toLowerCase();\n\n // Check if it's the same domain\n if (hostname === window.location.hostname.toLowerCase()) {\n return undefined;\n }\n\n // Common referrer sources\n const sources: Record<string, string> = {\n 'google.': 'Google',\n 'bing.': 'Bing',\n 'yahoo.': 'Yahoo',\n 'duckduckgo.': 'DuckDuckGo',\n 'facebook.': 'Facebook',\n 'twitter.': 'Twitter',\n 'x.com': 'Twitter',\n 't.co': 'Twitter',\n 'linkedin.': 'LinkedIn',\n 'reddit.': 'Reddit',\n 'youtube.': 'YouTube',\n 'instagram.': 'Instagram',\n 'pinterest.': 'Pinterest',\n 'tiktok.': 'TikTok',\n 'github.': 'GitHub',\n };\n\n for (const [domain, source] of Object.entries(sources)) {\n if (hostname.includes(domain)) {\n return source;\n }\n }\n\n // Return the hostname for unknown sources\n return hostname;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Get root domain for cross-subdomain tracking\n */\nexport function getRootDomain(): string {\n const hostname = window.location.hostname;\n\n // Handle localhost and IP addresses\n if (hostname === 'localhost' || /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(hostname)) {\n return hostname;\n }\n\n // Split hostname into parts\n const parts = hostname.split('.');\n\n // If we have at least 2 parts, return the last 2\n // This handles most common cases like subdomain.example.com -> example.com\n if (parts.length >= 2) {\n return parts.slice(-2).join('.');\n }\n\n return hostname;\n}\n\n/**\n * Debounce function\n */\nexport function debounce<T extends (...args: any[]) => any>(\n func: T,\n wait: number,\n): (...args: Parameters<T>) => void {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n\n return function (this: any, ...args: Parameters<T>) {\n const context = this;\n\n if (timeout !== null) {\n clearTimeout(timeout);\n }\n\n timeout = setTimeout(() => {\n func.apply(context, args);\n timeout = null;\n }, wait);\n };\n}\n\n/**\n * Check if we're in a browser environment\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/**\n * Safe console log for debug mode\n */\nexport function debugLog(message: string, data?: any): void {\n if (typeof console !== 'undefined' && console.log) {\n if (data) {\n console.log(`[Pulsora] ${message}`, data);\n } else {\n console.log(`[Pulsora] ${message}`);\n }\n }\n}\n","import { sha256 } from './utils';\n\ninterface FingerprintComponents {\n userAgent: string;\n language: string;\n languages: string;\n platform: string;\n screenResolution: string;\n screenColorDepth: number;\n timezoneOffset: number;\n hardwareConcurrency: number;\n deviceMemory: number | undefined;\n maxTouchPoints: number;\n canvas: string;\n webgl: string;\n audio: string;\n fonts: string;\n}\n\n/**\n * Generate a stable browser fingerprint\n * This fingerprint is designed to be unique but not personally identifiable\n */\nexport async function generateFingerprint(): Promise<string> {\n const components = await collectComponents();\n const fingerprintString = Object.values(components).join('|');\n return sha256(fingerprintString);\n}\n\nasync function collectComponents(): Promise<FingerprintComponents> {\n return {\n userAgent: navigator.userAgent || '',\n language: navigator.language || '',\n languages: (navigator.languages || []).join(','),\n platform: navigator.platform || '',\n screenResolution: `${screen.width}x${screen.height}`,\n screenColorDepth: screen.colorDepth || 0,\n timezoneOffset: new Date().getTimezoneOffset(),\n hardwareConcurrency: navigator.hardwareConcurrency || 0,\n deviceMemory: (navigator as any).deviceMemory,\n maxTouchPoints: navigator.maxTouchPoints || 0,\n canvas: await getCanvasFingerprint(),\n webgl: getWebGLFingerprint(),\n audio: getAudioFingerprint(),\n fonts: getFontFingerprint(),\n };\n}\n\n/**\n * Canvas fingerprinting\n */\nasync function getCanvasFingerprint(): Promise<string> {\n try {\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return '';\n\n // Set canvas size\n canvas.width = 280;\n canvas.height = 60;\n\n // Draw text with various styles\n ctx.fillStyle = '#f60';\n ctx.fillRect(125, 1, 62, 20);\n\n ctx.fillStyle = '#069';\n ctx.font = '11pt Arial';\n ctx.fillText('Pulsora Analytics 🚀', 2, 15);\n\n ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';\n ctx.font = '18pt Arial';\n ctx.fillText('Analytics', 4, 45);\n\n // Draw some shapes\n ctx.globalCompositeOperation = 'multiply';\n ctx.fillStyle = 'rgb(255,0,255)';\n ctx.beginPath();\n ctx.arc(50, 50, 50, 0, Math.PI * 2, true);\n ctx.closePath();\n ctx.fill();\n\n ctx.fillStyle = 'rgb(0,255,255)';\n ctx.beginPath();\n ctx.arc(100, 50, 50, 0, Math.PI * 2, true);\n ctx.closePath();\n ctx.fill();\n\n ctx.fillStyle = 'rgb(255,255,0)';\n ctx.beginPath();\n ctx.arc(75, 100, 50, 0, Math.PI * 2, true);\n ctx.closePath();\n ctx.fill();\n\n // Get canvas data\n const dataUrl = canvas.toDataURL();\n return await sha256(dataUrl);\n } catch {\n return '';\n }\n}\n\n/**\n * WebGL fingerprinting\n */\nfunction getWebGLFingerprint(): string {\n try {\n const canvas = document.createElement('canvas');\n const gl =\n canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) return '';\n\n const debugInfo = (gl as any).getExtension('WEBGL_debug_renderer_info');\n if (!debugInfo) return '';\n\n const vendor = (gl as any).getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || '';\n const renderer = (gl as any).getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '';\n\n return `${vendor}~${renderer}`;\n } catch {\n return '';\n }\n}\n\n/**\n * Audio fingerprinting\n */\nfunction getAudioFingerprint(): string {\n try {\n const AudioContext =\n (window as any).AudioContext || (window as any).webkitAudioContext;\n if (!AudioContext) return '';\n\n const context = new AudioContext();\n const oscillator = context.createOscillator();\n const analyser = context.createAnalyser();\n const gainNode = context.createGain();\n const scriptProcessor = context.createScriptProcessor(4096, 1, 1);\n\n oscillator.type = 'triangle';\n oscillator.frequency.value = 10000;\n gainNode.gain.value = 0;\n\n oscillator.connect(analyser);\n analyser.connect(scriptProcessor);\n scriptProcessor.connect(gainNode);\n gainNode.connect(context.destination);\n\n oscillator.start(0);\n context.startRendering?.();\n\n // Simplified audio fingerprint based on context properties\n const fingerprint = [\n context.sampleRate,\n context.destination.maxChannelCount,\n analyser.frequencyBinCount,\n ].join('~');\n\n oscillator.stop();\n context.close();\n\n return fingerprint;\n } catch {\n return '';\n }\n}\n\n/**\n * Font fingerprinting\n */\nfunction getFontFingerprint(): string {\n try {\n const testString = 'mmmmmmmmmmlli';\n const testSize = '72px';\n const baseFonts = ['monospace', 'sans-serif', 'serif'];\n\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return '';\n\n const baseFontWidths: Record<string, number> = {};\n\n // Measure base fonts\n for (const baseFont of baseFonts) {\n ctx.font = `${testSize} ${baseFont}`;\n baseFontWidths[baseFont] = ctx.measureText(testString).width;\n }\n\n // Test fonts\n const testFonts = [\n 'Arial',\n 'Verdana',\n 'Times New Roman',\n 'Courier New',\n 'Georgia',\n 'Palatino',\n 'Garamond',\n 'Bookman',\n 'Comic Sans MS',\n 'Trebuchet MS',\n 'Arial Black',\n 'Impact',\n 'Lucida Sans',\n 'Tahoma',\n 'Helvetica',\n 'Century Gothic',\n 'Lucida Console',\n 'Futura',\n 'Roboto',\n 'Ubuntu',\n ];\n\n const detectedFonts: string[] = [];\n\n for (const font of testFonts) {\n let detected = false;\n\n for (const baseFont of baseFonts) {\n ctx.font = `${testSize} '${font}', ${baseFont}`;\n const width = ctx.measureText(testString).width;\n\n if (width !== baseFontWidths[baseFont]) {\n detected = true;\n break;\n }\n }\n\n if (detected) {\n detectedFonts.push(font);\n }\n }\n\n return detectedFonts.join(',');\n } catch {\n return '';\n }\n}\n","import { generateUUID } from './utils';\n\n/**\n * Session management\n * Sessions are in-memory only and last for the duration of the page\n */\nexport class SessionManager {\n private _sessionId: string;\n private _customerId: string | null = null;\n private _startTime: number;\n\n constructor() {\n this._sessionId = generateUUID();\n this._startTime = Date.now();\n }\n\n /**\n * Get the current session ID\n */\n get sessionId(): string {\n return this._sessionId;\n }\n\n /**\n * Get the session duration in seconds\n */\n get duration(): number {\n return Math.floor((Date.now() - this._startTime) / 1000);\n }\n\n /**\n * Set the customer ID for this session\n */\n setCustomerId(customerId: string): void {\n this._customerId = customerId;\n }\n\n /**\n * Get the customer ID if identified\n */\n get customerId(): string | null {\n return this._customerId;\n }\n\n /**\n * Check if the user is identified\n */\n isIdentified(): boolean {\n return this._customerId !== null;\n }\n\n /**\n * Reset the session (creates a new session)\n */\n reset(): void {\n this._sessionId = generateUUID();\n this._customerId = null;\n this._startTime = Date.now();\n }\n\n /**\n * Clear customer identification\n */\n clearIdentification(): void {\n this._customerId = null;\n }\n}\n","import { QueuedEvent, TrackingData } from './types';\nimport { debugLog, generateUUID } from './utils';\n\nexport interface TransportConfig {\n endpoint: string;\n apiToken: string;\n maxRetries: number;\n retryBackoff: number;\n debug: boolean;\n}\n\n/**\n * Transport layer for sending events to the API\n * Handles retries, queuing, and network failures\n */\nexport class Transport {\n private config: TransportConfig;\n private queue: Map<string, QueuedEvent> = new Map();\n private retryTimeouts: Map<string, ReturnType<typeof setTimeout>> = new Map();\n\n constructor(config: TransportConfig) {\n this.config = config;\n }\n\n /**\n * Send tracking data to the API\n */\n async send(data: TrackingData): Promise<void> {\n const eventId = generateUUID();\n const event: QueuedEvent = {\n id: eventId,\n timestamp: Date.now(),\n attempts: 0,\n data,\n };\n\n await this.sendEvent(event);\n }\n\n /**\n * Send a queued event with retry logic\n */\n private async sendEvent(event: QueuedEvent): Promise<void> {\n event.attempts++;\n\n try {\n // Prepare the payload\n const payload = {\n type: event.data.type,\n data: this.prepareEventData(event.data),\n token: this.config.apiToken,\n };\n\n // Try sendBeacon first for better reliability\n if (navigator.sendBeacon && event.data.type !== 'identify') {\n const blob = new Blob([JSON.stringify(payload)], {\n type: 'application/json',\n });\n\n const success = navigator.sendBeacon(this.config.endpoint, blob);\n\n if (success) {\n if (this.config.debug) {\n debugLog(`Event sent via sendBeacon`, event.data);\n }\n this.removeFromQueue(event.id);\n return;\n }\n }\n\n // Fallback to fetch\n const response = await fetch(this.config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Token': this.config.apiToken,\n },\n body: JSON.stringify(payload),\n keepalive: true, // Allow request to outlive the page\n });\n\n if (response.ok) {\n if (this.config.debug) {\n debugLog(`Event sent successfully`, event.data);\n }\n this.removeFromQueue(event.id);\n return;\n }\n\n // Handle rate limiting\n if (response.status === 429) {\n const retryAfter = parseInt(\n response.headers.get('Retry-After') || '60',\n 10,\n );\n if (this.config.debug) {\n debugLog(`Rate limited, retrying after ${retryAfter}s`);\n }\n this.scheduleRetry(event, retryAfter * 1000);\n return;\n }\n\n // Handle other errors\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n } catch (error) {\n if (this.config.debug) {\n debugLog(`Failed to send event`, { error, event });\n }\n\n // Check if we should retry\n if (event.attempts < this.config.maxRetries) {\n const backoff = this.calculateBackoff(event.attempts);\n this.scheduleRetry(event, backoff);\n } else {\n if (this.config.debug) {\n debugLog(\n `Event dropped after ${event.attempts} attempts`,\n event.data,\n );\n }\n this.removeFromQueue(event.id);\n }\n }\n }\n\n /**\n * Prepare event data for sending\n */\n private prepareEventData(data: TrackingData): Record<string, any> {\n const { type, timestamp, ...eventData } = data;\n\n // Remove undefined values\n const cleanData: Record<string, any> = {};\n for (const [key, value] of Object.entries(eventData)) {\n if (value !== undefined) {\n cleanData[key] = value;\n }\n }\n\n return cleanData;\n }\n\n /**\n * Schedule a retry for a failed event\n */\n private scheduleRetry(event: QueuedEvent, delayMs: number): void {\n // Add to queue\n this.queue.set(event.id, event);\n\n // Clear any existing retry timeout\n const existingTimeout = this.retryTimeouts.get(event.id);\n if (existingTimeout) {\n clearTimeout(existingTimeout);\n }\n\n // Schedule retry\n const timeout = setTimeout(() => {\n this.retryTimeouts.delete(event.id);\n const queuedEvent = this.queue.get(event.id);\n if (queuedEvent) {\n this.sendEvent(queuedEvent);\n }\n }, delayMs);\n\n this.retryTimeouts.set(event.id, timeout);\n }\n\n /**\n * Calculate exponential backoff delay\n */\n private calculateBackoff(attempts: number): number {\n const baseDelay = this.config.retryBackoff;\n const maxDelay = 30000; // 30 seconds max\n const delay = Math.min(baseDelay * Math.pow(2, attempts - 1), maxDelay);\n\n // Add some jitter to prevent thundering herd\n const jitter = Math.random() * 0.3 * delay;\n return Math.floor(delay + jitter);\n }\n\n /**\n * Remove event from queue and cancel any pending retries\n */\n private removeFromQueue(eventId: string): void {\n this.queue.delete(eventId);\n\n const timeout = this.retryTimeouts.get(eventId);\n if (timeout) {\n clearTimeout(timeout);\n this.retryTimeouts.delete(eventId);\n }\n }\n\n /**\n * Flush all queued events (best effort)\n */\n flush(): void {\n const events = Array.from(this.queue.values());\n this.queue.clear();\n\n // Clear all retry timeouts\n for (const timeout of this.retryTimeouts.values()) {\n clearTimeout(timeout);\n }\n this.retryTimeouts.clear();\n\n // Try to send all queued events\n for (const event of events) {\n this.sendEvent(event);\n }\n }\n\n /**\n * Get the number of queued events\n */\n get queueSize(): number {\n return this.queue.size;\n }\n}\n","import { generateFingerprint } from './fingerprint';\nimport { SessionManager } from './session';\nimport { Transport } from './transport';\nimport {\n EventData,\n PageviewOptions,\n PulsoraConfig,\n PulsoraCore,\n PulsoraExtension,\n TrackingData,\n} from './types';\nimport { debugLog, getReferrerSource, isBrowser, parseUrl } from './utils';\n\n/**\n * Main Pulsora tracker implementation\n */\nexport class Tracker implements PulsoraCore {\n private config: PulsoraConfig | null = null;\n private transport: Transport | null = null;\n private session: SessionManager;\n private visitorFingerprint: string | null = null;\n private extensions: Map<string, PulsoraExtension> = new Map();\n private initialized = false;\n private fingerprintPromise: Promise<string> | null = null;\n\n constructor() {\n this.session = new SessionManager();\n }\n\n /**\n * Initialize the tracker\n */\n init(config: PulsoraConfig): void {\n if (this.initialized) {\n if (config.debug) {\n debugLog('Pulsora already initialized');\n }\n return;\n }\n\n if (!isBrowser()) {\n throw new Error(\n 'Pulsora can only be initialized in a browser environment',\n );\n }\n\n this.config = {\n endpoint: 'https://api.pulsora.co/ingest',\n autoPageviews: true,\n debug: false,\n maxRetries: 10,\n retryBackoff: 1000,\n ...config,\n };\n\n this.transport = new Transport({\n endpoint: this.config.endpoint!,\n apiToken: this.config.apiToken,\n maxRetries: this.config.maxRetries!,\n retryBackoff: this.config.retryBackoff!,\n debug: this.config.debug!,\n });\n\n // Start fingerprint generation\n this.fingerprintPromise = generateFingerprint();\n this.fingerprintPromise.then((fp) => {\n this.visitorFingerprint = fp;\n if (this.config?.debug) {\n debugLog('Visitor fingerprint generated', fp);\n }\n });\n\n this.initialized = true;\n\n // Track initial pageview if enabled\n if (this.config.autoPageviews) {\n // Wait for DOM to be ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => this.pageview());\n } else {\n this.pageview();\n }\n\n // Track pageviews on history changes (for SPAs)\n this.setupHistoryTracking();\n }\n\n // Flush events on page unload\n this.setupUnloadHandlers();\n\n if (this.config.debug) {\n debugLog('Pulsora initialized', this.config);\n }\n }\n\n /**\n * Track a pageview\n */\n async pageview(options?: PageviewOptions): Promise<void> {\n if (!this.isReady()) return;\n\n const url = options?.url || window.location.href;\n const referrer = options?.referrer || document.referrer;\n const title = options?.title || document.title;\n\n const urlData = parseUrl(url);\n const referrerSource = getReferrerSource(referrer);\n\n const data: TrackingData = {\n type: 'pageview',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.session.sessionId,\n url,\n path: urlData.path,\n referrer: referrer || undefined,\n referrer_source: referrerSource,\n title,\n utm_source: urlData.utm_source,\n utm_medium: urlData.utm_medium,\n utm_campaign: urlData.utm_campaign,\n utm_term: urlData.utm_term,\n utm_content: urlData.utm_content,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n /**\n * Track a custom event\n */\n async event(eventName: string, eventData?: EventData): Promise<void> {\n if (!this.isReady()) return;\n\n if (!eventName || typeof eventName !== 'string') {\n if (this.config?.debug) {\n debugLog('Invalid event name', eventName);\n }\n return;\n }\n\n const url = window.location.href;\n const urlData = parseUrl(url);\n\n const data: TrackingData = {\n type: 'event',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.session.sessionId,\n event_name: eventName,\n event_data: eventData,\n url,\n path: urlData.path,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n /**\n * Identify a user\n */\n async identify(customerId: string): Promise<void> {\n if (!this.isReady()) return;\n\n if (!customerId || typeof customerId !== 'string') {\n if (this.config?.debug) {\n debugLog('Invalid customer ID', customerId);\n }\n return;\n }\n\n // Set customer ID in session\n this.session.setCustomerId(customerId);\n\n // Send identify event\n const data: TrackingData = {\n type: 'identify',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.session.sessionId,\n customer_id: customerId,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n\n if (this.config?.debug) {\n debugLog('User identified', customerId);\n }\n }\n\n /**\n * Reset user identification\n */\n reset(): void {\n this.session.reset();\n if (this.config?.debug) {\n debugLog('Session reset');\n }\n }\n\n /**\n * Get the visitor fingerprint\n */\n async getVisitorFingerprint(): Promise<string> {\n if (this.visitorFingerprint) {\n return this.visitorFingerprint;\n }\n\n if (this.fingerprintPromise) {\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n // Generate fingerprint if not already in progress\n this.fingerprintPromise = generateFingerprint();\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n /**\n * Get the current session ID\n */\n getSessionId(): string {\n return this.session.sessionId;\n }\n\n /**\n * Check if user is identified\n */\n isIdentified(): boolean {\n return this.session.isIdentified();\n }\n\n /**\n * Register an extension\n */\n use(extension: PulsoraExtension): void {\n if (this.extensions.has(extension.name)) {\n if (this.config?.debug) {\n debugLog(`Extension ${extension.name} already registered`);\n }\n return;\n }\n\n this.extensions.set(extension.name, extension);\n extension.init(this);\n\n if (this.config?.debug) {\n debugLog(`Extension ${extension.name} registered`);\n }\n }\n\n /**\n * Check if tracker is ready\n */\n private isReady(): boolean {\n if (!this.initialized) {\n console.warn('[Pulsora] Tracker not initialized. Call init() first.');\n return false;\n }\n return true;\n }\n\n /**\n * Setup history tracking for SPAs\n */\n private setupHistoryTracking(): void {\n // Store original pushState and replaceState\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n // Override pushState\n history.pushState = (...args) => {\n originalPushState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Override replaceState\n history.replaceState = (...args) => {\n originalReplaceState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Listen for popstate events (back/forward buttons)\n window.addEventListener('popstate', () => {\n setTimeout(() => this.pageview(), 0);\n });\n }\n\n /**\n * Setup unload handlers to flush events\n */\n private setupUnloadHandlers(): void {\n // Try to flush events on page unload\n const flushEvents = () => {\n if (this.transport) {\n this.transport.flush();\n }\n };\n\n // Use visibilitychange as it's more reliable than unload\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') {\n flushEvents();\n }\n });\n\n // Also use pagehide for better mobile support\n window.addEventListener('pagehide', flushEvents);\n\n // Fallback to beforeunload\n window.addEventListener('beforeunload', flushEvents);\n }\n}\n","import { Tracker } from './tracker';\n\n// Create singleton instance\nconst tracker = new Tracker();\n\n// Export the instance as default\nexport default tracker;\n\n// Export types\nexport * from './types';\n\n// Export the tracker class for advanced usage\nexport { Tracker };\n\n// For UMD builds, attach to window\nif (typeof window !== 'undefined') {\n (window as any).Pulsora = tracker;\n\n // Auto-initialize if data-token is present\n if (document.currentScript) {\n const script = document.currentScript as HTMLScriptElement;\n const token = script.getAttribute('data-token');\n const endpoint = script.getAttribute('data-endpoint');\n const debug = script.getAttribute('data-debug') === 'true';\n\n if (token) {\n tracker.init({\n apiToken: token,\n endpoint: endpoint || undefined,\n debug,\n });\n }\n }\n}\n"],"names":["generateUUID","crypto","randomUUID","replace","c","r","Math","random","toString","async","sha256","message","subtle","digest","msgBuffer","TextEncoder","encode","hashBuffer","Array","from","Uint8Array","map","b","padStart","join","e","hash","i","length","charCodeAt","abs","parseUrl","url","urlObj","URL","params","URLSearchParams","search","path","pathname","utm_source","get","undefined","utm_medium","utm_campaign","utm_term","utm_content","_a","debugLog","data","console","log","generateFingerprint","components","userAgent","navigator","language","languages","platform","screenResolution","screen","width","height","screenColorDepth","colorDepth","timezoneOffset","Date","getTimezoneOffset","hardwareConcurrency","deviceMemory","maxTouchPoints","canvas","getCanvasFingerprint","webgl","getWebGLFingerprint","audio","getAudioFingerprint","fonts","getFontFingerprint","collectComponents","Object","values","document","createElement","ctx","getContext","fillStyle","fillRect","font","fillText","globalCompositeOperation","beginPath","arc","PI","closePath","fill","dataUrl","toDataURL","gl","debugInfo","getExtension","vendor","getParameter","UNMASKED_VENDOR_WEBGL","UNMASKED_RENDERER_WEBGL","AudioContext","window","webkitAudioContext","context","oscillator","createOscillator","analyser","createAnalyser","gainNode","createGain","scriptProcessor","createScriptProcessor","type","frequency","value","gain","connect","destination","start","startRendering","call","fingerprint","sampleRate","maxChannelCount","frequencyBinCount","stop","close","_b","testString","testSize","baseFonts","baseFontWidths","baseFont","measureText","testFonts","detectedFonts","detected","push","SessionManager","constructor","this","_customerId","_sessionId","_startTime","now","sessionId","duration","floor","setCustomerId","customerId","isIdentified","reset","clearIdentification","Transport","config","queue","Map","retryTimeouts","send","event","id","timestamp","attempts","sendEvent","payload","prepareEventData","token","apiToken","sendBeacon","blob","Blob","JSON","stringify","endpoint","debug","removeFromQueue","response","fetch","method","headers","body","keepalive","ok","status","retryAfter","parseInt","scheduleRetry","Error","statusText","error","maxRetries","backoff","calculateBackoff","eventData","cleanData","key","entries","delayMs","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","baseDelay","retryBackoff","delay","min","pow","jitter","eventId","flush","events","clear","queueSize","size","Tracker","transport","visitorFingerprint","extensions","initialized","fingerprintPromise","session","init","autoPageviews","then","fp","readyState","addEventListener","pageview","setupHistoryTracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","urlData","referrerSource","hostname","toLowerCase","sources","domain","source","includes","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","eventName","event_name","event_data","identify","customer_id","getSessionId","use","extension","has","name","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","flushEvents","visibilityState","tracker","Pulsora","currentScript","script","getAttribute"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@pulsora/core",
3
+ "version": "0.1.0",
4
+ "description": "Privacy-first analytics tracking library - Core package",
5
+ "author": "Pulsora.co",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "unpkg": "./dist/browser.js",
12
+ "cratebox": "./dist/browser.js",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "require": "./dist/index.cjs",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./browser": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/browser.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "sideEffects": false,
29
+ "scripts": {
30
+ "dev": "rollup -c -w",
31
+ "build": "rollup -c",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "test:coverage": "vitest run --coverage",
35
+ "typecheck": "tsc --noEmit",
36
+ "lint": "eslint src --ext .ts",
37
+ "size": "size-limit",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "devDependencies": {
41
+ "@rollup/plugin-commonjs": "^25.0.7",
42
+ "@rollup/plugin-node-resolve": "^15.2.3",
43
+ "@rollup/plugin-terser": "^0.4.4",
44
+ "@rollup/plugin-typescript": "^11.1.5",
45
+ "@size-limit/preset-small-lib": "^11.0.1",
46
+ "@types/node": "^20.10.5",
47
+ "@vitest/coverage-v8": "^1.1.0",
48
+ "rollup": "^4.9.1",
49
+ "rollup-plugin-dts": "^6.1.0",
50
+ "rollup-plugin-visualizer": "^6.0.5",
51
+ "size-limit": "^11.0.1",
52
+ "tslib": "^2.6.2",
53
+ "typescript": "^5.3.3",
54
+ "vitest": "^1.1.0"
55
+ },
56
+ "size-limit": [
57
+ {
58
+ "path": "dist/index.js",
59
+ "limit": "1 KB"
60
+ }
61
+ ],
62
+ "keywords": [
63
+ "analytics",
64
+ "tracking",
65
+ "revenue",
66
+ "attribution",
67
+ "funnels",
68
+ "journeys",
69
+ "segments",
70
+ "lifecycle",
71
+ "customer",
72
+ "lifetime",
73
+ "value",
74
+ "segmentation"
75
+ ],
76
+ "repository": {
77
+ "type": "git",
78
+ "url": "https://github.com/pulsora/javascript-sdk"
79
+ },
80
+ "bugs": {
81
+ "url": "https://github.com/pulsora/javascript-sdk/issues"
82
+ },
83
+ "homepage": "https://pulsora.co"
84
+ }