@pulsora/core 0.1.0 → 1.0.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 +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +12 -14
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +15 -18
- package/dist/index.umd.js +0 -2
- package/dist/index.umd.js.map +0 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ const tracker = new Pulsora();
|
|
|
46
46
|
// Initialize
|
|
47
47
|
tracker.init({
|
|
48
48
|
apiToken: 'your-api-token',
|
|
49
|
-
endpoint: 'https://
|
|
49
|
+
endpoint: 'https://pulsora.co/api/ingest', // optional, this is the default
|
|
50
50
|
autoPageviews: true, // optional, default: true
|
|
51
51
|
debug: false, // optional, default: false
|
|
52
52
|
});
|
package/dist/browser.js
CHANGED
|
@@ -1,2 +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}
|
|
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}class s{constructor(){this.sessionId=t(),this.startTime=Date.now()}getSessionId(){return this.sessionId}getDuration(){return Math.floor((Date.now()-this.startTime)/1e3)}reset(){this.sessionId=t(),this.startTime=Date.now()}}class n{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){const s=new Blob([JSON.stringify(e)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,s))return this.config.debug&&i(0,t.data),void this.removeFromQueue(t.id)}const s=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(s.ok)return this.config.debug&&i(0,t.data),void this.removeFromQueue(t.id);if(429===s.status){const e=parseInt(s.headers.get("Retry-After")||"60",10);return this.config.debug&&i(),void this.scheduleRetry(t,1e3*e)}throw new Error(`HTTP ${s.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,...s}=t,n={};for(const[t,e]of Object.entries(s))void 0!==e&&(n[t]=e);return n}scheduleRetry(t,e){this.queue.set(t.id,t);const i=this.retryTimeouts.get(t.id);i&&clearTimeout(i);const s=setTimeout(()=>{this.retryTimeouts.delete(t.id);const e=this.queue.get(t.id);e&&this.sendEvent(e)},e);this.retryTimeouts.set(t.id,s)}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 o=new class{constructor(){this.visitorFingerprint=null,this.fingerprintPromise=null,this.extensions=new Map,this.initialized=!1,this.sessionManager=new s}init(t){if(this.initialized)t.debug&&i();else{if("undefined"==typeof window)throw new Error("Browser environment required");this.config={endpoint:"https://pulsora.co/api/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...t},this.transport=new n({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),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,s=(null==t?void 0:t.referrer)||document.referrer,n=(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}}(s),h={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),url:i,path:o.path,referrer:s||void 0,referrer_source:r,title:n,locale:navigator.language||void 0,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(h)}async event(t,i){if(!this.isReady()||!t)return;const s=location.href,n=e(s),o={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),event_name:t,event_data:i,url:s,path:n.path,timestamp:Date.now()};this.transport.send(o)}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?await this.fingerprintPromise:null}getSessionId(){return this.sessionManager.getSessionId()}use(t){var e,s;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===(s=this.config)||void 0===s?void 0:s.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=o,document.currentScript)){const t=document.currentScript,e=t.getAttribute("data-token"),i=t.getAttribute("data-endpoint"),s="true"===t.getAttribute("data-debug");e&&o.init({apiToken:e,endpoint:i||void 0,debug:s})}return o});
|
|
2
2
|
//# sourceMappingURL=browser.js.map
|
package/dist/browser.js.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"browser.js","sources":["../src/utils.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: improved hash for better uniqueness\n let hash1 = 0;\n let hash2 = 0;\n\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash1 = ((hash1 << 5) - hash1 + char) | 0;\n hash2 = ((hash2 << 3) + hash2 + char + i) | 0;\n }\n\n // Combine both hashes for 16 characters of entropy\n const combined =\n Math.abs(hash1).toString(16).padStart(8, '0') +\n Math.abs(hash2).toString(16).padStart(8, '0');\n\n return combined;\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 { generateUUID } from './utils';\n\n/**\n * Session manager for tracking user sessions\n * Handles session ID generation for 100% anonymous analytics\n */\nexport class SessionManager {\n private sessionId: string;\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 reset(): void {\n this.sessionId = generateUUID();\n this.startTime = Date.now();\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\n if (navigator.sendBeacon) {\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: _type, timestamp: _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 { 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 and custom events with server-side fingerprinting\n */\nexport class Tracker implements PulsoraCore {\n private config?: PulsoraConfig;\n private transport?: Transport;\n private sessionManager: SessionManager;\n private visitorFingerprint: string | null = null;\n private fingerprintPromise: Promise<string | null> | null = null;\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://pulsora.co/api/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 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 locale: navigator.language || undefined, // Add locale from browser\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 /**\n * Get visitor fingerprint (generated server-side)\n * Returns null if not available yet\n */\n async getVisitorFingerprint(): Promise<string | null> {\n if (this.visitorFingerprint) {\n return this.visitorFingerprint;\n }\n\n if (this.fingerprintPromise) {\n return await this.fingerprintPromise;\n }\n\n return null;\n }\n\n getSessionId(): string {\n return this.sessionManager.getSessionId();\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","SessionManager","constructor","this","sessionId","startTime","getSessionId","getDuration","reset","Transport","config","queue","Map","retryTimeouts","send","event","id","timestamp","attempts","sendEvent","payload","type","prepareEventData","token","apiToken","navigator","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","_type","_timestamp","rest","cleanData","key","value","Object","entries","undefined","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","eventId","flush","events","Array","from","values","clear","queueSize","size","pulsora","visitorFingerprint","fingerprintPromise","extensions","initialized","sessionManager","init","window","autoPageviews","transport","document","readyState","addEventListener","pageview","setupSPATracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","parsed","referrerSource","referrerHost","hostname","toLowerCase","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","locale","language","utm_source","utm_medium","utm_campaign","utm_term","utm_content","eventName","eventData","event_name","event_data","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
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";function
|
|
1
|
+
"use strict";function e(){if("undefined"!=typeof crypto&&crypto.randomUUID)return crypto.randomUUID();let e=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const i=(e+16*Math.random())%16|0;return e=Math.floor(e/16),("x"===t?i:3&i|8).toString(16)})}function t(e){try{const t=new URL(e),i={};return t.searchParams.forEach((e,t)=>{i[t]=e}),{path:t.pathname,search:i}}catch(e){return{path:"/",search:{}}}}function i(e,t){"undefined"!=typeof console&&console.log&&(void 0!==t?console.log(`[Pulsora] ${e}`,t):console.log(`[Pulsora] ${e}`))}class s{constructor(){this.sessionId=e(),this.startTime=Date.now()}getSessionId(){return this.sessionId}getDuration(){return Math.floor((Date.now()-this.startTime)/1e3)}reset(){this.sessionId=e(),this.startTime=Date.now()}}class n{constructor(e){this.queue=new Map,this.retryTimeouts=new Map,this.config=e}async send(t){const i={id:e(),timestamp:Date.now(),attempts:0,data:t};await this.sendEvent(i)}async sendEvent(e){e.attempts++;try{const t={type:e.data.type,data:this.prepareEventData(e.data),token:this.config.apiToken};if(navigator.sendBeacon){const s=new Blob([JSON.stringify(t)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,s))return this.config.debug&&i("Event sent",e.data),void this.removeFromQueue(e.id)}const s=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(t),keepalive:!0});if(s.ok)return this.config.debug&&i("Event sent",e.data),void this.removeFromQueue(e.id);if(429===s.status){const t=parseInt(s.headers.get("Retry-After")||"60",10);return this.config.debug&&i(`Rate limited, retry after ${t}s`),void this.scheduleRetry(e,1e3*t)}throw new Error(`HTTP ${s.status}`)}catch(t){if(this.config.debug&&i("Send failed",{error:t,event:e}),e.attempts<this.config.maxRetries){const t=Math.min(this.config.retryBackoff*Math.pow(2,e.attempts-1),3e4);this.scheduleRetry(e,t)}else this.config.debug&&i(`Dropped after ${e.attempts} attempts`),this.removeFromQueue(e.id)}}prepareEventData(e){const{type:t,timestamp:i,...s}=e,n={};for(const[e,t]of Object.entries(s))void 0!==t&&(n[e]=t);return n}scheduleRetry(e,t){this.queue.set(e.id,e);const i=this.retryTimeouts.get(e.id);i&&clearTimeout(i);const s=setTimeout(()=>{this.retryTimeouts.delete(e.id);const t=this.queue.get(e.id);t&&this.sendEvent(t)},t);this.retryTimeouts.set(e.id,s)}removeFromQueue(e){this.queue.delete(e);const t=this.retryTimeouts.get(e);t&&(clearTimeout(t),this.retryTimeouts.delete(e))}flush(){const e=Array.from(this.queue.values());this.queue.clear();for(const e of this.retryTimeouts.values())clearTimeout(e);this.retryTimeouts.clear();for(const t of e)this.sendEvent(t)}get queueSize(){return this.queue.size}}exports.Pulsora=class{constructor(){this.visitorFingerprint=null,this.fingerprintPromise=null,this.extensions=new Map,this.initialized=!1,this.sessionManager=new s}init(e){if(this.initialized)e.debug&&i("Already initialized");else{if("undefined"==typeof window)throw new Error("Browser environment required");this.config={endpoint:"https://pulsora.co/api/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...e},this.transport=new n({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),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(e){if(!this.isReady())return;const i=(null==e?void 0:e.url)||location.href,s=(null==e?void 0:e.referrer)||document.referrer,n=(null==e?void 0:e.title)||document.title,r=t(i),o=function(e){if(e)try{const t=new URL(e).hostname.toLowerCase();if(t===location.hostname.toLowerCase())return;return t.replace(/^www\./,"")}catch(e){return}}(s),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),url:i,path:r.path,referrer:s||void 0,referrer_source:o,title:n,locale:navigator.language||void 0,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(e,i){if(!this.isReady()||!e)return;const s=location.href,n=t(s),r={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),event_name:e,event_data:i,url:s,path:n.path,timestamp:Date.now()};this.transport.send(r)}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?await this.fingerprintPromise:null}getSessionId(){return this.sessionManager.getSessionId()}use(e){var t,s;this.extensions.has(e.name)?(null===(t=this.config)||void 0===t?void 0:t.debug)&&i(`Extension ${e.name} already loaded`):(this.extensions.set(e.name,e),e.init(this),(null===(s=this.config)||void 0===s?void 0:s.debug)&&i(`Extension ${e.name} loaded`))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Not initialized"),!1)}setupSPATracking(){const e=history.pushState,t=history.replaceState;history.pushState=(...t)=>{e.apply(history,t),setTimeout(()=>this.pageview(),0)},history.replaceState=(...e)=>{t.apply(history,e),setTimeout(()=>this.pageview(),0)},addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const e=()=>{var e;return null===(e=this.transport)||void 0===e?void 0:e.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&e()}),addEventListener("pagehide",e),addEventListener("beforeunload",e)}};
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/utils.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: improved hash for better uniqueness\n let hash1 = 0;\n let hash2 = 0;\n\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash1 = ((hash1 << 5) - hash1 + char) | 0;\n hash2 = ((hash2 << 3) + hash2 + char + i) | 0;\n }\n\n // Combine both hashes for 16 characters of entropy\n const combined =\n Math.abs(hash1).toString(16).padStart(8, '0') +\n Math.abs(hash2).toString(16).padStart(8, '0');\n\n return combined;\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 { generateUUID } from './utils';\n\n/**\n * Session manager for tracking user sessions\n * Handles session ID generation for 100% anonymous analytics\n */\nexport class SessionManager {\n private sessionId: string;\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 reset(): void {\n this.sessionId = generateUUID();\n this.startTime = Date.now();\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\n if (navigator.sendBeacon) {\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: _type, timestamp: _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 { 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 and custom events with server-side fingerprinting\n */\nexport class Tracker implements PulsoraCore {\n private config?: PulsoraConfig;\n private transport?: Transport;\n private sessionManager: SessionManager;\n private visitorFingerprint: string | null = null;\n private fingerprintPromise: Promise<string | null> | null = null;\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://pulsora.co/api/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 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 locale: navigator.language || undefined, // Add locale from browser\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 /**\n * Get visitor fingerprint (generated server-side)\n * Returns null if not available yet\n */\n async getVisitorFingerprint(): Promise<string | null> {\n if (this.visitorFingerprint) {\n return this.visitorFingerprint;\n }\n\n if (this.fingerprintPromise) {\n return await this.fingerprintPromise;\n }\n\n return null;\n }\n\n getSessionId(): string {\n return this.sessionManager.getSessionId();\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","SessionManager","constructor","this","sessionId","startTime","getSessionId","getDuration","reset","Transport","config","queue","Map","retryTimeouts","send","event","id","timestamp","attempts","sendEvent","payload","type","prepareEventData","token","apiToken","navigator","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","_type","_timestamp","rest","cleanData","key","value","Object","entries","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","eventId","flush","events","Array","from","values","clear","queueSize","size","visitorFingerprint","fingerprintPromise","extensions","initialized","sessionManager","init","window","autoPageviews","transport","document","readyState","addEventListener","pageview","setupSPATracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","parsed","referrerSource","referrerHost","hostname","toLowerCase","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","locale","language","utm_source","utm_medium","utm_campaign","utm_term","utm_content","eventName","eventData","event_name","event_data","use","extension","has","name","_b","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","visibilityState"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,11 +22,8 @@ interface PulsoraCore {
|
|
|
22
22
|
init(config: PulsoraConfig): void;
|
|
23
23
|
pageview(options?: PageviewOptions): void;
|
|
24
24
|
event(eventName: string, eventData?: EventData): void;
|
|
25
|
-
|
|
26
|
-
reset(): void;
|
|
27
|
-
getVisitorFingerprint(): Promise<string>;
|
|
25
|
+
getVisitorFingerprint(): Promise<string | null>;
|
|
28
26
|
getSessionId(): string;
|
|
29
|
-
isIdentified(): boolean;
|
|
30
27
|
use(extension: PulsoraExtension): void;
|
|
31
28
|
}
|
|
32
29
|
interface QueuedEvent {
|
|
@@ -36,17 +33,17 @@ interface QueuedEvent {
|
|
|
36
33
|
data: TrackingData;
|
|
37
34
|
}
|
|
38
35
|
interface TrackingData {
|
|
39
|
-
type: 'pageview' | 'event'
|
|
40
|
-
visitor_fingerprint: string;
|
|
36
|
+
type: 'pageview' | 'event';
|
|
37
|
+
visitor_fingerprint: string | null;
|
|
41
38
|
session_id: string;
|
|
42
39
|
url?: string;
|
|
43
40
|
path?: string;
|
|
44
41
|
referrer?: string;
|
|
45
42
|
referrer_source?: string;
|
|
46
43
|
title?: string;
|
|
44
|
+
locale?: string;
|
|
47
45
|
event_name?: string;
|
|
48
46
|
event_data?: EventData;
|
|
49
|
-
customer_id?: string;
|
|
50
47
|
utm_source?: string;
|
|
51
48
|
utm_medium?: string;
|
|
52
49
|
utm_campaign?: string;
|
|
@@ -57,25 +54,26 @@ interface TrackingData {
|
|
|
57
54
|
|
|
58
55
|
/**
|
|
59
56
|
* Main Pulsora tracker implementation
|
|
60
|
-
* Handles pageview tracking
|
|
57
|
+
* Handles pageview tracking and custom events with server-side fingerprinting
|
|
61
58
|
*/
|
|
62
59
|
declare class Tracker implements PulsoraCore {
|
|
63
60
|
private config?;
|
|
64
61
|
private transport?;
|
|
65
62
|
private sessionManager;
|
|
66
|
-
private visitorFingerprint
|
|
67
|
-
private fingerprintPromise
|
|
63
|
+
private visitorFingerprint;
|
|
64
|
+
private fingerprintPromise;
|
|
68
65
|
private extensions;
|
|
69
66
|
private initialized;
|
|
70
67
|
constructor();
|
|
71
68
|
init(config: PulsoraConfig): void;
|
|
72
69
|
pageview(options?: PageviewOptions): Promise<void>;
|
|
73
70
|
event(eventName: string, eventData?: EventData): Promise<void>;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Get visitor fingerprint (generated server-side)
|
|
73
|
+
* Returns null if not available yet
|
|
74
|
+
*/
|
|
75
|
+
getVisitorFingerprint(): Promise<string | null>;
|
|
77
76
|
getSessionId(): string;
|
|
78
|
-
isIdentified(): boolean;
|
|
79
77
|
use(extension: PulsoraExtension): void;
|
|
80
78
|
private isReady;
|
|
81
79
|
private setupSPATracking;
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function
|
|
1
|
+
function e(){if("undefined"!=typeof crypto&&crypto.randomUUID)return crypto.randomUUID();let e=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const i=(e+16*Math.random())%16|0;return e=Math.floor(e/16),("x"===t?i:3&i|8).toString(16)})}function t(e){try{const t=new URL(e),i={};return t.searchParams.forEach((e,t)=>{i[t]=e}),{path:t.pathname,search:i}}catch(e){return{path:"/",search:{}}}}function i(e,t){"undefined"!=typeof console&&console.log&&(void 0!==t?console.log(`[Pulsora] ${e}`,t):console.log(`[Pulsora] ${e}`))}class s{constructor(){this.sessionId=e(),this.startTime=Date.now()}getSessionId(){return this.sessionId}getDuration(){return Math.floor((Date.now()-this.startTime)/1e3)}reset(){this.sessionId=e(),this.startTime=Date.now()}}class n{constructor(e){this.queue=new Map,this.retryTimeouts=new Map,this.config=e}async send(t){const i={id:e(),timestamp:Date.now(),attempts:0,data:t};await this.sendEvent(i)}async sendEvent(e){e.attempts++;try{const t={type:e.data.type,data:this.prepareEventData(e.data),token:this.config.apiToken};if(navigator.sendBeacon){const s=new Blob([JSON.stringify(t)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,s))return this.config.debug&&i("Event sent",e.data),void this.removeFromQueue(e.id)}const s=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(t),keepalive:!0});if(s.ok)return this.config.debug&&i("Event sent",e.data),void this.removeFromQueue(e.id);if(429===s.status){const t=parseInt(s.headers.get("Retry-After")||"60",10);return this.config.debug&&i(`Rate limited, retry after ${t}s`),void this.scheduleRetry(e,1e3*t)}throw new Error(`HTTP ${s.status}`)}catch(t){if(this.config.debug&&i("Send failed",{error:t,event:e}),e.attempts<this.config.maxRetries){const t=Math.min(this.config.retryBackoff*Math.pow(2,e.attempts-1),3e4);this.scheduleRetry(e,t)}else this.config.debug&&i(`Dropped after ${e.attempts} attempts`),this.removeFromQueue(e.id)}}prepareEventData(e){const{type:t,timestamp:i,...s}=e,n={};for(const[e,t]of Object.entries(s))void 0!==t&&(n[e]=t);return n}scheduleRetry(e,t){this.queue.set(e.id,e);const i=this.retryTimeouts.get(e.id);i&&clearTimeout(i);const s=setTimeout(()=>{this.retryTimeouts.delete(e.id);const t=this.queue.get(e.id);t&&this.sendEvent(t)},t);this.retryTimeouts.set(e.id,s)}removeFromQueue(e){this.queue.delete(e);const t=this.retryTimeouts.get(e);t&&(clearTimeout(t),this.retryTimeouts.delete(e))}flush(){const e=Array.from(this.queue.values());this.queue.clear();for(const e of this.retryTimeouts.values())clearTimeout(e);this.retryTimeouts.clear();for(const t of e)this.sendEvent(t)}get queueSize(){return this.queue.size}}class r{constructor(){this.visitorFingerprint=null,this.fingerprintPromise=null,this.extensions=new Map,this.initialized=!1,this.sessionManager=new s}init(e){if(this.initialized)e.debug&&i("Already initialized");else{if("undefined"==typeof window)throw new Error("Browser environment required");this.config={endpoint:"https://pulsora.co/api/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...e},this.transport=new n({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),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(e){if(!this.isReady())return;const i=(null==e?void 0:e.url)||location.href,s=(null==e?void 0:e.referrer)||document.referrer,n=(null==e?void 0:e.title)||document.title,r=t(i),o=function(e){if(e)try{const t=new URL(e).hostname.toLowerCase();if(t===location.hostname.toLowerCase())return;return t.replace(/^www\./,"")}catch(e){return}}(s),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),url:i,path:r.path,referrer:s||void 0,referrer_source:o,title:n,locale:navigator.language||void 0,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(e,i){if(!this.isReady()||!e)return;const s=location.href,n=t(s),r={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),event_name:e,event_data:i,url:s,path:n.path,timestamp:Date.now()};this.transport.send(r)}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?await this.fingerprintPromise:null}getSessionId(){return this.sessionManager.getSessionId()}use(e){var t,s;this.extensions.has(e.name)?(null===(t=this.config)||void 0===t?void 0:t.debug)&&i(`Extension ${e.name} already loaded`):(this.extensions.set(e.name,e),e.init(this),(null===(s=this.config)||void 0===s?void 0:s.debug)&&i(`Extension ${e.name} loaded`))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Not initialized"),!1)}setupSPATracking(){const e=history.pushState,t=history.replaceState;history.pushState=(...t)=>{e.apply(history,t),setTimeout(()=>this.pageview(),0)},history.replaceState=(...e)=>{t.apply(history,e),setTimeout(()=>this.pageview(),0)},addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const e=()=>{var e;return null===(e=this.transport)||void 0===e?void 0:e.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&e()}),addEventListener("pagehide",e),addEventListener("beforeunload",e)}}export{r as Pulsora};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/utils.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: improved hash for better uniqueness\n let hash1 = 0;\n let hash2 = 0;\n\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash1 = ((hash1 << 5) - hash1 + char) | 0;\n hash2 = ((hash2 << 3) + hash2 + char + i) | 0;\n }\n\n // Combine both hashes for 16 characters of entropy\n const combined =\n Math.abs(hash1).toString(16).padStart(8, '0') +\n Math.abs(hash2).toString(16).padStart(8, '0');\n\n return combined;\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 { generateUUID } from './utils';\n\n/**\n * Session manager for tracking user sessions\n * Handles session ID generation for 100% anonymous analytics\n */\nexport class SessionManager {\n private sessionId: string;\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 reset(): void {\n this.sessionId = generateUUID();\n this.startTime = Date.now();\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\n if (navigator.sendBeacon) {\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: _type, timestamp: _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 { 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 and custom events with server-side fingerprinting\n */\nexport class Tracker implements PulsoraCore {\n private config?: PulsoraConfig;\n private transport?: Transport;\n private sessionManager: SessionManager;\n private visitorFingerprint: string | null = null;\n private fingerprintPromise: Promise<string | null> | null = null;\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://pulsora.co/api/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 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 locale: navigator.language || undefined, // Add locale from browser\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 /**\n * Get visitor fingerprint (generated server-side)\n * Returns null if not available yet\n */\n async getVisitorFingerprint(): Promise<string | null> {\n if (this.visitorFingerprint) {\n return this.visitorFingerprint;\n }\n\n if (this.fingerprintPromise) {\n return await this.fingerprintPromise;\n }\n\n return null;\n }\n\n getSessionId(): string {\n return this.sessionManager.getSessionId();\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","SessionManager","constructor","this","sessionId","startTime","getSessionId","getDuration","reset","Transport","config","queue","Map","retryTimeouts","send","event","id","timestamp","attempts","sendEvent","payload","type","prepareEventData","token","apiToken","navigator","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","_type","_timestamp","rest","cleanData","key","value","Object","entries","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","eventId","flush","events","Array","from","values","clear","queueSize","size","Tracker","visitorFingerprint","fingerprintPromise","extensions","initialized","sessionManager","init","window","autoPageviews","transport","document","readyState","addEventListener","pageview","setupSPATracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","parsed","referrerSource","referrerHost","hostname","toLowerCase","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","locale","language","utm_source","utm_medium","utm_campaign","utm_term","utm_content","eventName","eventData","event_name","event_data","use","extension","has","name","_b","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","visibilityState"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulsora/core",
|
|
3
|
-
"
|
|
3
|
+
"public": true,
|
|
4
|
+
"version": "1.0.0",
|
|
4
5
|
"description": "Privacy-first analytics tracking library - Core package",
|
|
5
6
|
"author": "Pulsora.co",
|
|
6
7
|
"license": "MIT",
|
|
@@ -38,20 +39,20 @@
|
|
|
38
39
|
"prepublishOnly": "npm run build"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@rollup/plugin-commonjs": "^
|
|
42
|
-
"@rollup/plugin-node-resolve": "^
|
|
42
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
43
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
43
44
|
"@rollup/plugin-terser": "^0.4.4",
|
|
44
|
-
"@rollup/plugin-typescript": "^
|
|
45
|
-
"@size-limit/preset-small-lib": "^
|
|
46
|
-
"@types/node": "^
|
|
47
|
-
"@vitest/coverage-v8": "^
|
|
48
|
-
"rollup": "^4.
|
|
49
|
-
"rollup-plugin-dts": "^6.
|
|
45
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
46
|
+
"@size-limit/preset-small-lib": "^12.0.0",
|
|
47
|
+
"@types/node": "^25.0.8",
|
|
48
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
49
|
+
"rollup": "^4.55.1",
|
|
50
|
+
"rollup-plugin-dts": "^6.3.0",
|
|
50
51
|
"rollup-plugin-visualizer": "^6.0.5",
|
|
51
|
-
"size-limit": "^
|
|
52
|
-
"tslib": "^2.
|
|
53
|
-
"typescript": "^5.
|
|
54
|
-
"vitest": "^
|
|
52
|
+
"size-limit": "^12.0.0",
|
|
53
|
+
"tslib": "^2.8.1",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vitest": "^4.0.17"
|
|
55
56
|
},
|
|
56
57
|
"size-limit": [
|
|
57
58
|
{
|
|
@@ -73,12 +74,8 @@
|
|
|
73
74
|
"value",
|
|
74
75
|
"segmentation"
|
|
75
76
|
],
|
|
76
|
-
"repository": {
|
|
77
|
-
"type": "git",
|
|
78
|
-
"url": "https://github.com/pulsora/javascript-sdk"
|
|
79
|
-
},
|
|
80
77
|
"bugs": {
|
|
81
|
-
"url": "https://github.com/
|
|
78
|
+
"url": "https://github.com/alexvcasillas/analytics/issues"
|
|
82
79
|
},
|
|
83
80
|
"homepage": "https://pulsora.co"
|
|
84
81
|
}
|
package/dist/index.umd.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
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
|
package/dist/index.umd.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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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"}
|