@pulsora/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +280 -0
- package/dist/browser.js +2 -0
- package/dist/browser.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +2 -0
- package/dist/index.umd.js.map +1 -0
- package/package.json +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# @pulsora/core
|
|
2
|
+
|
|
3
|
+
Privacy-first, cookieless analytics tracking library. Core package for Pulsora Analytics.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔒 **Privacy-First**: No cookies, no localStorage (by default)
|
|
8
|
+
- 🎯 **Lightweight**: <1KB gzipped
|
|
9
|
+
- 🚀 **High Performance**: Uses sendBeacon API, async processing
|
|
10
|
+
- 🔧 **Extensible**: Plugin system for additional features
|
|
11
|
+
- 📊 **Browser Fingerprinting**: Stable, anonymous visitor identification
|
|
12
|
+
- 🔄 **Retry Logic**: Automatic retry with exponential backoff
|
|
13
|
+
- 📱 **SPA Support**: Automatic pageview tracking on route changes
|
|
14
|
+
- 💪 **TypeScript**: Full type definitions included
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
### NPM
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @pulsora/core
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### CDN
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<script
|
|
28
|
+
async
|
|
29
|
+
src="https://cdn.pulsora.co/v1/pulsora.min.js"
|
|
30
|
+
data-token="YOUR_API_TOKEN"
|
|
31
|
+
></script>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### For NPM Users (Recommended for Apps)
|
|
37
|
+
|
|
38
|
+
Create and manage your own tracker instances:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { Pulsora } from '@pulsora/core';
|
|
42
|
+
|
|
43
|
+
// Create a tracker instance
|
|
44
|
+
const tracker = new Pulsora();
|
|
45
|
+
|
|
46
|
+
// Initialize
|
|
47
|
+
tracker.init({
|
|
48
|
+
apiToken: 'your-api-token',
|
|
49
|
+
endpoint: 'https://api.pulsora.co/ingest', // optional
|
|
50
|
+
autoPageviews: true, // optional, default: true
|
|
51
|
+
debug: false, // optional, default: false
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Track pageview
|
|
55
|
+
tracker.pageview();
|
|
56
|
+
|
|
57
|
+
// Track custom event
|
|
58
|
+
tracker.event('button_click', {
|
|
59
|
+
button: 'signup',
|
|
60
|
+
location: 'header',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Identify user
|
|
64
|
+
tracker.identify('user_123');
|
|
65
|
+
|
|
66
|
+
// Get visitor fingerprint (useful for server-side attribution)
|
|
67
|
+
const fingerprint = await tracker.getVisitorFingerprint();
|
|
68
|
+
const sessionId = tracker.getSessionId();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Multiple Tracker Instances
|
|
72
|
+
|
|
73
|
+
Perfect for tracking multiple websites or separating different types of analytics:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { Pulsora } from '@pulsora/core';
|
|
77
|
+
|
|
78
|
+
const mainSiteTracker = new Pulsora();
|
|
79
|
+
mainSiteTracker.init({ apiToken: 'token-1' });
|
|
80
|
+
|
|
81
|
+
const blogTracker = new Pulsora();
|
|
82
|
+
blogTracker.init({ apiToken: 'token-2' });
|
|
83
|
+
|
|
84
|
+
// Each tracker operates independently
|
|
85
|
+
mainSiteTracker.pageview();
|
|
86
|
+
blogTracker.pageview();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### For CDN Users (Recommended for Websites)
|
|
90
|
+
|
|
91
|
+
Add the script tag with your API token:
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<!-- Auto-initializes and tracks pageviews -->
|
|
95
|
+
<script
|
|
96
|
+
async
|
|
97
|
+
src="https://cdn.pulsora.co/v1/pulsora.min.js"
|
|
98
|
+
data-token="YOUR_API_TOKEN"
|
|
99
|
+
data-debug="false"
|
|
100
|
+
></script>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
// Track custom event
|
|
104
|
+
window.pulsora.event('purchase', {
|
|
105
|
+
amount: 99.99,
|
|
106
|
+
currency: 'USD',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Identify user
|
|
110
|
+
window.pulsora.identify('user_123');
|
|
111
|
+
</script>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API Reference
|
|
115
|
+
|
|
116
|
+
### `init(config: PulsoraConfig)`
|
|
117
|
+
|
|
118
|
+
Initialize the tracker.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
interface PulsoraConfig {
|
|
122
|
+
apiToken: string; // Required: Your Pulsora API token
|
|
123
|
+
endpoint?: string; // Optional: API endpoint (default: https://api.pulsora.co/ingest)
|
|
124
|
+
autoPageviews?: boolean; // Optional: Auto-track pageviews (default: true)
|
|
125
|
+
debug?: boolean; // Optional: Enable debug logging (default: false)
|
|
126
|
+
maxRetries?: number; // Optional: Max retry attempts (default: 10)
|
|
127
|
+
retryBackoff?: number; // Optional: Initial retry backoff in ms (default: 1000)
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `pageview(options?: PageviewOptions)`
|
|
132
|
+
|
|
133
|
+
Track a pageview.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
tracker.pageview(); // Current page
|
|
137
|
+
|
|
138
|
+
tracker.pageview({
|
|
139
|
+
url: 'https://example.com/page',
|
|
140
|
+
referrer: 'https://google.com',
|
|
141
|
+
title: 'Page Title',
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `event(eventName: string, eventData?: object)`
|
|
146
|
+
|
|
147
|
+
Track a custom event.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
tracker.event('button_click');
|
|
151
|
+
|
|
152
|
+
tracker.event('purchase', {
|
|
153
|
+
product: 'Premium Plan',
|
|
154
|
+
amount: 99,
|
|
155
|
+
currency: 'USD',
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `identify(customerId: string)`
|
|
160
|
+
|
|
161
|
+
Identify a user. Links all previous anonymous activity to this user.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
tracker.identify('user_123');
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### `reset()`
|
|
168
|
+
|
|
169
|
+
Reset the session and clear user identification.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
tracker.reset();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `getVisitorFingerprint(): Promise<string>`
|
|
176
|
+
|
|
177
|
+
Get the current visitor's fingerprint. Useful for server-side revenue attribution.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const fingerprint = await tracker.getVisitorFingerprint();
|
|
181
|
+
// Send to your backend for revenue tracking
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `getSessionId(): string`
|
|
185
|
+
|
|
186
|
+
Get the current session ID.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const sessionId = tracker.getSessionId();
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `isIdentified(): boolean`
|
|
193
|
+
|
|
194
|
+
Check if the current visitor is identified.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
if (tracker.isIdentified()) {
|
|
198
|
+
console.log('User is logged in');
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `use(extension: PulsoraExtension)`
|
|
203
|
+
|
|
204
|
+
Register an extension/plugin.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { Pulsora } from '@pulsora/core';
|
|
208
|
+
import revenue from '@pulsora/revenue';
|
|
209
|
+
|
|
210
|
+
const tracker = new Pulsora();
|
|
211
|
+
tracker.use(revenue);
|
|
212
|
+
tracker.init({ apiToken: 'token' });
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## TypeScript
|
|
216
|
+
|
|
217
|
+
Full TypeScript support with type definitions included:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { Pulsora, PulsoraConfig, PageviewOptions } from '@pulsora/core';
|
|
221
|
+
|
|
222
|
+
const config: PulsoraConfig = {
|
|
223
|
+
apiToken: 'token',
|
|
224
|
+
debug: true,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const tracker = new Pulsora();
|
|
228
|
+
tracker.init(config);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Privacy & GDPR Compliance
|
|
232
|
+
|
|
233
|
+
- **No cookies**: Pulsora doesn't use cookies
|
|
234
|
+
- **No localStorage**: All data is in-memory by default
|
|
235
|
+
- **Browser fingerprinting**: Uses non-PII technical characteristics
|
|
236
|
+
- **Anonymous by default**: Identification is opt-in via `identify()`
|
|
237
|
+
- **Transparent**: Open source and auditable
|
|
238
|
+
|
|
239
|
+
## SPA Support
|
|
240
|
+
|
|
241
|
+
Automatic pageview tracking for Single Page Applications:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// Automatically tracks pageviews on:
|
|
245
|
+
// - history.pushState()
|
|
246
|
+
// - history.replaceState()
|
|
247
|
+
// - popstate events (back/forward buttons)
|
|
248
|
+
|
|
249
|
+
const tracker = new Pulsora();
|
|
250
|
+
tracker.init({
|
|
251
|
+
apiToken: 'token',
|
|
252
|
+
autoPageviews: true, // ← Enables SPA tracking
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Error Handling
|
|
257
|
+
|
|
258
|
+
Pulsora handles errors gracefully with automatic retry:
|
|
259
|
+
|
|
260
|
+
- Network failures: Exponential backoff retry
|
|
261
|
+
- Rate limiting: Respects Retry-After headers
|
|
262
|
+
- Max retries: Events dropped after max attempts
|
|
263
|
+
- Debug mode: Console warnings for troubleshooting
|
|
264
|
+
|
|
265
|
+
## Browser Support
|
|
266
|
+
|
|
267
|
+
- Chrome/Edge (latest 2 versions)
|
|
268
|
+
- Firefox (latest 2 versions)
|
|
269
|
+
- Safari (latest 2 versions)
|
|
270
|
+
- Mobile browsers (iOS Safari, Chrome Mobile)
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT
|
|
275
|
+
|
|
276
|
+
## Links
|
|
277
|
+
|
|
278
|
+
- [Documentation](https://pulsora.co/docs)
|
|
279
|
+
- [GitHub](https://github.com/pulsora/javascript-sdk)
|
|
280
|
+
- [Website](https://pulsora.co)
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).pulsora=e()}(this,function(){"use strict";function t(){if("undefined"!=typeof crypto&&crypto.randomUUID)return crypto.randomUUID();let t=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const i=(t+16*Math.random())%16|0;return t=Math.floor(t/16),("x"===e?i:3&i|8).toString(16)})}function e(t){try{const e=new URL(t),i={};return e.searchParams.forEach((t,e)=>{i[e]=t}),{path:e.pathname,search:i}}catch(t){return{path:"/",search:{}}}}function i(t,e){"undefined"!=typeof console&&console.log}async function n(){return async function(t){if("undefined"!=typeof crypto&&crypto.subtle&&crypto.subtle.digest)try{const e=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(t));return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}catch(t){}let e=0;for(let i=0;i<t.length;i++)e=(e<<5)-e+t.charCodeAt(i),e&=e;return Math.abs(e).toString(16).padStart(8,"0")}([navigator.userAgent,navigator.language,(navigator.languages||[]).join(","),screen.width,screen.height,screen.colorDepth,window.devicePixelRatio||1,navigator.hardwareConcurrency||0,navigator.deviceMemory||0,navigator.maxTouchPoints||0,navigator.platform,(new Date).getTimezoneOffset(),s(),await o()].join("~"))}function s(){try{const t=document.createElement("canvas"),e=t.getContext("webgl")||t.getContext("experimental-webgl");if(!e)return"0";const i=e.getExtension("WEBGL_debug_renderer_info");if(!i)return"1";const n=e.getParameter(i.UNMASKED_VENDOR_WEBGL);return n+"~"+e.getParameter(i.UNMASKED_RENDERER_WEBGL)}catch(t){return"2"}}async function o(){try{const t=document.createElement("canvas");t.width=200,t.height=20;const e=t.getContext("2d");if(!e)return"0";e.textBaseline="top",e.font="14px 'PulsoraFont123'",e.textBaseline="alphabetic",e.fillStyle="#f60",e.fillRect(125,1,62,20),e.fillStyle="#069",e.fillText("Cwm fjord 🎨 glyph",2,15);return t.toDataURL().substring(100,200)}catch(t){return"1"}}class r{constructor(){this.customerId=null,this.sessionId=t(),this.startTime=Date.now()}getSessionId(){return this.sessionId}getDuration(){return Math.floor((Date.now()-this.startTime)/1e3)}setCustomerId(t){this.customerId=t}getCustomerId(){return this.customerId}isIdentified(){return null!==this.customerId}reset(){this.sessionId=t(),this.customerId=null,this.startTime=Date.now()}clearIdentification(){this.customerId=null}}class a{constructor(t){this.queue=new Map,this.retryTimeouts=new Map,this.config=t}async send(e){const i={id:t(),timestamp:Date.now(),attempts:0,data:e};await this.sendEvent(i)}async sendEvent(t){t.attempts++;try{const e={type:t.data.type,data:this.prepareEventData(t.data),token:this.config.apiToken};if(navigator.sendBeacon&&"identify"!==t.data.type){const n=new Blob([JSON.stringify(e)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,n))return this.config.debug&&i(0,t.data),void this.removeFromQueue(t.id)}const n=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(e),keepalive:!0});if(n.ok)return this.config.debug&&i(0,t.data),void this.removeFromQueue(t.id);if(429===n.status){const e=parseInt(n.headers.get("Retry-After")||"60",10);return this.config.debug&&i(),void this.scheduleRetry(t,1e3*e)}throw new Error(`HTTP ${n.status}`)}catch(e){if(this.config.debug&&i(),t.attempts<this.config.maxRetries){const e=Math.min(this.config.retryBackoff*Math.pow(2,t.attempts-1),3e4);this.scheduleRetry(t,e)}else this.config.debug&&i(t.attempts),this.removeFromQueue(t.id)}}prepareEventData(t){const{type:e,timestamp:i,...n}=t,s={};for(const[t,e]of Object.entries(n))void 0!==e&&(s[t]=e);return s}scheduleRetry(t,e){this.queue.set(t.id,t);const i=this.retryTimeouts.get(t.id);i&&clearTimeout(i);const n=setTimeout(()=>{this.retryTimeouts.delete(t.id);const e=this.queue.get(t.id);e&&this.sendEvent(e)},e);this.retryTimeouts.set(t.id,n)}removeFromQueue(t){this.queue.delete(t);const e=this.retryTimeouts.get(t);e&&(clearTimeout(e),this.retryTimeouts.delete(t))}flush(){const t=Array.from(this.queue.values());this.queue.clear();for(const t of this.retryTimeouts.values())clearTimeout(t);this.retryTimeouts.clear();for(const e of t)this.sendEvent(e)}get queueSize(){return this.queue.size}}const h=new class{constructor(){this.extensions=new Map,this.initialized=!1,this.sessionManager=new r}init(t){if(this.initialized)t.debug&&i();else{if("undefined"==typeof window)throw new Error("Browser environment required");this.config={endpoint:"https://api.pulsora.co/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...t},this.transport=new a({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),this.fingerprintPromise=n(),this.fingerprintPromise.then(t=>{var e;this.visitorFingerprint=t,(null===(e=this.config)||void 0===e?void 0:e.debug)&&i()}),this.initialized=!0,this.config.autoPageviews&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.pageview()):this.pageview(),this.setupSPATracking()),this.setupUnloadHandlers(),this.config.debug&&i(0,this.config)}}async pageview(t){if(!this.isReady())return;const i=(null==t?void 0:t.url)||location.href,n=(null==t?void 0:t.referrer)||document.referrer,s=(null==t?void 0:t.title)||document.title,o=e(i),r=function(t){if(t)try{const e=new URL(t).hostname.toLowerCase();if(e===location.hostname.toLowerCase())return;return e.replace(/^www\./,"")}catch(t){return}}(n),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),url:i,path:o.path,referrer:n||void 0,referrer_source:r,title:s,utm_source:o.search.utm_source,utm_medium:o.search.utm_medium,utm_campaign:o.search.utm_campaign,utm_term:o.search.utm_term,utm_content:o.search.utm_content,timestamp:Date.now()};this.transport.send(a)}async event(t,i){if(!this.isReady()||!t)return;const n=location.href,s=e(n),o={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),event_name:t,event_data:i,url:n,path:s.path,timestamp:Date.now()};this.transport.send(o)}async identify(t){var e;if(!this.isReady()||!t)return;this.sessionManager.setCustomerId(t);const n={type:"identify",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),customer_id:t,timestamp:Date.now()};this.transport.send(n),(null===(e=this.config)||void 0===e?void 0:e.debug)&&i()}reset(){var t;this.sessionManager.reset(),(null===(t=this.config)||void 0===t?void 0:t.debug)&&i()}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?(this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint):(this.fingerprintPromise=n(),this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint)}getSessionId(){return this.sessionManager.getSessionId()}isIdentified(){return this.sessionManager.isIdentified()}use(t){var e,n;this.extensions.has(t.name)?(null===(e=this.config)||void 0===e?void 0:e.debug)&&i(t.name):(this.extensions.set(t.name,t),t.init(this),(null===(n=this.config)||void 0===n?void 0:n.debug)&&i(t.name))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Not initialized"),!1)}setupSPATracking(){const t=history.pushState,e=history.replaceState;history.pushState=(...e)=>{t.apply(history,e),setTimeout(()=>this.pageview(),0)},history.replaceState=(...t)=>{e.apply(history,t),setTimeout(()=>this.pageview(),0)},addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const t=()=>{var t;return null===(t=this.transport)||void 0===t?void 0:t.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&t()}),addEventListener("pagehide",t),addEventListener("beforeunload",t)}};if("undefined"!=typeof window&&"undefined"!=typeof document&&(window.pulsora=h,document.currentScript)){const t=document.currentScript,e=t.getAttribute("data-token"),i=t.getAttribute("data-endpoint"),n="true"===t.getAttribute("data-debug");e&&h.init({apiToken:e,endpoint:i||void 0,debug:n})}return h});
|
|
2
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sources":["../src/utils.ts","../src/fingerprint.ts","../src/session.ts","../src/transport.ts","../src/browser.ts","../src/tracker.ts"],"sourcesContent":["/**\n * Generate a UUID v4\n */\nexport function generateUUID(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Compact fallback for older browsers\n let d = Date.now();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (d + Math.random() * 16) % 16 | 0;\n d = Math.floor(d / 16);\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n/**\n * SHA-256 hash function with fallback\n */\nexport async function sha256(str: string): Promise<string> {\n if (typeof crypto !== 'undefined' && crypto.subtle && crypto.subtle.digest) {\n try {\n const buf = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(str),\n );\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n } catch {}\n }\n\n // Fallback: simple hash (good enough for fingerprinting)\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash << 5) - hash + str.charCodeAt(i);\n hash = hash & hash;\n }\n return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\n/**\n * Parse URL and extract components\n */\nexport function parseUrl(url: string): {\n path: string;\n search: Record<string, string>;\n} {\n try {\n const u = new URL(url);\n const search: Record<string, string> = {};\n u.searchParams.forEach((v, k) => {\n search[k] = v;\n });\n return { path: u.pathname, search };\n } catch {\n return { path: '/', search: {} };\n }\n}\n\n/**\n * Get referrer source from URL\n * Returns the domain name for ANY external referrer\n */\nexport function getReferrerSource(referrer: string): string | undefined {\n if (!referrer) return;\n\n try {\n const referrerHost = new URL(referrer).hostname.toLowerCase();\n\n // Skip if same domain\n if (referrerHost === location.hostname.toLowerCase()) return;\n\n // Return the clean hostname (remove www. prefix if present)\n return referrerHost.replace(/^www\\./, '');\n } catch {\n return undefined;\n }\n}\n\n/**\n * Simple debounce function\n */\nexport function debounce<T extends (...args: any[]) => void>(\n fn: T,\n delay: number,\n): T {\n let timeout: any;\n return ((...args: any[]) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), delay);\n }) as T;\n}\n\n/**\n * Check if we're in a browser environment\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/**\n * Debug logging\n */\nexport function debugLog(message: string, data?: any): void {\n if (typeof console !== 'undefined' && console.log) {\n if (data !== undefined) {\n console.log(`[Pulsora] ${message}`, data);\n } else {\n console.log(`[Pulsora] ${message}`);\n }\n }\n}\n","import { sha256 } from './utils';\n\n/**\n * Generates a unique, stable browser fingerprint for tracking\n * Uses multiple browser characteristics to create a highly unique identifier\n * GDPR compliant - no PII is collected\n */\nexport async function generateFingerprint(): Promise<string> {\n const components = [\n // User agent and language (high entropy)\n navigator.userAgent,\n navigator.language,\n (navigator.languages || []).join(','),\n\n // Screen (very high entropy, cheap)\n screen.width,\n screen.height,\n screen.colorDepth,\n window.devicePixelRatio || 1,\n\n // Hardware\n navigator.hardwareConcurrency || 0,\n (navigator as any).deviceMemory || 0,\n navigator.maxTouchPoints || 0,\n\n // Browser/OS\n navigator.platform,\n new Date().getTimezoneOffset(),\n\n // WebGL (high entropy)\n getWebGLFingerprint(),\n\n // Canvas (high entropy, optimized)\n await getCanvasFingerprint(),\n ];\n\n return sha256(components.join('~'));\n}\n\n/**\n * WebGL fingerprinting - vendor and renderer info\n */\nfunction getWebGLFingerprint(): string {\n try {\n const canvas = document.createElement('canvas');\n const gl =\n canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) return '0';\n\n const debugInfo = (gl as any).getExtension('WEBGL_debug_renderer_info');\n if (!debugInfo) return '1';\n\n const vendor = (gl as any).getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);\n const renderer = (gl as any).getParameter(\n debugInfo.UNMASKED_RENDERER_WEBGL,\n );\n\n return vendor + '~' + renderer;\n } catch {\n return '2';\n }\n}\n\n/**\n * Canvas fingerprinting - optimized for size\n */\nasync function getCanvasFingerprint(): Promise<string> {\n try {\n const canvas = document.createElement('canvas');\n canvas.width = 200;\n canvas.height = 20;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return '0';\n\n // Use non-existent font to force system fallback (high entropy)\n ctx.textBaseline = 'top';\n ctx.font = \"14px 'PulsoraFont123'\";\n ctx.textBaseline = 'alphabetic';\n ctx.fillStyle = '#f60';\n ctx.fillRect(125, 1, 62, 20);\n ctx.fillStyle = '#069';\n\n // Text with emoji for extra entropy\n ctx.fillText('Cwm fjord 🎨 glyph', 2, 15);\n\n // Extract just a portion of the data URL to save space\n const dataUrl = canvas.toDataURL();\n // Take middle portion for better entropy\n return dataUrl.substring(100, 200);\n } catch {\n return '1';\n }\n}\n","import { generateUUID } from './utils';\n\n/**\n * Session manager for tracking user sessions\n * Handles session ID generation and customer identification\n */\nexport class SessionManager {\n private sessionId: string;\n private customerId: string | null = null;\n private startTime: number;\n\n constructor() {\n this.sessionId = generateUUID();\n this.startTime = Date.now();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n getDuration(): number {\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n setCustomerId(id: string): void {\n this.customerId = id;\n }\n\n getCustomerId(): string | null {\n return this.customerId;\n }\n\n isIdentified(): boolean {\n return this.customerId !== null;\n }\n\n reset(): void {\n this.sessionId = generateUUID();\n this.customerId = null;\n this.startTime = Date.now();\n }\n\n clearIdentification(): void {\n this.customerId = null;\n }\n}\n","import { QueuedEvent, TrackingData } from './types';\nimport { debugLog, generateUUID } from './utils';\n\nexport interface TransportConfig {\n endpoint: string;\n apiToken: string;\n maxRetries: number;\n retryBackoff: number;\n debug: boolean;\n}\n\n/**\n * Transport layer for sending events to the API\n * Handles retries, queuing, and network failures\n */\nexport class Transport {\n private config: TransportConfig;\n private queue = new Map<string, QueuedEvent>();\n private retryTimeouts = new Map<string, any>();\n\n constructor(config: TransportConfig) {\n this.config = config;\n }\n\n async send(data: TrackingData): Promise<void> {\n const event: QueuedEvent = {\n id: generateUUID(),\n timestamp: Date.now(),\n attempts: 0,\n data,\n };\n await this.sendEvent(event);\n }\n\n private async sendEvent(event: QueuedEvent): Promise<void> {\n event.attempts++;\n\n try {\n const payload = {\n type: event.data.type,\n data: this.prepareEventData(event.data),\n token: this.config.apiToken,\n };\n\n // Try sendBeacon first (except for identify)\n if (navigator.sendBeacon && event.data.type !== 'identify') {\n const blob = new Blob([JSON.stringify(payload)], {\n type: 'application/json',\n });\n\n if (navigator.sendBeacon(this.config.endpoint, blob)) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n }\n\n // Fallback to fetch\n const response = await fetch(this.config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Token': this.config.apiToken,\n },\n body: JSON.stringify(payload),\n keepalive: true,\n });\n\n if (response.ok) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n\n // Rate limit\n if (response.status === 429) {\n const retryAfter = parseInt(\n response.headers.get('Retry-After') || '60',\n 10,\n );\n this.config.debug &&\n debugLog(`Rate limited, retry after ${retryAfter}s`);\n this.scheduleRetry(event, retryAfter * 1000);\n return;\n }\n\n throw new Error(`HTTP ${response.status}`);\n } catch (error) {\n this.config.debug && debugLog('Send failed', { error, event });\n\n if (event.attempts < this.config.maxRetries) {\n const delay = Math.min(\n this.config.retryBackoff * Math.pow(2, event.attempts - 1),\n 30000,\n );\n this.scheduleRetry(event, delay);\n } else {\n this.config.debug &&\n debugLog(`Dropped after ${event.attempts} attempts`);\n this.removeFromQueue(event.id);\n }\n }\n }\n\n private prepareEventData(data: TrackingData): Record<string, any> {\n const { type, timestamp, ...rest } = data;\n const cleanData: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(rest)) {\n if (value !== undefined) {\n cleanData[key] = value;\n }\n }\n\n return cleanData;\n }\n\n private scheduleRetry(event: QueuedEvent, delay: number): void {\n this.queue.set(event.id, event);\n\n const existingTimeout = this.retryTimeouts.get(event.id);\n if (existingTimeout) {\n clearTimeout(existingTimeout);\n }\n\n const timeout = setTimeout(() => {\n this.retryTimeouts.delete(event.id);\n const queuedEvent = this.queue.get(event.id);\n if (queuedEvent) {\n this.sendEvent(queuedEvent);\n }\n }, delay);\n\n this.retryTimeouts.set(event.id, timeout);\n }\n\n private removeFromQueue(eventId: string): void {\n this.queue.delete(eventId);\n const timeout = this.retryTimeouts.get(eventId);\n if (timeout) {\n clearTimeout(timeout);\n this.retryTimeouts.delete(eventId);\n }\n }\n\n flush(): void {\n const events = Array.from(this.queue.values());\n this.queue.clear();\n\n for (const timeout of this.retryTimeouts.values()) {\n clearTimeout(timeout);\n }\n this.retryTimeouts.clear();\n\n for (const event of events) {\n this.sendEvent(event);\n }\n }\n\n get queueSize(): number {\n return this.queue.size;\n }\n}\n","// Browser/CDN entry point with singleton instance and auto-initialization\nimport { Tracker } from './tracker';\n\n// Create singleton instance\nconst pulsora = new Tracker();\n\n// Auto-initialize if data-token is present on script tag\nif (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Attach to window for global access\n (window as any).pulsora = pulsora;\n\n // Check for script tag with data attributes\n if (document.currentScript) {\n const script = document.currentScript as HTMLScriptElement;\n const token = script.getAttribute('data-token');\n const endpoint = script.getAttribute('data-endpoint');\n const debug = script.getAttribute('data-debug') === 'true';\n\n if (token) {\n pulsora.init({\n apiToken: token,\n endpoint: endpoint || undefined,\n debug,\n });\n }\n }\n}\n\n// Export for module systems (though typically used via window.pulsora)\nexport default pulsora;\n","import { generateFingerprint } from './fingerprint';\nimport { SessionManager } from './session';\nimport { Transport } from './transport';\nimport {\n EventData,\n PageviewOptions,\n PulsoraConfig,\n PulsoraCore,\n PulsoraExtension,\n TrackingData,\n} from './types';\nimport { debugLog, getReferrerSource, parseUrl } from './utils';\n\n/**\n * Main Pulsora tracker implementation\n * Handles pageview tracking, custom events, and user identification\n */\nexport class Tracker implements PulsoraCore {\n private config?: PulsoraConfig;\n private transport?: Transport;\n private sessionManager: SessionManager;\n private visitorFingerprint?: string;\n private fingerprintPromise?: Promise<string>;\n private extensions = new Map<string, PulsoraExtension>();\n private initialized = false;\n\n constructor() {\n this.sessionManager = new SessionManager();\n }\n\n init(config: PulsoraConfig): void {\n if (this.initialized) {\n config.debug && debugLog('Already initialized');\n return;\n }\n\n if (typeof window === 'undefined') {\n throw new Error('Browser environment required');\n }\n\n this.config = {\n endpoint: 'https://api.pulsora.co/ingest',\n autoPageviews: true,\n debug: false,\n maxRetries: 10,\n retryBackoff: 1000,\n ...config,\n };\n\n this.transport = new Transport({\n endpoint: this.config.endpoint!,\n apiToken: this.config.apiToken,\n maxRetries: this.config.maxRetries!,\n retryBackoff: this.config.retryBackoff!,\n debug: this.config.debug!,\n });\n\n // Start fingerprint generation\n this.fingerprintPromise = generateFingerprint();\n this.fingerprintPromise.then((fingerprint) => {\n this.visitorFingerprint = fingerprint;\n this.config?.debug && debugLog('Fingerprint ready', fingerprint);\n });\n\n this.initialized = true;\n\n // Auto pageviews\n if (this.config.autoPageviews) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => this.pageview());\n } else {\n this.pageview();\n }\n this.setupSPATracking();\n }\n\n // Flush on unload\n this.setupUnloadHandlers();\n\n this.config.debug && debugLog('Initialized', this.config);\n }\n\n async pageview(options?: PageviewOptions): Promise<void> {\n if (!this.isReady()) return;\n\n const url = options?.url || location.href;\n const referrer = options?.referrer || document.referrer;\n const title = options?.title || document.title;\n\n const parsed = parseUrl(url);\n const referrerSource = getReferrerSource(referrer);\n\n const data: TrackingData = {\n type: 'pageview',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n url,\n path: parsed.path,\n referrer: referrer || undefined,\n referrer_source: referrerSource,\n title,\n utm_source: parsed.search.utm_source,\n utm_medium: parsed.search.utm_medium,\n utm_campaign: parsed.search.utm_campaign,\n utm_term: parsed.search.utm_term,\n utm_content: parsed.search.utm_content,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async event(eventName: string, eventData?: EventData): Promise<void> {\n if (!this.isReady() || !eventName) return;\n\n const url = location.href;\n const parsed = parseUrl(url);\n\n const data: TrackingData = {\n type: 'event',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n event_name: eventName,\n event_data: eventData,\n url,\n path: parsed.path,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async identify(customerId: string): Promise<void> {\n if (!this.isReady() || !customerId) return;\n\n this.sessionManager.setCustomerId(customerId);\n\n const data: TrackingData = {\n type: 'identify',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n customer_id: customerId,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n this.config?.debug && debugLog('User identified', customerId);\n }\n\n reset(): void {\n this.sessionManager.reset();\n this.config?.debug && debugLog('Session reset');\n }\n\n async getVisitorFingerprint(): Promise<string> {\n if (this.visitorFingerprint) return this.visitorFingerprint;\n if (this.fingerprintPromise) {\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n this.fingerprintPromise = generateFingerprint();\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n getSessionId(): string {\n return this.sessionManager.getSessionId();\n }\n\n isIdentified(): boolean {\n return this.sessionManager.isIdentified();\n }\n\n use(extension: PulsoraExtension): void {\n if (this.extensions.has(extension.name)) {\n this.config?.debug &&\n debugLog(`Extension ${extension.name} already loaded`);\n return;\n }\n this.extensions.set(extension.name, extension);\n extension.init(this);\n this.config?.debug && debugLog(`Extension ${extension.name} loaded`);\n }\n\n private isReady(): boolean {\n if (!this.initialized) {\n console.warn('[Pulsora] Not initialized');\n return false;\n }\n return true;\n }\n\n private setupSPATracking(): void {\n // Intercept pushState/replaceState\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n history.pushState = (...args) => {\n originalPushState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n history.replaceState = (...args) => {\n originalReplaceState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Back/forward navigation\n addEventListener('popstate', () => {\n setTimeout(() => this.pageview(), 0);\n });\n }\n\n private setupUnloadHandlers(): void {\n const flush = () => this.transport?.flush();\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush();\n });\n\n addEventListener('pagehide', flush);\n addEventListener('beforeunload', flush);\n }\n}\n"],"names":["generateUUID","crypto","randomUUID","d","Date","now","replace","c","r","Math","random","floor","toString","parseUrl","url","u","URL","search","searchParams","forEach","v","k","path","pathname","_a","debugLog","message","data","console","log","async","generateFingerprint","str","subtle","digest","buf","TextEncoder","encode","Array","from","Uint8Array","map","b","padStart","join","hash","i","length","charCodeAt","abs","sha256","navigator","userAgent","language","languages","screen","width","height","colorDepth","window","devicePixelRatio","hardwareConcurrency","deviceMemory","maxTouchPoints","platform","getTimezoneOffset","getWebGLFingerprint","getCanvasFingerprint","canvas","document","createElement","gl","getContext","debugInfo","getExtension","vendor","getParameter","UNMASKED_VENDOR_WEBGL","UNMASKED_RENDERER_WEBGL","ctx","textBaseline","font","fillStyle","fillRect","fillText","toDataURL","substring","SessionManager","constructor","this","customerId","sessionId","startTime","getSessionId","getDuration","setCustomerId","id","getCustomerId","isIdentified","reset","clearIdentification","Transport","config","queue","Map","retryTimeouts","send","event","timestamp","attempts","sendEvent","payload","type","prepareEventData","token","apiToken","sendBeacon","blob","Blob","JSON","stringify","endpoint","debug","removeFromQueue","response","fetch","method","headers","body","keepalive","ok","status","retryAfter","parseInt","get","scheduleRetry","Error","error","maxRetries","delay","min","retryBackoff","pow","rest","cleanData","key","value","Object","entries","undefined","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","eventId","flush","events","values","clear","queueSize","size","pulsora","extensions","initialized","sessionManager","init","autoPageviews","transport","fingerprintPromise","then","fingerprint","visitorFingerprint","readyState","addEventListener","pageview","setupSPATracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","parsed","referrerSource","referrerHost","hostname","toLowerCase","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","utm_source","utm_medium","utm_campaign","utm_term","utm_content","eventName","eventData","event_name","event_data","identify","customer_id","use","extension","has","name","_b","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","visibilityState","currentScript","script","getAttribute"],"mappings":"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"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";function t(){if("undefined"!=typeof crypto&&crypto.randomUUID)return crypto.randomUUID();let t=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const i=(t+16*Math.random())%16|0;return t=Math.floor(t/16),("x"===e?i:3&i|8).toString(16)})}function e(t){try{const e=new URL(t),i={};return e.searchParams.forEach((t,e)=>{i[e]=t}),{path:e.pathname,search:i}}catch(t){return{path:"/",search:{}}}}function i(t,e){"undefined"!=typeof console&&console.log&&(void 0!==e?console.log(`[Pulsora] ${t}`,e):console.log(`[Pulsora] ${t}`))}async function n(){return async function(t){if("undefined"!=typeof crypto&&crypto.subtle&&crypto.subtle.digest)try{const e=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(t));return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}catch(t){}let e=0;for(let i=0;i<t.length;i++)e=(e<<5)-e+t.charCodeAt(i),e&=e;return Math.abs(e).toString(16).padStart(8,"0")}([navigator.userAgent,navigator.language,(navigator.languages||[]).join(","),screen.width,screen.height,screen.colorDepth,window.devicePixelRatio||1,navigator.hardwareConcurrency||0,navigator.deviceMemory||0,navigator.maxTouchPoints||0,navigator.platform,(new Date).getTimezoneOffset(),s(),await r()].join("~"))}function s(){try{const t=document.createElement("canvas"),e=t.getContext("webgl")||t.getContext("experimental-webgl");if(!e)return"0";const i=e.getExtension("WEBGL_debug_renderer_info");if(!i)return"1";const n=e.getParameter(i.UNMASKED_VENDOR_WEBGL);return n+"~"+e.getParameter(i.UNMASKED_RENDERER_WEBGL)}catch(t){return"2"}}async function r(){try{const t=document.createElement("canvas");t.width=200,t.height=20;const e=t.getContext("2d");if(!e)return"0";e.textBaseline="top",e.font="14px 'PulsoraFont123'",e.textBaseline="alphabetic",e.fillStyle="#f60",e.fillRect(125,1,62,20),e.fillStyle="#069",e.fillText("Cwm fjord 🎨 glyph",2,15);return t.toDataURL().substring(100,200)}catch(t){return"1"}}class o{constructor(){this.customerId=null,this.sessionId=t(),this.startTime=Date.now()}getSessionId(){return this.sessionId}getDuration(){return Math.floor((Date.now()-this.startTime)/1e3)}setCustomerId(t){this.customerId=t}getCustomerId(){return this.customerId}isIdentified(){return null!==this.customerId}reset(){this.sessionId=t(),this.customerId=null,this.startTime=Date.now()}clearIdentification(){this.customerId=null}}class a{constructor(t){this.queue=new Map,this.retryTimeouts=new Map,this.config=t}async send(e){const i={id:t(),timestamp:Date.now(),attempts:0,data:e};await this.sendEvent(i)}async sendEvent(t){t.attempts++;try{const e={type:t.data.type,data:this.prepareEventData(t.data),token:this.config.apiToken};if(navigator.sendBeacon&&"identify"!==t.data.type){const n=new Blob([JSON.stringify(e)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,n))return this.config.debug&&i("Event sent",t.data),void this.removeFromQueue(t.id)}const n=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(e),keepalive:!0});if(n.ok)return this.config.debug&&i("Event sent",t.data),void this.removeFromQueue(t.id);if(429===n.status){const e=parseInt(n.headers.get("Retry-After")||"60",10);return this.config.debug&&i(`Rate limited, retry after ${e}s`),void this.scheduleRetry(t,1e3*e)}throw new Error(`HTTP ${n.status}`)}catch(e){if(this.config.debug&&i("Send failed",{error:e,event:t}),t.attempts<this.config.maxRetries){const e=Math.min(this.config.retryBackoff*Math.pow(2,t.attempts-1),3e4);this.scheduleRetry(t,e)}else this.config.debug&&i(`Dropped after ${t.attempts} attempts`),this.removeFromQueue(t.id)}}prepareEventData(t){const{type:e,timestamp:i,...n}=t,s={};for(const[t,e]of Object.entries(n))void 0!==e&&(s[t]=e);return s}scheduleRetry(t,e){this.queue.set(t.id,t);const i=this.retryTimeouts.get(t.id);i&&clearTimeout(i);const n=setTimeout(()=>{this.retryTimeouts.delete(t.id);const e=this.queue.get(t.id);e&&this.sendEvent(e)},e);this.retryTimeouts.set(t.id,n)}removeFromQueue(t){this.queue.delete(t);const e=this.retryTimeouts.get(t);e&&(clearTimeout(e),this.retryTimeouts.delete(t))}flush(){const t=Array.from(this.queue.values());this.queue.clear();for(const t of this.retryTimeouts.values())clearTimeout(t);this.retryTimeouts.clear();for(const e of t)this.sendEvent(e)}get queueSize(){return this.queue.size}}exports.Pulsora=class{constructor(){this.extensions=new Map,this.initialized=!1,this.sessionManager=new o}init(t){if(this.initialized)t.debug&&i("Already initialized");else{if("undefined"==typeof window)throw new Error("Browser environment required");this.config={endpoint:"https://api.pulsora.co/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...t},this.transport=new a({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),this.fingerprintPromise=n(),this.fingerprintPromise.then(t=>{var e;this.visitorFingerprint=t,(null===(e=this.config)||void 0===e?void 0:e.debug)&&i("Fingerprint ready",t)}),this.initialized=!0,this.config.autoPageviews&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.pageview()):this.pageview(),this.setupSPATracking()),this.setupUnloadHandlers(),this.config.debug&&i("Initialized",this.config)}}async pageview(t){if(!this.isReady())return;const i=(null==t?void 0:t.url)||location.href,n=(null==t?void 0:t.referrer)||document.referrer,s=(null==t?void 0:t.title)||document.title,r=e(i),o=function(t){if(t)try{const e=new URL(t).hostname.toLowerCase();if(e===location.hostname.toLowerCase())return;return e.replace(/^www\./,"")}catch(t){return}}(n),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),url:i,path:r.path,referrer:n||void 0,referrer_source:o,title:s,utm_source:r.search.utm_source,utm_medium:r.search.utm_medium,utm_campaign:r.search.utm_campaign,utm_term:r.search.utm_term,utm_content:r.search.utm_content,timestamp:Date.now()};this.transport.send(a)}async event(t,i){if(!this.isReady()||!t)return;const n=location.href,s=e(n),r={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),event_name:t,event_data:i,url:n,path:s.path,timestamp:Date.now()};this.transport.send(r)}async identify(t){var e;if(!this.isReady()||!t)return;this.sessionManager.setCustomerId(t);const n={type:"identify",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),customer_id:t,timestamp:Date.now()};this.transport.send(n),(null===(e=this.config)||void 0===e?void 0:e.debug)&&i("User identified",t)}reset(){var t;this.sessionManager.reset(),(null===(t=this.config)||void 0===t?void 0:t.debug)&&i("Session reset")}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?(this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint):(this.fingerprintPromise=n(),this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint)}getSessionId(){return this.sessionManager.getSessionId()}isIdentified(){return this.sessionManager.isIdentified()}use(t){var e,n;this.extensions.has(t.name)?(null===(e=this.config)||void 0===e?void 0:e.debug)&&i(`Extension ${t.name} already loaded`):(this.extensions.set(t.name,t),t.init(this),(null===(n=this.config)||void 0===n?void 0:n.debug)&&i(`Extension ${t.name} loaded`))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Not initialized"),!1)}setupSPATracking(){const t=history.pushState,e=history.replaceState;history.pushState=(...e)=>{t.apply(history,e),setTimeout(()=>this.pageview(),0)},history.replaceState=(...t)=>{e.apply(history,t),setTimeout(()=>this.pageview(),0)},addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const t=()=>{var t;return null===(t=this.transport)||void 0===t?void 0:t.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&t()}),addEventListener("pagehide",t),addEventListener("beforeunload",t)}};
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/utils.ts","../src/fingerprint.ts","../src/session.ts","../src/transport.ts","../src/tracker.ts"],"sourcesContent":["/**\n * Generate a UUID v4\n */\nexport function generateUUID(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Compact fallback for older browsers\n let d = Date.now();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (d + Math.random() * 16) % 16 | 0;\n d = Math.floor(d / 16);\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n/**\n * SHA-256 hash function with fallback\n */\nexport async function sha256(str: string): Promise<string> {\n if (typeof crypto !== 'undefined' && crypto.subtle && crypto.subtle.digest) {\n try {\n const buf = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(str),\n );\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n } catch {}\n }\n\n // Fallback: simple hash (good enough for fingerprinting)\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash << 5) - hash + str.charCodeAt(i);\n hash = hash & hash;\n }\n return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\n/**\n * Parse URL and extract components\n */\nexport function parseUrl(url: string): {\n path: string;\n search: Record<string, string>;\n} {\n try {\n const u = new URL(url);\n const search: Record<string, string> = {};\n u.searchParams.forEach((v, k) => {\n search[k] = v;\n });\n return { path: u.pathname, search };\n } catch {\n return { path: '/', search: {} };\n }\n}\n\n/**\n * Get referrer source from URL\n * Returns the domain name for ANY external referrer\n */\nexport function getReferrerSource(referrer: string): string | undefined {\n if (!referrer) return;\n\n try {\n const referrerHost = new URL(referrer).hostname.toLowerCase();\n\n // Skip if same domain\n if (referrerHost === location.hostname.toLowerCase()) return;\n\n // Return the clean hostname (remove www. prefix if present)\n return referrerHost.replace(/^www\\./, '');\n } catch {\n return undefined;\n }\n}\n\n/**\n * Simple debounce function\n */\nexport function debounce<T extends (...args: any[]) => void>(\n fn: T,\n delay: number,\n): T {\n let timeout: any;\n return ((...args: any[]) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), delay);\n }) as T;\n}\n\n/**\n * Check if we're in a browser environment\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/**\n * Debug logging\n */\nexport function debugLog(message: string, data?: any): void {\n if (typeof console !== 'undefined' && console.log) {\n if (data !== undefined) {\n console.log(`[Pulsora] ${message}`, data);\n } else {\n console.log(`[Pulsora] ${message}`);\n }\n }\n}\n","import { sha256 } from './utils';\n\n/**\n * Generates a unique, stable browser fingerprint for tracking\n * Uses multiple browser characteristics to create a highly unique identifier\n * GDPR compliant - no PII is collected\n */\nexport async function generateFingerprint(): Promise<string> {\n const components = [\n // User agent and language (high entropy)\n navigator.userAgent,\n navigator.language,\n (navigator.languages || []).join(','),\n\n // Screen (very high entropy, cheap)\n screen.width,\n screen.height,\n screen.colorDepth,\n window.devicePixelRatio || 1,\n\n // Hardware\n navigator.hardwareConcurrency || 0,\n (navigator as any).deviceMemory || 0,\n navigator.maxTouchPoints || 0,\n\n // Browser/OS\n navigator.platform,\n new Date().getTimezoneOffset(),\n\n // WebGL (high entropy)\n getWebGLFingerprint(),\n\n // Canvas (high entropy, optimized)\n await getCanvasFingerprint(),\n ];\n\n return sha256(components.join('~'));\n}\n\n/**\n * WebGL fingerprinting - vendor and renderer info\n */\nfunction getWebGLFingerprint(): string {\n try {\n const canvas = document.createElement('canvas');\n const gl =\n canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) return '0';\n\n const debugInfo = (gl as any).getExtension('WEBGL_debug_renderer_info');\n if (!debugInfo) return '1';\n\n const vendor = (gl as any).getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);\n const renderer = (gl as any).getParameter(\n debugInfo.UNMASKED_RENDERER_WEBGL,\n );\n\n return vendor + '~' + renderer;\n } catch {\n return '2';\n }\n}\n\n/**\n * Canvas fingerprinting - optimized for size\n */\nasync function getCanvasFingerprint(): Promise<string> {\n try {\n const canvas = document.createElement('canvas');\n canvas.width = 200;\n canvas.height = 20;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return '0';\n\n // Use non-existent font to force system fallback (high entropy)\n ctx.textBaseline = 'top';\n ctx.font = \"14px 'PulsoraFont123'\";\n ctx.textBaseline = 'alphabetic';\n ctx.fillStyle = '#f60';\n ctx.fillRect(125, 1, 62, 20);\n ctx.fillStyle = '#069';\n\n // Text with emoji for extra entropy\n ctx.fillText('Cwm fjord 🎨 glyph', 2, 15);\n\n // Extract just a portion of the data URL to save space\n const dataUrl = canvas.toDataURL();\n // Take middle portion for better entropy\n return dataUrl.substring(100, 200);\n } catch {\n return '1';\n }\n}\n","import { generateUUID } from './utils';\n\n/**\n * Session manager for tracking user sessions\n * Handles session ID generation and customer identification\n */\nexport class SessionManager {\n private sessionId: string;\n private customerId: string | null = null;\n private startTime: number;\n\n constructor() {\n this.sessionId = generateUUID();\n this.startTime = Date.now();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n getDuration(): number {\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n setCustomerId(id: string): void {\n this.customerId = id;\n }\n\n getCustomerId(): string | null {\n return this.customerId;\n }\n\n isIdentified(): boolean {\n return this.customerId !== null;\n }\n\n reset(): void {\n this.sessionId = generateUUID();\n this.customerId = null;\n this.startTime = Date.now();\n }\n\n clearIdentification(): void {\n this.customerId = null;\n }\n}\n","import { QueuedEvent, TrackingData } from './types';\nimport { debugLog, generateUUID } from './utils';\n\nexport interface TransportConfig {\n endpoint: string;\n apiToken: string;\n maxRetries: number;\n retryBackoff: number;\n debug: boolean;\n}\n\n/**\n * Transport layer for sending events to the API\n * Handles retries, queuing, and network failures\n */\nexport class Transport {\n private config: TransportConfig;\n private queue = new Map<string, QueuedEvent>();\n private retryTimeouts = new Map<string, any>();\n\n constructor(config: TransportConfig) {\n this.config = config;\n }\n\n async send(data: TrackingData): Promise<void> {\n const event: QueuedEvent = {\n id: generateUUID(),\n timestamp: Date.now(),\n attempts: 0,\n data,\n };\n await this.sendEvent(event);\n }\n\n private async sendEvent(event: QueuedEvent): Promise<void> {\n event.attempts++;\n\n try {\n const payload = {\n type: event.data.type,\n data: this.prepareEventData(event.data),\n token: this.config.apiToken,\n };\n\n // Try sendBeacon first (except for identify)\n if (navigator.sendBeacon && event.data.type !== 'identify') {\n const blob = new Blob([JSON.stringify(payload)], {\n type: 'application/json',\n });\n\n if (navigator.sendBeacon(this.config.endpoint, blob)) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n }\n\n // Fallback to fetch\n const response = await fetch(this.config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Token': this.config.apiToken,\n },\n body: JSON.stringify(payload),\n keepalive: true,\n });\n\n if (response.ok) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n\n // Rate limit\n if (response.status === 429) {\n const retryAfter = parseInt(\n response.headers.get('Retry-After') || '60',\n 10,\n );\n this.config.debug &&\n debugLog(`Rate limited, retry after ${retryAfter}s`);\n this.scheduleRetry(event, retryAfter * 1000);\n return;\n }\n\n throw new Error(`HTTP ${response.status}`);\n } catch (error) {\n this.config.debug && debugLog('Send failed', { error, event });\n\n if (event.attempts < this.config.maxRetries) {\n const delay = Math.min(\n this.config.retryBackoff * Math.pow(2, event.attempts - 1),\n 30000,\n );\n this.scheduleRetry(event, delay);\n } else {\n this.config.debug &&\n debugLog(`Dropped after ${event.attempts} attempts`);\n this.removeFromQueue(event.id);\n }\n }\n }\n\n private prepareEventData(data: TrackingData): Record<string, any> {\n const { type, timestamp, ...rest } = data;\n const cleanData: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(rest)) {\n if (value !== undefined) {\n cleanData[key] = value;\n }\n }\n\n return cleanData;\n }\n\n private scheduleRetry(event: QueuedEvent, delay: number): void {\n this.queue.set(event.id, event);\n\n const existingTimeout = this.retryTimeouts.get(event.id);\n if (existingTimeout) {\n clearTimeout(existingTimeout);\n }\n\n const timeout = setTimeout(() => {\n this.retryTimeouts.delete(event.id);\n const queuedEvent = this.queue.get(event.id);\n if (queuedEvent) {\n this.sendEvent(queuedEvent);\n }\n }, delay);\n\n this.retryTimeouts.set(event.id, timeout);\n }\n\n private removeFromQueue(eventId: string): void {\n this.queue.delete(eventId);\n const timeout = this.retryTimeouts.get(eventId);\n if (timeout) {\n clearTimeout(timeout);\n this.retryTimeouts.delete(eventId);\n }\n }\n\n flush(): void {\n const events = Array.from(this.queue.values());\n this.queue.clear();\n\n for (const timeout of this.retryTimeouts.values()) {\n clearTimeout(timeout);\n }\n this.retryTimeouts.clear();\n\n for (const event of events) {\n this.sendEvent(event);\n }\n }\n\n get queueSize(): number {\n return this.queue.size;\n }\n}\n","import { generateFingerprint } from './fingerprint';\nimport { SessionManager } from './session';\nimport { Transport } from './transport';\nimport {\n EventData,\n PageviewOptions,\n PulsoraConfig,\n PulsoraCore,\n PulsoraExtension,\n TrackingData,\n} from './types';\nimport { debugLog, getReferrerSource, parseUrl } from './utils';\n\n/**\n * Main Pulsora tracker implementation\n * Handles pageview tracking, custom events, and user identification\n */\nexport class Tracker implements PulsoraCore {\n private config?: PulsoraConfig;\n private transport?: Transport;\n private sessionManager: SessionManager;\n private visitorFingerprint?: string;\n private fingerprintPromise?: Promise<string>;\n private extensions = new Map<string, PulsoraExtension>();\n private initialized = false;\n\n constructor() {\n this.sessionManager = new SessionManager();\n }\n\n init(config: PulsoraConfig): void {\n if (this.initialized) {\n config.debug && debugLog('Already initialized');\n return;\n }\n\n if (typeof window === 'undefined') {\n throw new Error('Browser environment required');\n }\n\n this.config = {\n endpoint: 'https://api.pulsora.co/ingest',\n autoPageviews: true,\n debug: false,\n maxRetries: 10,\n retryBackoff: 1000,\n ...config,\n };\n\n this.transport = new Transport({\n endpoint: this.config.endpoint!,\n apiToken: this.config.apiToken,\n maxRetries: this.config.maxRetries!,\n retryBackoff: this.config.retryBackoff!,\n debug: this.config.debug!,\n });\n\n // Start fingerprint generation\n this.fingerprintPromise = generateFingerprint();\n this.fingerprintPromise.then((fingerprint) => {\n this.visitorFingerprint = fingerprint;\n this.config?.debug && debugLog('Fingerprint ready', fingerprint);\n });\n\n this.initialized = true;\n\n // Auto pageviews\n if (this.config.autoPageviews) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => this.pageview());\n } else {\n this.pageview();\n }\n this.setupSPATracking();\n }\n\n // Flush on unload\n this.setupUnloadHandlers();\n\n this.config.debug && debugLog('Initialized', this.config);\n }\n\n async pageview(options?: PageviewOptions): Promise<void> {\n if (!this.isReady()) return;\n\n const url = options?.url || location.href;\n const referrer = options?.referrer || document.referrer;\n const title = options?.title || document.title;\n\n const parsed = parseUrl(url);\n const referrerSource = getReferrerSource(referrer);\n\n const data: TrackingData = {\n type: 'pageview',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n url,\n path: parsed.path,\n referrer: referrer || undefined,\n referrer_source: referrerSource,\n title,\n utm_source: parsed.search.utm_source,\n utm_medium: parsed.search.utm_medium,\n utm_campaign: parsed.search.utm_campaign,\n utm_term: parsed.search.utm_term,\n utm_content: parsed.search.utm_content,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async event(eventName: string, eventData?: EventData): Promise<void> {\n if (!this.isReady() || !eventName) return;\n\n const url = location.href;\n const parsed = parseUrl(url);\n\n const data: TrackingData = {\n type: 'event',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n event_name: eventName,\n event_data: eventData,\n url,\n path: parsed.path,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async identify(customerId: string): Promise<void> {\n if (!this.isReady() || !customerId) return;\n\n this.sessionManager.setCustomerId(customerId);\n\n const data: TrackingData = {\n type: 'identify',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n customer_id: customerId,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n this.config?.debug && debugLog('User identified', customerId);\n }\n\n reset(): void {\n this.sessionManager.reset();\n this.config?.debug && debugLog('Session reset');\n }\n\n async getVisitorFingerprint(): Promise<string> {\n if (this.visitorFingerprint) return this.visitorFingerprint;\n if (this.fingerprintPromise) {\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n this.fingerprintPromise = generateFingerprint();\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n getSessionId(): string {\n return this.sessionManager.getSessionId();\n }\n\n isIdentified(): boolean {\n return this.sessionManager.isIdentified();\n }\n\n use(extension: PulsoraExtension): void {\n if (this.extensions.has(extension.name)) {\n this.config?.debug &&\n debugLog(`Extension ${extension.name} already loaded`);\n return;\n }\n this.extensions.set(extension.name, extension);\n extension.init(this);\n this.config?.debug && debugLog(`Extension ${extension.name} loaded`);\n }\n\n private isReady(): boolean {\n if (!this.initialized) {\n console.warn('[Pulsora] Not initialized');\n return false;\n }\n return true;\n }\n\n private setupSPATracking(): void {\n // Intercept pushState/replaceState\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n history.pushState = (...args) => {\n originalPushState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n history.replaceState = (...args) => {\n originalReplaceState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Back/forward navigation\n addEventListener('popstate', () => {\n setTimeout(() => this.pageview(), 0);\n });\n }\n\n private setupUnloadHandlers(): void {\n const flush = () => this.transport?.flush();\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush();\n });\n\n addEventListener('pagehide', flush);\n addEventListener('beforeunload', flush);\n }\n}\n"],"names":["generateUUID","crypto","randomUUID","d","Date","now","replace","c","r","Math","random","floor","toString","parseUrl","url","u","URL","search","searchParams","forEach","v","k","path","pathname","_a","debugLog","message","data","console","log","undefined","async","generateFingerprint","str","subtle","digest","buf","TextEncoder","encode","Array","from","Uint8Array","map","b","padStart","join","hash","i","length","charCodeAt","abs","sha256","navigator","userAgent","language","languages","screen","width","height","colorDepth","window","devicePixelRatio","hardwareConcurrency","deviceMemory","maxTouchPoints","platform","getTimezoneOffset","getWebGLFingerprint","getCanvasFingerprint","canvas","document","createElement","gl","getContext","debugInfo","getExtension","vendor","getParameter","UNMASKED_VENDOR_WEBGL","UNMASKED_RENDERER_WEBGL","ctx","textBaseline","font","fillStyle","fillRect","fillText","toDataURL","substring","SessionManager","constructor","this","customerId","sessionId","startTime","getSessionId","getDuration","setCustomerId","id","getCustomerId","isIdentified","reset","clearIdentification","Transport","config","queue","Map","retryTimeouts","send","event","timestamp","attempts","sendEvent","payload","type","prepareEventData","token","apiToken","sendBeacon","blob","Blob","JSON","stringify","endpoint","debug","removeFromQueue","response","fetch","method","headers","body","keepalive","ok","status","retryAfter","parseInt","get","scheduleRetry","Error","error","maxRetries","delay","min","retryBackoff","pow","rest","cleanData","key","value","Object","entries","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","eventId","flush","events","values","clear","queueSize","size","extensions","initialized","sessionManager","init","autoPageviews","transport","fingerprintPromise","then","fingerprint","visitorFingerprint","readyState","addEventListener","pageview","setupSPATracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","parsed","referrerSource","referrerHost","hostname","toLowerCase","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","utm_source","utm_medium","utm_campaign","utm_term","utm_content","eventName","eventData","event_name","event_data","identify","customer_id","use","extension","has","name","_b","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","visibilityState"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
interface PulsoraConfig {
|
|
2
|
+
apiToken: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
autoPageviews?: boolean;
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
retryBackoff?: number;
|
|
8
|
+
}
|
|
9
|
+
interface PageviewOptions {
|
|
10
|
+
url?: string;
|
|
11
|
+
referrer?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
}
|
|
14
|
+
interface EventData {
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
interface PulsoraExtension {
|
|
18
|
+
name: string;
|
|
19
|
+
init(core: PulsoraCore): void;
|
|
20
|
+
}
|
|
21
|
+
interface PulsoraCore {
|
|
22
|
+
init(config: PulsoraConfig): void;
|
|
23
|
+
pageview(options?: PageviewOptions): void;
|
|
24
|
+
event(eventName: string, eventData?: EventData): void;
|
|
25
|
+
identify(customerId: string): void;
|
|
26
|
+
reset(): void;
|
|
27
|
+
getVisitorFingerprint(): Promise<string>;
|
|
28
|
+
getSessionId(): string;
|
|
29
|
+
isIdentified(): boolean;
|
|
30
|
+
use(extension: PulsoraExtension): void;
|
|
31
|
+
}
|
|
32
|
+
interface QueuedEvent {
|
|
33
|
+
id: string;
|
|
34
|
+
timestamp: number;
|
|
35
|
+
attempts: number;
|
|
36
|
+
data: TrackingData;
|
|
37
|
+
}
|
|
38
|
+
interface TrackingData {
|
|
39
|
+
type: 'pageview' | 'event' | 'identify';
|
|
40
|
+
visitor_fingerprint: string;
|
|
41
|
+
session_id: string;
|
|
42
|
+
url?: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
referrer?: string;
|
|
45
|
+
referrer_source?: string;
|
|
46
|
+
title?: string;
|
|
47
|
+
event_name?: string;
|
|
48
|
+
event_data?: EventData;
|
|
49
|
+
customer_id?: string;
|
|
50
|
+
utm_source?: string;
|
|
51
|
+
utm_medium?: string;
|
|
52
|
+
utm_campaign?: string;
|
|
53
|
+
utm_term?: string;
|
|
54
|
+
utm_content?: string;
|
|
55
|
+
timestamp: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Main Pulsora tracker implementation
|
|
60
|
+
* Handles pageview tracking, custom events, and user identification
|
|
61
|
+
*/
|
|
62
|
+
declare class Tracker implements PulsoraCore {
|
|
63
|
+
private config?;
|
|
64
|
+
private transport?;
|
|
65
|
+
private sessionManager;
|
|
66
|
+
private visitorFingerprint?;
|
|
67
|
+
private fingerprintPromise?;
|
|
68
|
+
private extensions;
|
|
69
|
+
private initialized;
|
|
70
|
+
constructor();
|
|
71
|
+
init(config: PulsoraConfig): void;
|
|
72
|
+
pageview(options?: PageviewOptions): Promise<void>;
|
|
73
|
+
event(eventName: string, eventData?: EventData): Promise<void>;
|
|
74
|
+
identify(customerId: string): Promise<void>;
|
|
75
|
+
reset(): void;
|
|
76
|
+
getVisitorFingerprint(): Promise<string>;
|
|
77
|
+
getSessionId(): string;
|
|
78
|
+
isIdentified(): boolean;
|
|
79
|
+
use(extension: PulsoraExtension): void;
|
|
80
|
+
private isReady;
|
|
81
|
+
private setupSPATracking;
|
|
82
|
+
private setupUnloadHandlers;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { Tracker as Pulsora };
|
|
86
|
+
export type { EventData, PageviewOptions, PulsoraConfig, PulsoraCore, PulsoraExtension, QueuedEvent, TrackingData };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function t(){if("undefined"!=typeof crypto&&crypto.randomUUID)return crypto.randomUUID();let t=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const i=(t+16*Math.random())%16|0;return t=Math.floor(t/16),("x"===e?i:3&i|8).toString(16)})}function e(t){try{const e=new URL(t),i={};return e.searchParams.forEach((t,e)=>{i[e]=t}),{path:e.pathname,search:i}}catch(t){return{path:"/",search:{}}}}function i(t,e){"undefined"!=typeof console&&console.log&&(void 0!==e?console.log(`[Pulsora] ${t}`,e):console.log(`[Pulsora] ${t}`))}async function n(){return async function(t){if("undefined"!=typeof crypto&&crypto.subtle&&crypto.subtle.digest)try{const e=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(t));return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}catch(t){}let e=0;for(let i=0;i<t.length;i++)e=(e<<5)-e+t.charCodeAt(i),e&=e;return Math.abs(e).toString(16).padStart(8,"0")}([navigator.userAgent,navigator.language,(navigator.languages||[]).join(","),screen.width,screen.height,screen.colorDepth,window.devicePixelRatio||1,navigator.hardwareConcurrency||0,navigator.deviceMemory||0,navigator.maxTouchPoints||0,navigator.platform,(new Date).getTimezoneOffset(),s(),await r()].join("~"))}function s(){try{const t=document.createElement("canvas"),e=t.getContext("webgl")||t.getContext("experimental-webgl");if(!e)return"0";const i=e.getExtension("WEBGL_debug_renderer_info");if(!i)return"1";const n=e.getParameter(i.UNMASKED_VENDOR_WEBGL);return n+"~"+e.getParameter(i.UNMASKED_RENDERER_WEBGL)}catch(t){return"2"}}async function r(){try{const t=document.createElement("canvas");t.width=200,t.height=20;const e=t.getContext("2d");if(!e)return"0";e.textBaseline="top",e.font="14px 'PulsoraFont123'",e.textBaseline="alphabetic",e.fillStyle="#f60",e.fillRect(125,1,62,20),e.fillStyle="#069",e.fillText("Cwm fjord 🎨 glyph",2,15);return t.toDataURL().substring(100,200)}catch(t){return"1"}}class o{constructor(){this.customerId=null,this.sessionId=t(),this.startTime=Date.now()}getSessionId(){return this.sessionId}getDuration(){return Math.floor((Date.now()-this.startTime)/1e3)}setCustomerId(t){this.customerId=t}getCustomerId(){return this.customerId}isIdentified(){return null!==this.customerId}reset(){this.sessionId=t(),this.customerId=null,this.startTime=Date.now()}clearIdentification(){this.customerId=null}}class a{constructor(t){this.queue=new Map,this.retryTimeouts=new Map,this.config=t}async send(e){const i={id:t(),timestamp:Date.now(),attempts:0,data:e};await this.sendEvent(i)}async sendEvent(t){t.attempts++;try{const e={type:t.data.type,data:this.prepareEventData(t.data),token:this.config.apiToken};if(navigator.sendBeacon&&"identify"!==t.data.type){const n=new Blob([JSON.stringify(e)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,n))return this.config.debug&&i("Event sent",t.data),void this.removeFromQueue(t.id)}const n=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(e),keepalive:!0});if(n.ok)return this.config.debug&&i("Event sent",t.data),void this.removeFromQueue(t.id);if(429===n.status){const e=parseInt(n.headers.get("Retry-After")||"60",10);return this.config.debug&&i(`Rate limited, retry after ${e}s`),void this.scheduleRetry(t,1e3*e)}throw new Error(`HTTP ${n.status}`)}catch(e){if(this.config.debug&&i("Send failed",{error:e,event:t}),t.attempts<this.config.maxRetries){const e=Math.min(this.config.retryBackoff*Math.pow(2,t.attempts-1),3e4);this.scheduleRetry(t,e)}else this.config.debug&&i(`Dropped after ${t.attempts} attempts`),this.removeFromQueue(t.id)}}prepareEventData(t){const{type:e,timestamp:i,...n}=t,s={};for(const[t,e]of Object.entries(n))void 0!==e&&(s[t]=e);return s}scheduleRetry(t,e){this.queue.set(t.id,t);const i=this.retryTimeouts.get(t.id);i&&clearTimeout(i);const n=setTimeout(()=>{this.retryTimeouts.delete(t.id);const e=this.queue.get(t.id);e&&this.sendEvent(e)},e);this.retryTimeouts.set(t.id,n)}removeFromQueue(t){this.queue.delete(t);const e=this.retryTimeouts.get(t);e&&(clearTimeout(e),this.retryTimeouts.delete(t))}flush(){const t=Array.from(this.queue.values());this.queue.clear();for(const t of this.retryTimeouts.values())clearTimeout(t);this.retryTimeouts.clear();for(const e of t)this.sendEvent(e)}get queueSize(){return this.queue.size}}class d{constructor(){this.extensions=new Map,this.initialized=!1,this.sessionManager=new o}init(t){if(this.initialized)t.debug&&i("Already initialized");else{if("undefined"==typeof window)throw new Error("Browser environment required");this.config={endpoint:"https://api.pulsora.co/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...t},this.transport=new a({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),this.fingerprintPromise=n(),this.fingerprintPromise.then(t=>{var e;this.visitorFingerprint=t,(null===(e=this.config)||void 0===e?void 0:e.debug)&&i("Fingerprint ready",t)}),this.initialized=!0,this.config.autoPageviews&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.pageview()):this.pageview(),this.setupSPATracking()),this.setupUnloadHandlers(),this.config.debug&&i("Initialized",this.config)}}async pageview(t){if(!this.isReady())return;const i=(null==t?void 0:t.url)||location.href,n=(null==t?void 0:t.referrer)||document.referrer,s=(null==t?void 0:t.title)||document.title,r=e(i),o=function(t){if(t)try{const e=new URL(t).hostname.toLowerCase();if(e===location.hostname.toLowerCase())return;return e.replace(/^www\./,"")}catch(t){return}}(n),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),url:i,path:r.path,referrer:n||void 0,referrer_source:o,title:s,utm_source:r.search.utm_source,utm_medium:r.search.utm_medium,utm_campaign:r.search.utm_campaign,utm_term:r.search.utm_term,utm_content:r.search.utm_content,timestamp:Date.now()};this.transport.send(a)}async event(t,i){if(!this.isReady()||!t)return;const n=location.href,s=e(n),r={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),event_name:t,event_data:i,url:n,path:s.path,timestamp:Date.now()};this.transport.send(r)}async identify(t){var e;if(!this.isReady()||!t)return;this.sessionManager.setCustomerId(t);const n={type:"identify",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.sessionManager.getSessionId(),customer_id:t,timestamp:Date.now()};this.transport.send(n),(null===(e=this.config)||void 0===e?void 0:e.debug)&&i("User identified",t)}reset(){var t;this.sessionManager.reset(),(null===(t=this.config)||void 0===t?void 0:t.debug)&&i("Session reset")}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?(this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint):(this.fingerprintPromise=n(),this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint)}getSessionId(){return this.sessionManager.getSessionId()}isIdentified(){return this.sessionManager.isIdentified()}use(t){var e,n;this.extensions.has(t.name)?(null===(e=this.config)||void 0===e?void 0:e.debug)&&i(`Extension ${t.name} already loaded`):(this.extensions.set(t.name,t),t.init(this),(null===(n=this.config)||void 0===n?void 0:n.debug)&&i(`Extension ${t.name} loaded`))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Not initialized"),!1)}setupSPATracking(){const t=history.pushState,e=history.replaceState;history.pushState=(...e)=>{t.apply(history,e),setTimeout(()=>this.pageview(),0)},history.replaceState=(...t)=>{e.apply(history,t),setTimeout(()=>this.pageview(),0)},addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const t=()=>{var t;return null===(t=this.transport)||void 0===t?void 0:t.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&t()}),addEventListener("pagehide",t),addEventListener("beforeunload",t)}}export{d as Pulsora};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/utils.ts","../src/fingerprint.ts","../src/session.ts","../src/transport.ts","../src/tracker.ts"],"sourcesContent":["/**\n * Generate a UUID v4\n */\nexport function generateUUID(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Compact fallback for older browsers\n let d = Date.now();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (d + Math.random() * 16) % 16 | 0;\n d = Math.floor(d / 16);\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n/**\n * SHA-256 hash function with fallback\n */\nexport async function sha256(str: string): Promise<string> {\n if (typeof crypto !== 'undefined' && crypto.subtle && crypto.subtle.digest) {\n try {\n const buf = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(str),\n );\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n } catch {}\n }\n\n // Fallback: simple hash (good enough for fingerprinting)\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash << 5) - hash + str.charCodeAt(i);\n hash = hash & hash;\n }\n return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\n/**\n * Parse URL and extract components\n */\nexport function parseUrl(url: string): {\n path: string;\n search: Record<string, string>;\n} {\n try {\n const u = new URL(url);\n const search: Record<string, string> = {};\n u.searchParams.forEach((v, k) => {\n search[k] = v;\n });\n return { path: u.pathname, search };\n } catch {\n return { path: '/', search: {} };\n }\n}\n\n/**\n * Get referrer source from URL\n * Returns the domain name for ANY external referrer\n */\nexport function getReferrerSource(referrer: string): string | undefined {\n if (!referrer) return;\n\n try {\n const referrerHost = new URL(referrer).hostname.toLowerCase();\n\n // Skip if same domain\n if (referrerHost === location.hostname.toLowerCase()) return;\n\n // Return the clean hostname (remove www. prefix if present)\n return referrerHost.replace(/^www\\./, '');\n } catch {\n return undefined;\n }\n}\n\n/**\n * Simple debounce function\n */\nexport function debounce<T extends (...args: any[]) => void>(\n fn: T,\n delay: number,\n): T {\n let timeout: any;\n return ((...args: any[]) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), delay);\n }) as T;\n}\n\n/**\n * Check if we're in a browser environment\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/**\n * Debug logging\n */\nexport function debugLog(message: string, data?: any): void {\n if (typeof console !== 'undefined' && console.log) {\n if (data !== undefined) {\n console.log(`[Pulsora] ${message}`, data);\n } else {\n console.log(`[Pulsora] ${message}`);\n }\n }\n}\n","import { sha256 } from './utils';\n\n/**\n * Generates a unique, stable browser fingerprint for tracking\n * Uses multiple browser characteristics to create a highly unique identifier\n * GDPR compliant - no PII is collected\n */\nexport async function generateFingerprint(): Promise<string> {\n const components = [\n // User agent and language (high entropy)\n navigator.userAgent,\n navigator.language,\n (navigator.languages || []).join(','),\n\n // Screen (very high entropy, cheap)\n screen.width,\n screen.height,\n screen.colorDepth,\n window.devicePixelRatio || 1,\n\n // Hardware\n navigator.hardwareConcurrency || 0,\n (navigator as any).deviceMemory || 0,\n navigator.maxTouchPoints || 0,\n\n // Browser/OS\n navigator.platform,\n new Date().getTimezoneOffset(),\n\n // WebGL (high entropy)\n getWebGLFingerprint(),\n\n // Canvas (high entropy, optimized)\n await getCanvasFingerprint(),\n ];\n\n return sha256(components.join('~'));\n}\n\n/**\n * WebGL fingerprinting - vendor and renderer info\n */\nfunction getWebGLFingerprint(): string {\n try {\n const canvas = document.createElement('canvas');\n const gl =\n canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) return '0';\n\n const debugInfo = (gl as any).getExtension('WEBGL_debug_renderer_info');\n if (!debugInfo) return '1';\n\n const vendor = (gl as any).getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);\n const renderer = (gl as any).getParameter(\n debugInfo.UNMASKED_RENDERER_WEBGL,\n );\n\n return vendor + '~' + renderer;\n } catch {\n return '2';\n }\n}\n\n/**\n * Canvas fingerprinting - optimized for size\n */\nasync function getCanvasFingerprint(): Promise<string> {\n try {\n const canvas = document.createElement('canvas');\n canvas.width = 200;\n canvas.height = 20;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return '0';\n\n // Use non-existent font to force system fallback (high entropy)\n ctx.textBaseline = 'top';\n ctx.font = \"14px 'PulsoraFont123'\";\n ctx.textBaseline = 'alphabetic';\n ctx.fillStyle = '#f60';\n ctx.fillRect(125, 1, 62, 20);\n ctx.fillStyle = '#069';\n\n // Text with emoji for extra entropy\n ctx.fillText('Cwm fjord 🎨 glyph', 2, 15);\n\n // Extract just a portion of the data URL to save space\n const dataUrl = canvas.toDataURL();\n // Take middle portion for better entropy\n return dataUrl.substring(100, 200);\n } catch {\n return '1';\n }\n}\n","import { generateUUID } from './utils';\n\n/**\n * Session manager for tracking user sessions\n * Handles session ID generation and customer identification\n */\nexport class SessionManager {\n private sessionId: string;\n private customerId: string | null = null;\n private startTime: number;\n\n constructor() {\n this.sessionId = generateUUID();\n this.startTime = Date.now();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n getDuration(): number {\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n setCustomerId(id: string): void {\n this.customerId = id;\n }\n\n getCustomerId(): string | null {\n return this.customerId;\n }\n\n isIdentified(): boolean {\n return this.customerId !== null;\n }\n\n reset(): void {\n this.sessionId = generateUUID();\n this.customerId = null;\n this.startTime = Date.now();\n }\n\n clearIdentification(): void {\n this.customerId = null;\n }\n}\n","import { QueuedEvent, TrackingData } from './types';\nimport { debugLog, generateUUID } from './utils';\n\nexport interface TransportConfig {\n endpoint: string;\n apiToken: string;\n maxRetries: number;\n retryBackoff: number;\n debug: boolean;\n}\n\n/**\n * Transport layer for sending events to the API\n * Handles retries, queuing, and network failures\n */\nexport class Transport {\n private config: TransportConfig;\n private queue = new Map<string, QueuedEvent>();\n private retryTimeouts = new Map<string, any>();\n\n constructor(config: TransportConfig) {\n this.config = config;\n }\n\n async send(data: TrackingData): Promise<void> {\n const event: QueuedEvent = {\n id: generateUUID(),\n timestamp: Date.now(),\n attempts: 0,\n data,\n };\n await this.sendEvent(event);\n }\n\n private async sendEvent(event: QueuedEvent): Promise<void> {\n event.attempts++;\n\n try {\n const payload = {\n type: event.data.type,\n data: this.prepareEventData(event.data),\n token: this.config.apiToken,\n };\n\n // Try sendBeacon first (except for identify)\n if (navigator.sendBeacon && event.data.type !== 'identify') {\n const blob = new Blob([JSON.stringify(payload)], {\n type: 'application/json',\n });\n\n if (navigator.sendBeacon(this.config.endpoint, blob)) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n }\n\n // Fallback to fetch\n const response = await fetch(this.config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Token': this.config.apiToken,\n },\n body: JSON.stringify(payload),\n keepalive: true,\n });\n\n if (response.ok) {\n this.config.debug && debugLog('Event sent', event.data);\n this.removeFromQueue(event.id);\n return;\n }\n\n // Rate limit\n if (response.status === 429) {\n const retryAfter = parseInt(\n response.headers.get('Retry-After') || '60',\n 10,\n );\n this.config.debug &&\n debugLog(`Rate limited, retry after ${retryAfter}s`);\n this.scheduleRetry(event, retryAfter * 1000);\n return;\n }\n\n throw new Error(`HTTP ${response.status}`);\n } catch (error) {\n this.config.debug && debugLog('Send failed', { error, event });\n\n if (event.attempts < this.config.maxRetries) {\n const delay = Math.min(\n this.config.retryBackoff * Math.pow(2, event.attempts - 1),\n 30000,\n );\n this.scheduleRetry(event, delay);\n } else {\n this.config.debug &&\n debugLog(`Dropped after ${event.attempts} attempts`);\n this.removeFromQueue(event.id);\n }\n }\n }\n\n private prepareEventData(data: TrackingData): Record<string, any> {\n const { type, timestamp, ...rest } = data;\n const cleanData: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(rest)) {\n if (value !== undefined) {\n cleanData[key] = value;\n }\n }\n\n return cleanData;\n }\n\n private scheduleRetry(event: QueuedEvent, delay: number): void {\n this.queue.set(event.id, event);\n\n const existingTimeout = this.retryTimeouts.get(event.id);\n if (existingTimeout) {\n clearTimeout(existingTimeout);\n }\n\n const timeout = setTimeout(() => {\n this.retryTimeouts.delete(event.id);\n const queuedEvent = this.queue.get(event.id);\n if (queuedEvent) {\n this.sendEvent(queuedEvent);\n }\n }, delay);\n\n this.retryTimeouts.set(event.id, timeout);\n }\n\n private removeFromQueue(eventId: string): void {\n this.queue.delete(eventId);\n const timeout = this.retryTimeouts.get(eventId);\n if (timeout) {\n clearTimeout(timeout);\n this.retryTimeouts.delete(eventId);\n }\n }\n\n flush(): void {\n const events = Array.from(this.queue.values());\n this.queue.clear();\n\n for (const timeout of this.retryTimeouts.values()) {\n clearTimeout(timeout);\n }\n this.retryTimeouts.clear();\n\n for (const event of events) {\n this.sendEvent(event);\n }\n }\n\n get queueSize(): number {\n return this.queue.size;\n }\n}\n","import { generateFingerprint } from './fingerprint';\nimport { SessionManager } from './session';\nimport { Transport } from './transport';\nimport {\n EventData,\n PageviewOptions,\n PulsoraConfig,\n PulsoraCore,\n PulsoraExtension,\n TrackingData,\n} from './types';\nimport { debugLog, getReferrerSource, parseUrl } from './utils';\n\n/**\n * Main Pulsora tracker implementation\n * Handles pageview tracking, custom events, and user identification\n */\nexport class Tracker implements PulsoraCore {\n private config?: PulsoraConfig;\n private transport?: Transport;\n private sessionManager: SessionManager;\n private visitorFingerprint?: string;\n private fingerprintPromise?: Promise<string>;\n private extensions = new Map<string, PulsoraExtension>();\n private initialized = false;\n\n constructor() {\n this.sessionManager = new SessionManager();\n }\n\n init(config: PulsoraConfig): void {\n if (this.initialized) {\n config.debug && debugLog('Already initialized');\n return;\n }\n\n if (typeof window === 'undefined') {\n throw new Error('Browser environment required');\n }\n\n this.config = {\n endpoint: 'https://api.pulsora.co/ingest',\n autoPageviews: true,\n debug: false,\n maxRetries: 10,\n retryBackoff: 1000,\n ...config,\n };\n\n this.transport = new Transport({\n endpoint: this.config.endpoint!,\n apiToken: this.config.apiToken,\n maxRetries: this.config.maxRetries!,\n retryBackoff: this.config.retryBackoff!,\n debug: this.config.debug!,\n });\n\n // Start fingerprint generation\n this.fingerprintPromise = generateFingerprint();\n this.fingerprintPromise.then((fingerprint) => {\n this.visitorFingerprint = fingerprint;\n this.config?.debug && debugLog('Fingerprint ready', fingerprint);\n });\n\n this.initialized = true;\n\n // Auto pageviews\n if (this.config.autoPageviews) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => this.pageview());\n } else {\n this.pageview();\n }\n this.setupSPATracking();\n }\n\n // Flush on unload\n this.setupUnloadHandlers();\n\n this.config.debug && debugLog('Initialized', this.config);\n }\n\n async pageview(options?: PageviewOptions): Promise<void> {\n if (!this.isReady()) return;\n\n const url = options?.url || location.href;\n const referrer = options?.referrer || document.referrer;\n const title = options?.title || document.title;\n\n const parsed = parseUrl(url);\n const referrerSource = getReferrerSource(referrer);\n\n const data: TrackingData = {\n type: 'pageview',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n url,\n path: parsed.path,\n referrer: referrer || undefined,\n referrer_source: referrerSource,\n title,\n utm_source: parsed.search.utm_source,\n utm_medium: parsed.search.utm_medium,\n utm_campaign: parsed.search.utm_campaign,\n utm_term: parsed.search.utm_term,\n utm_content: parsed.search.utm_content,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async event(eventName: string, eventData?: EventData): Promise<void> {\n if (!this.isReady() || !eventName) return;\n\n const url = location.href;\n const parsed = parseUrl(url);\n\n const data: TrackingData = {\n type: 'event',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n event_name: eventName,\n event_data: eventData,\n url,\n path: parsed.path,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n async identify(customerId: string): Promise<void> {\n if (!this.isReady() || !customerId) return;\n\n this.sessionManager.setCustomerId(customerId);\n\n const data: TrackingData = {\n type: 'identify',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.sessionManager.getSessionId(),\n customer_id: customerId,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n this.config?.debug && debugLog('User identified', customerId);\n }\n\n reset(): void {\n this.sessionManager.reset();\n this.config?.debug && debugLog('Session reset');\n }\n\n async getVisitorFingerprint(): Promise<string> {\n if (this.visitorFingerprint) return this.visitorFingerprint;\n if (this.fingerprintPromise) {\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n this.fingerprintPromise = generateFingerprint();\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n getSessionId(): string {\n return this.sessionManager.getSessionId();\n }\n\n isIdentified(): boolean {\n return this.sessionManager.isIdentified();\n }\n\n use(extension: PulsoraExtension): void {\n if (this.extensions.has(extension.name)) {\n this.config?.debug &&\n debugLog(`Extension ${extension.name} already loaded`);\n return;\n }\n this.extensions.set(extension.name, extension);\n extension.init(this);\n this.config?.debug && debugLog(`Extension ${extension.name} loaded`);\n }\n\n private isReady(): boolean {\n if (!this.initialized) {\n console.warn('[Pulsora] Not initialized');\n return false;\n }\n return true;\n }\n\n private setupSPATracking(): void {\n // Intercept pushState/replaceState\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n history.pushState = (...args) => {\n originalPushState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n history.replaceState = (...args) => {\n originalReplaceState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Back/forward navigation\n addEventListener('popstate', () => {\n setTimeout(() => this.pageview(), 0);\n });\n }\n\n private setupUnloadHandlers(): void {\n const flush = () => this.transport?.flush();\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush();\n });\n\n addEventListener('pagehide', flush);\n addEventListener('beforeunload', flush);\n }\n}\n"],"names":["generateUUID","crypto","randomUUID","d","Date","now","replace","c","r","Math","random","floor","toString","parseUrl","url","u","URL","search","searchParams","forEach","v","k","path","pathname","_a","debugLog","message","data","console","log","undefined","async","generateFingerprint","str","subtle","digest","buf","TextEncoder","encode","Array","from","Uint8Array","map","b","padStart","join","hash","i","length","charCodeAt","abs","sha256","navigator","userAgent","language","languages","screen","width","height","colorDepth","window","devicePixelRatio","hardwareConcurrency","deviceMemory","maxTouchPoints","platform","getTimezoneOffset","getWebGLFingerprint","getCanvasFingerprint","canvas","document","createElement","gl","getContext","debugInfo","getExtension","vendor","getParameter","UNMASKED_VENDOR_WEBGL","UNMASKED_RENDERER_WEBGL","ctx","textBaseline","font","fillStyle","fillRect","fillText","toDataURL","substring","SessionManager","constructor","this","customerId","sessionId","startTime","getSessionId","getDuration","setCustomerId","id","getCustomerId","isIdentified","reset","clearIdentification","Transport","config","queue","Map","retryTimeouts","send","event","timestamp","attempts","sendEvent","payload","type","prepareEventData","token","apiToken","sendBeacon","blob","Blob","JSON","stringify","endpoint","debug","removeFromQueue","response","fetch","method","headers","body","keepalive","ok","status","retryAfter","parseInt","get","scheduleRetry","Error","error","maxRetries","delay","min","retryBackoff","pow","rest","cleanData","key","value","Object","entries","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","eventId","flush","events","values","clear","queueSize","size","Tracker","extensions","initialized","sessionManager","init","autoPageviews","transport","fingerprintPromise","then","fingerprint","visitorFingerprint","readyState","addEventListener","pageview","setupSPATracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","parsed","referrerSource","referrerHost","hostname","toLowerCase","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","utm_source","utm_medium","utm_campaign","utm_term","utm_content","eventName","eventData","event_name","event_data","identify","customer_id","use","extension","has","name","_b","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","visibilityState"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).Pulsora={})}(this,function(t){"use strict";function i(){return"undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const i=16*Math.random()|0;return("x"===t?i:3&i|8).toString(16)})}async function e(t){if("undefined"!=typeof crypto&&crypto.subtle&&crypto.subtle.digest)try{const i=(new TextEncoder).encode(t),e=await crypto.subtle.digest("SHA-256",i);return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}catch(t){}let i=0;for(let e=0;e<t.length;e++){i=(i<<5)-i+t.charCodeAt(e),i&=i}return Math.abs(i).toString(16).padStart(8,"0")}function n(t){try{const i=new URL(t),e=new URLSearchParams(i.search);return{path:i.pathname,utm_source:e.get("utm_source")||void 0,utm_medium:e.get("utm_medium")||void 0,utm_campaign:e.get("utm_campaign")||void 0,utm_term:e.get("utm_term")||void 0,utm_content:e.get("utm_content")||void 0}}catch(t){return{path:"/"}}}function o(t,i){"undefined"!=typeof console&&console.log}async function s(){const t=await async function(){return{userAgent:navigator.userAgent||"",language:navigator.language||"",languages:(navigator.languages||[]).join(","),platform:navigator.platform||"",screenResolution:`${screen.width}x${screen.height}`,screenColorDepth:screen.colorDepth||0,timezoneOffset:(new Date).getTimezoneOffset(),hardwareConcurrency:navigator.hardwareConcurrency||0,deviceMemory:navigator.deviceMemory,maxTouchPoints:navigator.maxTouchPoints||0,canvas:await r(),webgl:a(),audio:u(),fonts:c()}}();return e(Object.values(t).join("|"))}async function r(){try{const t=document.createElement("canvas"),i=t.getContext("2d");if(!i)return"";t.width=280,t.height=60,i.fillStyle="#f60",i.fillRect(125,1,62,20),i.fillStyle="#069",i.font="11pt Arial",i.fillText("Pulsora Analytics 🚀",2,15),i.fillStyle="rgba(102, 204, 0, 0.7)",i.font="18pt Arial",i.fillText("Analytics",4,45),i.globalCompositeOperation="multiply",i.fillStyle="rgb(255,0,255)",i.beginPath(),i.arc(50,50,50,0,2*Math.PI,!0),i.closePath(),i.fill(),i.fillStyle="rgb(0,255,255)",i.beginPath(),i.arc(100,50,50,0,2*Math.PI,!0),i.closePath(),i.fill(),i.fillStyle="rgb(255,255,0)",i.beginPath(),i.arc(75,100,50,0,2*Math.PI,!0),i.closePath(),i.fill();const n=t.toDataURL();return await e(n)}catch(t){return""}}function a(){try{const t=document.createElement("canvas"),i=t.getContext("webgl")||t.getContext("experimental-webgl");if(!i)return"";const e=i.getExtension("WEBGL_debug_renderer_info");if(!e)return"";const n=i.getParameter(e.UNMASKED_VENDOR_WEBGL)||"";return`${n}~${i.getParameter(e.UNMASKED_RENDERER_WEBGL)||""}`}catch(t){return""}}function u(){var t;try{const i=window.AudioContext||window.webkitAudioContext;if(!i)return"";const e=new i,n=e.createOscillator(),o=e.createAnalyser(),s=e.createGain(),r=e.createScriptProcessor(4096,1,1);n.type="triangle",n.frequency.value=1e4,s.gain.value=0,n.connect(o),o.connect(r),r.connect(s),s.connect(e.destination),n.start(0),null===(t=e.startRendering)||void 0===t||t.call(e);const a=[e.sampleRate,e.destination.maxChannelCount,o.frequencyBinCount].join("~");return n.stop(),e.close(),a}catch(t){return""}}function c(){try{const t="mmmmmmmmmmlli",i="72px",e=["monospace","sans-serif","serif"],n=document.createElement("canvas").getContext("2d");if(!n)return"";const o={};for(const s of e)n.font=`${i} ${s}`,o[s]=n.measureText(t).width;const s=["Arial","Verdana","Times New Roman","Courier New","Georgia","Palatino","Garamond","Bookman","Comic Sans MS","Trebuchet MS","Arial Black","Impact","Lucida Sans","Tahoma","Helvetica","Century Gothic","Lucida Console","Futura","Roboto","Ubuntu"],r=[];for(const a of s){let s=!1;for(const r of e){n.font=`${i} '${a}', ${r}`;if(n.measureText(t).width!==o[r]){s=!0;break}}s&&r.push(a)}return r.join(",")}catch(t){return""}}class h{constructor(){this.t=null,this.i=i(),this.o=Date.now()}get sessionId(){return this.i}get duration(){return Math.floor((Date.now()-this.o)/1e3)}setCustomerId(t){this.t=t}get customerId(){return this.t}isIdentified(){return null!==this.t}reset(){this.i=i(),this.t=null,this.o=Date.now()}clearIdentification(){this.t=null}}class d{constructor(t){this.queue=new Map,this.retryTimeouts=new Map,this.config=t}async send(t){const e={id:i(),timestamp:Date.now(),attempts:0,data:t};await this.sendEvent(e)}async sendEvent(t){t.attempts++;try{const i={type:t.data.type,data:this.prepareEventData(t.data),token:this.config.apiToken};if(navigator.sendBeacon&&"identify"!==t.data.type){const e=new Blob([JSON.stringify(i)],{type:"application/json"});if(navigator.sendBeacon(this.config.endpoint,e))return this.config.debug&&o(0,t.data),void this.removeFromQueue(t.id)}const e=await fetch(this.config.endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-API-Token":this.config.apiToken},body:JSON.stringify(i),keepalive:!0});if(e.ok)return this.config.debug&&o(0,t.data),void this.removeFromQueue(t.id);if(429===e.status){const i=parseInt(e.headers.get("Retry-After")||"60",10);return this.config.debug&&o(),void this.scheduleRetry(t,1e3*i)}throw new Error(`HTTP ${e.status}: ${e.statusText}`)}catch(i){if(this.config.debug&&o(),t.attempts<this.config.maxRetries){const i=this.calculateBackoff(t.attempts);this.scheduleRetry(t,i)}else this.config.debug&&o(t.attempts,t.data),this.removeFromQueue(t.id)}}prepareEventData(t){const{type:i,timestamp:e,...n}=t,o={};for(const[t,i]of Object.entries(n))void 0!==i&&(o[t]=i);return o}scheduleRetry(t,i){this.queue.set(t.id,t);const e=this.retryTimeouts.get(t.id);e&&clearTimeout(e);const n=setTimeout(()=>{this.retryTimeouts.delete(t.id);const i=this.queue.get(t.id);i&&this.sendEvent(i)},i);this.retryTimeouts.set(t.id,n)}calculateBackoff(t){const i=this.config.retryBackoff,e=Math.min(i*Math.pow(2,t-1),3e4),n=.3*Math.random()*e;return Math.floor(e+n)}removeFromQueue(t){this.queue.delete(t);const i=this.retryTimeouts.get(t);i&&(clearTimeout(i),this.retryTimeouts.delete(t))}flush(){const t=Array.from(this.queue.values());this.queue.clear();for(const t of this.retryTimeouts.values())clearTimeout(t);this.retryTimeouts.clear();for(const i of t)this.sendEvent(i)}get queueSize(){return this.queue.size}}class l{constructor(){this.config=null,this.transport=null,this.visitorFingerprint=null,this.extensions=new Map,this.initialized=!1,this.fingerprintPromise=null,this.session=new h}init(t){if(this.initialized)t.debug&&o();else{if("undefined"==typeof window||"undefined"==typeof document)throw new Error("Pulsora can only be initialized in a browser environment");this.config={endpoint:"https://api.pulsora.co/ingest",autoPageviews:!0,debug:!1,maxRetries:10,retryBackoff:1e3,...t},this.transport=new d({endpoint:this.config.endpoint,apiToken:this.config.apiToken,maxRetries:this.config.maxRetries,retryBackoff:this.config.retryBackoff,debug:this.config.debug}),this.fingerprintPromise=s(),this.fingerprintPromise.then(t=>{var i;this.visitorFingerprint=t,(null===(i=this.config)||void 0===i?void 0:i.debug)&&o()}),this.initialized=!0,this.config.autoPageviews&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.pageview()):this.pageview(),this.setupHistoryTracking()),this.setupUnloadHandlers(),this.config.debug&&o(0,this.config)}}async pageview(t){if(!this.isReady())return;const i=(null==t?void 0:t.url)||window.location.href,e=(null==t?void 0:t.referrer)||document.referrer,o=(null==t?void 0:t.title)||document.title,s=n(i),r=function(t){if(t)try{const i=new URL(t).hostname.toLowerCase();if(i===window.location.hostname.toLowerCase())return;const e={"google.":"Google","bing.":"Bing","yahoo.":"Yahoo","duckduckgo.":"DuckDuckGo","facebook.":"Facebook","twitter.":"Twitter","x.com":"Twitter","t.co":"Twitter","linkedin.":"LinkedIn","reddit.":"Reddit","youtube.":"YouTube","instagram.":"Instagram","pinterest.":"Pinterest","tiktok.":"TikTok","github.":"GitHub"};for(const[t,n]of Object.entries(e))if(i.includes(t))return n;return i}catch(t){return}}(e),a={type:"pageview",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.session.sessionId,url:i,path:s.path,referrer:e||void 0,referrer_source:r,title:o,utm_source:s.utm_source,utm_medium:s.utm_medium,utm_campaign:s.utm_campaign,utm_term:s.utm_term,utm_content:s.utm_content,timestamp:Date.now()};this.transport.send(a)}async event(t,i){var e;if(!this.isReady())return;if(!t||"string"!=typeof t)return void((null===(e=this.config)||void 0===e?void 0:e.debug)&&o());const s=window.location.href,r=n(s),a={type:"event",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.session.sessionId,event_name:t,event_data:i,url:s,path:r.path,timestamp:Date.now()};this.transport.send(a)}async identify(t){var i,e;if(!this.isReady())return;if(!t||"string"!=typeof t)return void((null===(i=this.config)||void 0===i?void 0:i.debug)&&o());this.session.setCustomerId(t);const n={type:"identify",visitor_fingerprint:await this.getVisitorFingerprint(),session_id:this.session.sessionId,customer_id:t,timestamp:Date.now()};this.transport.send(n),(null===(e=this.config)||void 0===e?void 0:e.debug)&&o()}reset(){var t;this.session.reset(),(null===(t=this.config)||void 0===t?void 0:t.debug)&&o()}async getVisitorFingerprint(){return this.visitorFingerprint?this.visitorFingerprint:this.fingerprintPromise?(this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint):(this.fingerprintPromise=s(),this.visitorFingerprint=await this.fingerprintPromise,this.visitorFingerprint)}getSessionId(){return this.session.sessionId}isIdentified(){return this.session.isIdentified()}use(t){var i,e;this.extensions.has(t.name)?(null===(i=this.config)||void 0===i?void 0:i.debug)&&o(t.name):(this.extensions.set(t.name,t),t.init(this),(null===(e=this.config)||void 0===e?void 0:e.debug)&&o(t.name))}isReady(){return!!this.initialized||(console.warn("[Pulsora] Tracker not initialized. Call init() first."),!1)}setupHistoryTracking(){const t=history.pushState,i=history.replaceState;history.pushState=(...i)=>{t.apply(history,i),setTimeout(()=>this.pageview(),0)},history.replaceState=(...t)=>{i.apply(history,t),setTimeout(()=>this.pageview(),0)},window.addEventListener("popstate",()=>{setTimeout(()=>this.pageview(),0)})}setupUnloadHandlers(){const t=()=>{this.transport&&this.transport.flush()};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&t()}),window.addEventListener("pagehide",t),window.addEventListener("beforeunload",t)}}const f=new l;if("undefined"!=typeof window&&(window.Pulsora=f,document.currentScript)){const t=document.currentScript,i=t.getAttribute("data-token"),e=t.getAttribute("data-endpoint"),n="true"===t.getAttribute("data-debug");i&&f.init({apiToken:i,endpoint:e||void 0,debug:n})}t.Tracker=l,t.default=f,Object.defineProperty(t,"u",{value:!0})});
|
|
2
|
+
//# sourceMappingURL=index.umd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.umd.js","sources":["../src/utils.ts","../src/fingerprint.ts","../src/session.ts","../src/transport.ts","../src/tracker.ts","../src/index.ts"],"sourcesContent":["/**\n * Generate a UUID v4\n * Uses crypto.randomUUID() if available, otherwise falls back to manual generation\n */\nexport function generateUUID(): string {\n // Use native crypto.randomUUID if available\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Fallback to manual UUID v4 generation\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Simple SHA-256 hash function using Web Crypto API\n * Falls back to a simple hash function if Web Crypto is not available\n */\nexport async function sha256(message: string): Promise<string> {\n // Use Web Crypto API if available\n if (typeof crypto !== 'undefined' && crypto.subtle && crypto.subtle.digest) {\n try {\n const msgBuffer = new TextEncoder().encode(message);\n const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n } catch (e) {\n // Fall through to simple hash\n }\n }\n\n // Fallback to simple hash function (not cryptographically secure, but good enough for fingerprinting)\n let hash = 0;\n for (let i = 0; i < message.length; i++) {\n const char = message.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash; // Convert to 32-bit integer\n }\n return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\n/**\n * Parse URL and extract components\n */\nexport function parseUrl(url: string): {\n path: string;\n utm_source?: string;\n utm_medium?: string;\n utm_campaign?: string;\n utm_term?: string;\n utm_content?: string;\n} {\n try {\n const urlObj = new URL(url);\n const params = new URLSearchParams(urlObj.search);\n\n return {\n path: urlObj.pathname,\n utm_source: params.get('utm_source') || undefined,\n utm_medium: params.get('utm_medium') || undefined,\n utm_campaign: params.get('utm_campaign') || undefined,\n utm_term: params.get('utm_term') || undefined,\n utm_content: params.get('utm_content') || undefined,\n };\n } catch {\n return { path: '/' };\n }\n}\n\n/**\n * Extract referrer source from referrer URL\n */\nexport function getReferrerSource(referrer: string): string | undefined {\n if (!referrer) return undefined;\n\n try {\n const url = new URL(referrer);\n const hostname = url.hostname.toLowerCase();\n\n // Check if it's the same domain\n if (hostname === window.location.hostname.toLowerCase()) {\n return undefined;\n }\n\n // Common referrer sources\n const sources: Record<string, string> = {\n 'google.': 'Google',\n 'bing.': 'Bing',\n 'yahoo.': 'Yahoo',\n 'duckduckgo.': 'DuckDuckGo',\n 'facebook.': 'Facebook',\n 'twitter.': 'Twitter',\n 'x.com': 'Twitter',\n 't.co': 'Twitter',\n 'linkedin.': 'LinkedIn',\n 'reddit.': 'Reddit',\n 'youtube.': 'YouTube',\n 'instagram.': 'Instagram',\n 'pinterest.': 'Pinterest',\n 'tiktok.': 'TikTok',\n 'github.': 'GitHub',\n };\n\n for (const [domain, source] of Object.entries(sources)) {\n if (hostname.includes(domain)) {\n return source;\n }\n }\n\n // Return the hostname for unknown sources\n return hostname;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Get root domain for cross-subdomain tracking\n */\nexport function getRootDomain(): string {\n const hostname = window.location.hostname;\n\n // Handle localhost and IP addresses\n if (hostname === 'localhost' || /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(hostname)) {\n return hostname;\n }\n\n // Split hostname into parts\n const parts = hostname.split('.');\n\n // If we have at least 2 parts, return the last 2\n // This handles most common cases like subdomain.example.com -> example.com\n if (parts.length >= 2) {\n return parts.slice(-2).join('.');\n }\n\n return hostname;\n}\n\n/**\n * Debounce function\n */\nexport function debounce<T extends (...args: any[]) => any>(\n func: T,\n wait: number,\n): (...args: Parameters<T>) => void {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n\n return function (this: any, ...args: Parameters<T>) {\n const context = this;\n\n if (timeout !== null) {\n clearTimeout(timeout);\n }\n\n timeout = setTimeout(() => {\n func.apply(context, args);\n timeout = null;\n }, wait);\n };\n}\n\n/**\n * Check if we're in a browser environment\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/**\n * Safe console log for debug mode\n */\nexport function debugLog(message: string, data?: any): void {\n if (typeof console !== 'undefined' && console.log) {\n if (data) {\n console.log(`[Pulsora] ${message}`, data);\n } else {\n console.log(`[Pulsora] ${message}`);\n }\n }\n}\n","import { sha256 } from './utils';\n\ninterface FingerprintComponents {\n userAgent: string;\n language: string;\n languages: string;\n platform: string;\n screenResolution: string;\n screenColorDepth: number;\n timezoneOffset: number;\n hardwareConcurrency: number;\n deviceMemory: number | undefined;\n maxTouchPoints: number;\n canvas: string;\n webgl: string;\n audio: string;\n fonts: string;\n}\n\n/**\n * Generate a stable browser fingerprint\n * This fingerprint is designed to be unique but not personally identifiable\n */\nexport async function generateFingerprint(): Promise<string> {\n const components = await collectComponents();\n const fingerprintString = Object.values(components).join('|');\n return sha256(fingerprintString);\n}\n\nasync function collectComponents(): Promise<FingerprintComponents> {\n return {\n userAgent: navigator.userAgent || '',\n language: navigator.language || '',\n languages: (navigator.languages || []).join(','),\n platform: navigator.platform || '',\n screenResolution: `${screen.width}x${screen.height}`,\n screenColorDepth: screen.colorDepth || 0,\n timezoneOffset: new Date().getTimezoneOffset(),\n hardwareConcurrency: navigator.hardwareConcurrency || 0,\n deviceMemory: (navigator as any).deviceMemory,\n maxTouchPoints: navigator.maxTouchPoints || 0,\n canvas: await getCanvasFingerprint(),\n webgl: getWebGLFingerprint(),\n audio: getAudioFingerprint(),\n fonts: getFontFingerprint(),\n };\n}\n\n/**\n * Canvas fingerprinting\n */\nasync function getCanvasFingerprint(): Promise<string> {\n try {\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return '';\n\n // Set canvas size\n canvas.width = 280;\n canvas.height = 60;\n\n // Draw text with various styles\n ctx.fillStyle = '#f60';\n ctx.fillRect(125, 1, 62, 20);\n\n ctx.fillStyle = '#069';\n ctx.font = '11pt Arial';\n ctx.fillText('Pulsora Analytics 🚀', 2, 15);\n\n ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';\n ctx.font = '18pt Arial';\n ctx.fillText('Analytics', 4, 45);\n\n // Draw some shapes\n ctx.globalCompositeOperation = 'multiply';\n ctx.fillStyle = 'rgb(255,0,255)';\n ctx.beginPath();\n ctx.arc(50, 50, 50, 0, Math.PI * 2, true);\n ctx.closePath();\n ctx.fill();\n\n ctx.fillStyle = 'rgb(0,255,255)';\n ctx.beginPath();\n ctx.arc(100, 50, 50, 0, Math.PI * 2, true);\n ctx.closePath();\n ctx.fill();\n\n ctx.fillStyle = 'rgb(255,255,0)';\n ctx.beginPath();\n ctx.arc(75, 100, 50, 0, Math.PI * 2, true);\n ctx.closePath();\n ctx.fill();\n\n // Get canvas data\n const dataUrl = canvas.toDataURL();\n return await sha256(dataUrl);\n } catch {\n return '';\n }\n}\n\n/**\n * WebGL fingerprinting\n */\nfunction getWebGLFingerprint(): string {\n try {\n const canvas = document.createElement('canvas');\n const gl =\n canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) return '';\n\n const debugInfo = (gl as any).getExtension('WEBGL_debug_renderer_info');\n if (!debugInfo) return '';\n\n const vendor = (gl as any).getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || '';\n const renderer = (gl as any).getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '';\n\n return `${vendor}~${renderer}`;\n } catch {\n return '';\n }\n}\n\n/**\n * Audio fingerprinting\n */\nfunction getAudioFingerprint(): string {\n try {\n const AudioContext =\n (window as any).AudioContext || (window as any).webkitAudioContext;\n if (!AudioContext) return '';\n\n const context = new AudioContext();\n const oscillator = context.createOscillator();\n const analyser = context.createAnalyser();\n const gainNode = context.createGain();\n const scriptProcessor = context.createScriptProcessor(4096, 1, 1);\n\n oscillator.type = 'triangle';\n oscillator.frequency.value = 10000;\n gainNode.gain.value = 0;\n\n oscillator.connect(analyser);\n analyser.connect(scriptProcessor);\n scriptProcessor.connect(gainNode);\n gainNode.connect(context.destination);\n\n oscillator.start(0);\n context.startRendering?.();\n\n // Simplified audio fingerprint based on context properties\n const fingerprint = [\n context.sampleRate,\n context.destination.maxChannelCount,\n analyser.frequencyBinCount,\n ].join('~');\n\n oscillator.stop();\n context.close();\n\n return fingerprint;\n } catch {\n return '';\n }\n}\n\n/**\n * Font fingerprinting\n */\nfunction getFontFingerprint(): string {\n try {\n const testString = 'mmmmmmmmmmlli';\n const testSize = '72px';\n const baseFonts = ['monospace', 'sans-serif', 'serif'];\n\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return '';\n\n const baseFontWidths: Record<string, number> = {};\n\n // Measure base fonts\n for (const baseFont of baseFonts) {\n ctx.font = `${testSize} ${baseFont}`;\n baseFontWidths[baseFont] = ctx.measureText(testString).width;\n }\n\n // Test fonts\n const testFonts = [\n 'Arial',\n 'Verdana',\n 'Times New Roman',\n 'Courier New',\n 'Georgia',\n 'Palatino',\n 'Garamond',\n 'Bookman',\n 'Comic Sans MS',\n 'Trebuchet MS',\n 'Arial Black',\n 'Impact',\n 'Lucida Sans',\n 'Tahoma',\n 'Helvetica',\n 'Century Gothic',\n 'Lucida Console',\n 'Futura',\n 'Roboto',\n 'Ubuntu',\n ];\n\n const detectedFonts: string[] = [];\n\n for (const font of testFonts) {\n let detected = false;\n\n for (const baseFont of baseFonts) {\n ctx.font = `${testSize} '${font}', ${baseFont}`;\n const width = ctx.measureText(testString).width;\n\n if (width !== baseFontWidths[baseFont]) {\n detected = true;\n break;\n }\n }\n\n if (detected) {\n detectedFonts.push(font);\n }\n }\n\n return detectedFonts.join(',');\n } catch {\n return '';\n }\n}\n","import { generateUUID } from './utils';\n\n/**\n * Session management\n * Sessions are in-memory only and last for the duration of the page\n */\nexport class SessionManager {\n private _sessionId: string;\n private _customerId: string | null = null;\n private _startTime: number;\n\n constructor() {\n this._sessionId = generateUUID();\n this._startTime = Date.now();\n }\n\n /**\n * Get the current session ID\n */\n get sessionId(): string {\n return this._sessionId;\n }\n\n /**\n * Get the session duration in seconds\n */\n get duration(): number {\n return Math.floor((Date.now() - this._startTime) / 1000);\n }\n\n /**\n * Set the customer ID for this session\n */\n setCustomerId(customerId: string): void {\n this._customerId = customerId;\n }\n\n /**\n * Get the customer ID if identified\n */\n get customerId(): string | null {\n return this._customerId;\n }\n\n /**\n * Check if the user is identified\n */\n isIdentified(): boolean {\n return this._customerId !== null;\n }\n\n /**\n * Reset the session (creates a new session)\n */\n reset(): void {\n this._sessionId = generateUUID();\n this._customerId = null;\n this._startTime = Date.now();\n }\n\n /**\n * Clear customer identification\n */\n clearIdentification(): void {\n this._customerId = null;\n }\n}\n","import { QueuedEvent, TrackingData } from './types';\nimport { debugLog, generateUUID } from './utils';\n\nexport interface TransportConfig {\n endpoint: string;\n apiToken: string;\n maxRetries: number;\n retryBackoff: number;\n debug: boolean;\n}\n\n/**\n * Transport layer for sending events to the API\n * Handles retries, queuing, and network failures\n */\nexport class Transport {\n private config: TransportConfig;\n private queue: Map<string, QueuedEvent> = new Map();\n private retryTimeouts: Map<string, ReturnType<typeof setTimeout>> = new Map();\n\n constructor(config: TransportConfig) {\n this.config = config;\n }\n\n /**\n * Send tracking data to the API\n */\n async send(data: TrackingData): Promise<void> {\n const eventId = generateUUID();\n const event: QueuedEvent = {\n id: eventId,\n timestamp: Date.now(),\n attempts: 0,\n data,\n };\n\n await this.sendEvent(event);\n }\n\n /**\n * Send a queued event with retry logic\n */\n private async sendEvent(event: QueuedEvent): Promise<void> {\n event.attempts++;\n\n try {\n // Prepare the payload\n const payload = {\n type: event.data.type,\n data: this.prepareEventData(event.data),\n token: this.config.apiToken,\n };\n\n // Try sendBeacon first for better reliability\n if (navigator.sendBeacon && event.data.type !== 'identify') {\n const blob = new Blob([JSON.stringify(payload)], {\n type: 'application/json',\n });\n\n const success = navigator.sendBeacon(this.config.endpoint, blob);\n\n if (success) {\n if (this.config.debug) {\n debugLog(`Event sent via sendBeacon`, event.data);\n }\n this.removeFromQueue(event.id);\n return;\n }\n }\n\n // Fallback to fetch\n const response = await fetch(this.config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Token': this.config.apiToken,\n },\n body: JSON.stringify(payload),\n keepalive: true, // Allow request to outlive the page\n });\n\n if (response.ok) {\n if (this.config.debug) {\n debugLog(`Event sent successfully`, event.data);\n }\n this.removeFromQueue(event.id);\n return;\n }\n\n // Handle rate limiting\n if (response.status === 429) {\n const retryAfter = parseInt(\n response.headers.get('Retry-After') || '60',\n 10,\n );\n if (this.config.debug) {\n debugLog(`Rate limited, retrying after ${retryAfter}s`);\n }\n this.scheduleRetry(event, retryAfter * 1000);\n return;\n }\n\n // Handle other errors\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n } catch (error) {\n if (this.config.debug) {\n debugLog(`Failed to send event`, { error, event });\n }\n\n // Check if we should retry\n if (event.attempts < this.config.maxRetries) {\n const backoff = this.calculateBackoff(event.attempts);\n this.scheduleRetry(event, backoff);\n } else {\n if (this.config.debug) {\n debugLog(\n `Event dropped after ${event.attempts} attempts`,\n event.data,\n );\n }\n this.removeFromQueue(event.id);\n }\n }\n }\n\n /**\n * Prepare event data for sending\n */\n private prepareEventData(data: TrackingData): Record<string, any> {\n const { type, timestamp, ...eventData } = data;\n\n // Remove undefined values\n const cleanData: Record<string, any> = {};\n for (const [key, value] of Object.entries(eventData)) {\n if (value !== undefined) {\n cleanData[key] = value;\n }\n }\n\n return cleanData;\n }\n\n /**\n * Schedule a retry for a failed event\n */\n private scheduleRetry(event: QueuedEvent, delayMs: number): void {\n // Add to queue\n this.queue.set(event.id, event);\n\n // Clear any existing retry timeout\n const existingTimeout = this.retryTimeouts.get(event.id);\n if (existingTimeout) {\n clearTimeout(existingTimeout);\n }\n\n // Schedule retry\n const timeout = setTimeout(() => {\n this.retryTimeouts.delete(event.id);\n const queuedEvent = this.queue.get(event.id);\n if (queuedEvent) {\n this.sendEvent(queuedEvent);\n }\n }, delayMs);\n\n this.retryTimeouts.set(event.id, timeout);\n }\n\n /**\n * Calculate exponential backoff delay\n */\n private calculateBackoff(attempts: number): number {\n const baseDelay = this.config.retryBackoff;\n const maxDelay = 30000; // 30 seconds max\n const delay = Math.min(baseDelay * Math.pow(2, attempts - 1), maxDelay);\n\n // Add some jitter to prevent thundering herd\n const jitter = Math.random() * 0.3 * delay;\n return Math.floor(delay + jitter);\n }\n\n /**\n * Remove event from queue and cancel any pending retries\n */\n private removeFromQueue(eventId: string): void {\n this.queue.delete(eventId);\n\n const timeout = this.retryTimeouts.get(eventId);\n if (timeout) {\n clearTimeout(timeout);\n this.retryTimeouts.delete(eventId);\n }\n }\n\n /**\n * Flush all queued events (best effort)\n */\n flush(): void {\n const events = Array.from(this.queue.values());\n this.queue.clear();\n\n // Clear all retry timeouts\n for (const timeout of this.retryTimeouts.values()) {\n clearTimeout(timeout);\n }\n this.retryTimeouts.clear();\n\n // Try to send all queued events\n for (const event of events) {\n this.sendEvent(event);\n }\n }\n\n /**\n * Get the number of queued events\n */\n get queueSize(): number {\n return this.queue.size;\n }\n}\n","import { generateFingerprint } from './fingerprint';\nimport { SessionManager } from './session';\nimport { Transport } from './transport';\nimport {\n EventData,\n PageviewOptions,\n PulsoraConfig,\n PulsoraCore,\n PulsoraExtension,\n TrackingData,\n} from './types';\nimport { debugLog, getReferrerSource, isBrowser, parseUrl } from './utils';\n\n/**\n * Main Pulsora tracker implementation\n */\nexport class Tracker implements PulsoraCore {\n private config: PulsoraConfig | null = null;\n private transport: Transport | null = null;\n private session: SessionManager;\n private visitorFingerprint: string | null = null;\n private extensions: Map<string, PulsoraExtension> = new Map();\n private initialized = false;\n private fingerprintPromise: Promise<string> | null = null;\n\n constructor() {\n this.session = new SessionManager();\n }\n\n /**\n * Initialize the tracker\n */\n init(config: PulsoraConfig): void {\n if (this.initialized) {\n if (config.debug) {\n debugLog('Pulsora already initialized');\n }\n return;\n }\n\n if (!isBrowser()) {\n throw new Error(\n 'Pulsora can only be initialized in a browser environment',\n );\n }\n\n this.config = {\n endpoint: 'https://api.pulsora.co/ingest',\n autoPageviews: true,\n debug: false,\n maxRetries: 10,\n retryBackoff: 1000,\n ...config,\n };\n\n this.transport = new Transport({\n endpoint: this.config.endpoint!,\n apiToken: this.config.apiToken,\n maxRetries: this.config.maxRetries!,\n retryBackoff: this.config.retryBackoff!,\n debug: this.config.debug!,\n });\n\n // Start fingerprint generation\n this.fingerprintPromise = generateFingerprint();\n this.fingerprintPromise.then((fp) => {\n this.visitorFingerprint = fp;\n if (this.config?.debug) {\n debugLog('Visitor fingerprint generated', fp);\n }\n });\n\n this.initialized = true;\n\n // Track initial pageview if enabled\n if (this.config.autoPageviews) {\n // Wait for DOM to be ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => this.pageview());\n } else {\n this.pageview();\n }\n\n // Track pageviews on history changes (for SPAs)\n this.setupHistoryTracking();\n }\n\n // Flush events on page unload\n this.setupUnloadHandlers();\n\n if (this.config.debug) {\n debugLog('Pulsora initialized', this.config);\n }\n }\n\n /**\n * Track a pageview\n */\n async pageview(options?: PageviewOptions): Promise<void> {\n if (!this.isReady()) return;\n\n const url = options?.url || window.location.href;\n const referrer = options?.referrer || document.referrer;\n const title = options?.title || document.title;\n\n const urlData = parseUrl(url);\n const referrerSource = getReferrerSource(referrer);\n\n const data: TrackingData = {\n type: 'pageview',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.session.sessionId,\n url,\n path: urlData.path,\n referrer: referrer || undefined,\n referrer_source: referrerSource,\n title,\n utm_source: urlData.utm_source,\n utm_medium: urlData.utm_medium,\n utm_campaign: urlData.utm_campaign,\n utm_term: urlData.utm_term,\n utm_content: urlData.utm_content,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n /**\n * Track a custom event\n */\n async event(eventName: string, eventData?: EventData): Promise<void> {\n if (!this.isReady()) return;\n\n if (!eventName || typeof eventName !== 'string') {\n if (this.config?.debug) {\n debugLog('Invalid event name', eventName);\n }\n return;\n }\n\n const url = window.location.href;\n const urlData = parseUrl(url);\n\n const data: TrackingData = {\n type: 'event',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.session.sessionId,\n event_name: eventName,\n event_data: eventData,\n url,\n path: urlData.path,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n }\n\n /**\n * Identify a user\n */\n async identify(customerId: string): Promise<void> {\n if (!this.isReady()) return;\n\n if (!customerId || typeof customerId !== 'string') {\n if (this.config?.debug) {\n debugLog('Invalid customer ID', customerId);\n }\n return;\n }\n\n // Set customer ID in session\n this.session.setCustomerId(customerId);\n\n // Send identify event\n const data: TrackingData = {\n type: 'identify',\n visitor_fingerprint: await this.getVisitorFingerprint(),\n session_id: this.session.sessionId,\n customer_id: customerId,\n timestamp: Date.now(),\n };\n\n this.transport!.send(data);\n\n if (this.config?.debug) {\n debugLog('User identified', customerId);\n }\n }\n\n /**\n * Reset user identification\n */\n reset(): void {\n this.session.reset();\n if (this.config?.debug) {\n debugLog('Session reset');\n }\n }\n\n /**\n * Get the visitor fingerprint\n */\n async getVisitorFingerprint(): Promise<string> {\n if (this.visitorFingerprint) {\n return this.visitorFingerprint;\n }\n\n if (this.fingerprintPromise) {\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n // Generate fingerprint if not already in progress\n this.fingerprintPromise = generateFingerprint();\n this.visitorFingerprint = await this.fingerprintPromise;\n return this.visitorFingerprint;\n }\n\n /**\n * Get the current session ID\n */\n getSessionId(): string {\n return this.session.sessionId;\n }\n\n /**\n * Check if user is identified\n */\n isIdentified(): boolean {\n return this.session.isIdentified();\n }\n\n /**\n * Register an extension\n */\n use(extension: PulsoraExtension): void {\n if (this.extensions.has(extension.name)) {\n if (this.config?.debug) {\n debugLog(`Extension ${extension.name} already registered`);\n }\n return;\n }\n\n this.extensions.set(extension.name, extension);\n extension.init(this);\n\n if (this.config?.debug) {\n debugLog(`Extension ${extension.name} registered`);\n }\n }\n\n /**\n * Check if tracker is ready\n */\n private isReady(): boolean {\n if (!this.initialized) {\n console.warn('[Pulsora] Tracker not initialized. Call init() first.');\n return false;\n }\n return true;\n }\n\n /**\n * Setup history tracking for SPAs\n */\n private setupHistoryTracking(): void {\n // Store original pushState and replaceState\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n // Override pushState\n history.pushState = (...args) => {\n originalPushState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Override replaceState\n history.replaceState = (...args) => {\n originalReplaceState.apply(history, args);\n setTimeout(() => this.pageview(), 0);\n };\n\n // Listen for popstate events (back/forward buttons)\n window.addEventListener('popstate', () => {\n setTimeout(() => this.pageview(), 0);\n });\n }\n\n /**\n * Setup unload handlers to flush events\n */\n private setupUnloadHandlers(): void {\n // Try to flush events on page unload\n const flushEvents = () => {\n if (this.transport) {\n this.transport.flush();\n }\n };\n\n // Use visibilitychange as it's more reliable than unload\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') {\n flushEvents();\n }\n });\n\n // Also use pagehide for better mobile support\n window.addEventListener('pagehide', flushEvents);\n\n // Fallback to beforeunload\n window.addEventListener('beforeunload', flushEvents);\n }\n}\n","import { Tracker } from './tracker';\n\n// Create singleton instance\nconst tracker = new Tracker();\n\n// Export the instance as default\nexport default tracker;\n\n// Export types\nexport * from './types';\n\n// Export the tracker class for advanced usage\nexport { Tracker };\n\n// For UMD builds, attach to window\nif (typeof window !== 'undefined') {\n (window as any).Pulsora = tracker;\n\n // Auto-initialize if data-token is present\n if (document.currentScript) {\n const script = document.currentScript as HTMLScriptElement;\n const token = script.getAttribute('data-token');\n const endpoint = script.getAttribute('data-endpoint');\n const debug = script.getAttribute('data-debug') === 'true';\n\n if (token) {\n tracker.init({\n apiToken: token,\n endpoint: endpoint || undefined,\n debug,\n });\n }\n }\n}\n"],"names":["generateUUID","crypto","randomUUID","replace","c","r","Math","random","toString","async","sha256","message","subtle","digest","msgBuffer","TextEncoder","encode","hashBuffer","Array","from","Uint8Array","map","b","padStart","join","e","hash","i","length","charCodeAt","abs","parseUrl","url","urlObj","URL","params","URLSearchParams","search","path","pathname","utm_source","get","undefined","utm_medium","utm_campaign","utm_term","utm_content","_a","debugLog","data","console","log","generateFingerprint","components","userAgent","navigator","language","languages","platform","screenResolution","screen","width","height","screenColorDepth","colorDepth","timezoneOffset","Date","getTimezoneOffset","hardwareConcurrency","deviceMemory","maxTouchPoints","canvas","getCanvasFingerprint","webgl","getWebGLFingerprint","audio","getAudioFingerprint","fonts","getFontFingerprint","collectComponents","Object","values","document","createElement","ctx","getContext","fillStyle","fillRect","font","fillText","globalCompositeOperation","beginPath","arc","PI","closePath","fill","dataUrl","toDataURL","gl","debugInfo","getExtension","vendor","getParameter","UNMASKED_VENDOR_WEBGL","UNMASKED_RENDERER_WEBGL","AudioContext","window","webkitAudioContext","context","oscillator","createOscillator","analyser","createAnalyser","gainNode","createGain","scriptProcessor","createScriptProcessor","type","frequency","value","gain","connect","destination","start","startRendering","call","fingerprint","sampleRate","maxChannelCount","frequencyBinCount","stop","close","_b","testString","testSize","baseFonts","baseFontWidths","baseFont","measureText","testFonts","detectedFonts","detected","push","SessionManager","constructor","this","_customerId","_sessionId","_startTime","now","sessionId","duration","floor","setCustomerId","customerId","isIdentified","reset","clearIdentification","Transport","config","queue","Map","retryTimeouts","send","event","id","timestamp","attempts","sendEvent","payload","prepareEventData","token","apiToken","sendBeacon","blob","Blob","JSON","stringify","endpoint","debug","removeFromQueue","response","fetch","method","headers","body","keepalive","ok","status","retryAfter","parseInt","scheduleRetry","Error","statusText","error","maxRetries","backoff","calculateBackoff","eventData","cleanData","key","entries","delayMs","set","existingTimeout","clearTimeout","timeout","setTimeout","delete","queuedEvent","baseDelay","retryBackoff","delay","min","pow","jitter","eventId","flush","events","clear","queueSize","size","Tracker","transport","visitorFingerprint","extensions","initialized","fingerprintPromise","session","init","autoPageviews","then","fp","readyState","addEventListener","pageview","setupHistoryTracking","setupUnloadHandlers","options","isReady","location","href","referrer","title","urlData","referrerSource","hostname","toLowerCase","sources","domain","source","includes","getReferrerSource","visitor_fingerprint","getVisitorFingerprint","session_id","referrer_source","eventName","event_name","event_data","identify","customer_id","getSessionId","use","extension","has","name","warn","originalPushState","history","pushState","originalReplaceState","replaceState","args","apply","flushEvents","visibilityState","tracker","Pulsora","currentScript","script","getAttribute"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pulsora/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Privacy-first analytics tracking library - Core package",
|
|
5
|
+
"author": "Pulsora.co",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"unpkg": "./dist/browser.js",
|
|
12
|
+
"cratebox": "./dist/browser.js",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.cjs",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./browser": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/browser.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"scripts": {
|
|
30
|
+
"dev": "rollup -c -w",
|
|
31
|
+
"build": "rollup -c",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"test:coverage": "vitest run --coverage",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"lint": "eslint src --ext .ts",
|
|
37
|
+
"size": "size-limit",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@rollup/plugin-commonjs": "^25.0.7",
|
|
42
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
43
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
44
|
+
"@rollup/plugin-typescript": "^11.1.5",
|
|
45
|
+
"@size-limit/preset-small-lib": "^11.0.1",
|
|
46
|
+
"@types/node": "^20.10.5",
|
|
47
|
+
"@vitest/coverage-v8": "^1.1.0",
|
|
48
|
+
"rollup": "^4.9.1",
|
|
49
|
+
"rollup-plugin-dts": "^6.1.0",
|
|
50
|
+
"rollup-plugin-visualizer": "^6.0.5",
|
|
51
|
+
"size-limit": "^11.0.1",
|
|
52
|
+
"tslib": "^2.6.2",
|
|
53
|
+
"typescript": "^5.3.3",
|
|
54
|
+
"vitest": "^1.1.0"
|
|
55
|
+
},
|
|
56
|
+
"size-limit": [
|
|
57
|
+
{
|
|
58
|
+
"path": "dist/index.js",
|
|
59
|
+
"limit": "1 KB"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"keywords": [
|
|
63
|
+
"analytics",
|
|
64
|
+
"tracking",
|
|
65
|
+
"revenue",
|
|
66
|
+
"attribution",
|
|
67
|
+
"funnels",
|
|
68
|
+
"journeys",
|
|
69
|
+
"segments",
|
|
70
|
+
"lifecycle",
|
|
71
|
+
"customer",
|
|
72
|
+
"lifetime",
|
|
73
|
+
"value",
|
|
74
|
+
"segmentation"
|
|
75
|
+
],
|
|
76
|
+
"repository": {
|
|
77
|
+
"type": "git",
|
|
78
|
+
"url": "https://github.com/pulsora/javascript-sdk"
|
|
79
|
+
},
|
|
80
|
+
"bugs": {
|
|
81
|
+
"url": "https://github.com/pulsora/javascript-sdk/issues"
|
|
82
|
+
},
|
|
83
|
+
"homepage": "https://pulsora.co"
|
|
84
|
+
}
|