@linkzly/web-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +163 -0
- package/dist/index.d.ts +163 -0
- package/dist/index.js +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# @linkzly/web-sdk
|
|
2
|
+
|
|
3
|
+
[](https://developer.mozilla.org)
|
|
4
|
+
[](https://www.typescriptlang.org)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Full-featured browser SDK for Linkzly — event tracking, attribution, session management, and deep linking for web applications. Zero dependencies, < 6KB gzipped.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Event Tracking** — Custom events, page views, purchases with automatic batching
|
|
12
|
+
- **Session Management** — Automatic visibility-based sessions (30s timeout, matching mobile SDKs)
|
|
13
|
+
- **Attribution Tracking** — UTM parameters, referrer capture, smart link handling
|
|
14
|
+
- **Deep Link / Smart Link** — Parse and handle Linkzly smart links with SPA support
|
|
15
|
+
- **User Identification** — Persistent visitor ID + custom user ID
|
|
16
|
+
- **Privacy Controls** — DNT/GPC respect, opt-out controls, GDPR-friendly
|
|
17
|
+
- **Offline Queue** — localStorage-backed event queue with retry and sendBeacon on unload
|
|
18
|
+
- **Scroll Depth** — Automatic max scroll depth tracking per page
|
|
19
|
+
- **Outbound Click Tracking** — Automatic external link click tracking
|
|
20
|
+
- **Time on Page** — Automatic session duration tracking
|
|
21
|
+
- **SPA Support** — History API hooks for pushState/popstate navigation
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Any modern browser (Chrome 66+, Firefox 60+, Safari 12+, Edge 79+)
|
|
26
|
+
- No dependencies
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
### npm / yarn
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @linkzly/web-sdk
|
|
34
|
+
# or
|
|
35
|
+
yarn add @linkzly/web-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### CDN (UMD)
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<script src="https://cdn.linkzly.com/web-sdk/latest/index.js"></script>
|
|
42
|
+
<script>
|
|
43
|
+
LinkzlySDK.configure('YOUR_SDK_KEY');
|
|
44
|
+
</script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import LinkzlySDK from '@linkzly/web-sdk';
|
|
51
|
+
|
|
52
|
+
// Initialize
|
|
53
|
+
LinkzlySDK.configure('YOUR_SDK_KEY');
|
|
54
|
+
|
|
55
|
+
// Track events
|
|
56
|
+
LinkzlySDK.trackEvent('button_click', { button_id: 'signup' });
|
|
57
|
+
|
|
58
|
+
// Track purchases
|
|
59
|
+
LinkzlySDK.trackPurchase({
|
|
60
|
+
order_id: 'ORD-123',
|
|
61
|
+
amount: 99.99,
|
|
62
|
+
currency: 'USD',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Set user ID after login
|
|
66
|
+
LinkzlySDK.setUserID('user_12345');
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Named Imports (Tree-Shakeable)
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { configure, trackEvent, trackPageView, setUserID } from '@linkzly/web-sdk';
|
|
73
|
+
|
|
74
|
+
configure('YOUR_SDK_KEY');
|
|
75
|
+
trackEvent('signup_completed', { method: 'google' });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## API Reference
|
|
79
|
+
|
|
80
|
+
### Configuration
|
|
81
|
+
|
|
82
|
+
#### `configure(sdkKey, environment?, options?)`
|
|
83
|
+
|
|
84
|
+
Initialize the SDK. Must be called before any other method.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import LinkzlySDK, { Environment } from '@linkzly/web-sdk';
|
|
88
|
+
|
|
89
|
+
LinkzlySDK.configure('YOUR_SDK_KEY', Environment.PRODUCTION, {
|
|
90
|
+
autoTrackPageViews: false, // opt-in SPA page view tracking (default: false)
|
|
91
|
+
autoTrackSessions: true, // visibility-based sessions (default: true)
|
|
92
|
+
autoExtractUTM: true, // auto-capture UTM params (default: true)
|
|
93
|
+
autoCaptureReferrer: true, // auto-capture document.referrer (default: true)
|
|
94
|
+
respectDNT: true, // honor Do Not Track / GPC (default: true)
|
|
95
|
+
batchSize: 20, // events per batch (default: 20)
|
|
96
|
+
flushIntervalMs: 30000, // flush timer (default: 30s)
|
|
97
|
+
sessionTimeoutMs: 30000, // session timeout (default: 30s)
|
|
98
|
+
debug: false, // console logging (default: false, auto-true in DEVELOPMENT)
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Environments:**
|
|
103
|
+
- `Environment.PRODUCTION` — Production endpoint (default)
|
|
104
|
+
- `Environment.STAGING` — Staging endpoint
|
|
105
|
+
- `Environment.DEVELOPMENT` — Development endpoint with verbose logging
|
|
106
|
+
|
|
107
|
+
### Event Tracking
|
|
108
|
+
|
|
109
|
+
#### `trackEvent(eventName, parameters?)`
|
|
110
|
+
|
|
111
|
+
Track a custom event.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
LinkzlySDK.trackEvent('add_to_cart', {
|
|
115
|
+
product_id: 'SKU-123',
|
|
116
|
+
quantity: 2,
|
|
117
|
+
price: 49.99,
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `trackPageView(pageUrl?, parameters?)`
|
|
122
|
+
|
|
123
|
+
Track a page view. Called automatically if `autoTrackPageViews: true`.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
LinkzlySDK.trackPageView(); // current page
|
|
127
|
+
LinkzlySDK.trackPageView('/products/123', { category: 'electronics' });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### `trackPurchase(parameters?)`
|
|
131
|
+
|
|
132
|
+
Track a purchase event.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
LinkzlySDK.trackPurchase({
|
|
136
|
+
order_id: 'ORD-98765',
|
|
137
|
+
total: 599.99,
|
|
138
|
+
currency: 'USD',
|
|
139
|
+
items: 3,
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### `trackEventBatch(events)`
|
|
144
|
+
|
|
145
|
+
Track multiple events in one call.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
LinkzlySDK.trackEventBatch([
|
|
149
|
+
{ eventName: 'screen_view', parameters: { screen: 'home' } },
|
|
150
|
+
{ eventName: 'button_click', parameters: { button: 'signup' } },
|
|
151
|
+
]);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### `flushEvents()`
|
|
155
|
+
|
|
156
|
+
Manually flush all queued events to the server.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
await LinkzlySDK.flushEvents();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `getPendingEventCount()`
|
|
163
|
+
|
|
164
|
+
Get the number of events waiting in the queue.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const count = LinkzlySDK.getPendingEventCount();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Deep Link / Smart Link
|
|
171
|
+
|
|
172
|
+
#### `handleSmartLink(url?)`
|
|
173
|
+
|
|
174
|
+
Parse a URL into deep link data (matching mobile SDK pattern).
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const data = LinkzlySDK.handleSmartLink();
|
|
178
|
+
// { url, path, parameters, smartLinkId, clickId }
|
|
179
|
+
|
|
180
|
+
const data = LinkzlySDK.handleSmartLink('https://example.com/product?id=123&slid=abc');
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### `addDeepLinkListener(listener)`
|
|
184
|
+
|
|
185
|
+
Listen for SPA navigation events (requires `autoTrackPageViews: true`).
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const unsubscribe = LinkzlySDK.addDeepLinkListener((data) => {
|
|
189
|
+
console.log('Navigation:', data.path, data.parameters);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Cleanup
|
|
193
|
+
unsubscribe();
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### User Management
|
|
197
|
+
|
|
198
|
+
#### `setUserID(userID)` / `getUserID()` / `clearUserID()`
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
LinkzlySDK.setUserID('user_12345');
|
|
202
|
+
const id = LinkzlySDK.getUserID(); // 'user_12345'
|
|
203
|
+
LinkzlySDK.clearUserID();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `getVisitorID()` / `resetVisitorID()`
|
|
207
|
+
|
|
208
|
+
Persistent device/browser identifier (survives across sessions).
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const visitorId = LinkzlySDK.getVisitorID(); // UUID persisted in localStorage
|
|
212
|
+
const newId = LinkzlySDK.resetVisitorID(); // generates new UUID
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Session Management
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const sessionId = LinkzlySDK.getSessionId();
|
|
219
|
+
LinkzlySDK.startSession(); // force new session
|
|
220
|
+
LinkzlySDK.endSession(); // end current session
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Sessions auto-manage via the Page Visibility API — a new session starts when the page becomes visible after 30+ seconds of being hidden.
|
|
224
|
+
|
|
225
|
+
### Privacy Controls
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
LinkzlySDK.setTrackingEnabled(false); // disable all tracking
|
|
229
|
+
const enabled = LinkzlySDK.isTrackingEnabled();
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The SDK respects `navigator.doNotTrack` and `navigator.globalPrivacyControl` by default. Set `respectDNT: false` in config to override.
|
|
233
|
+
|
|
234
|
+
### Cleanup
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
LinkzlySDK.destroy(); // remove all listeners, flush events, stop timers
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Framework Examples
|
|
241
|
+
|
|
242
|
+
### React / Next.js
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { useEffect } from 'react';
|
|
246
|
+
import LinkzlySDK, { Environment } from '@linkzly/web-sdk';
|
|
247
|
+
|
|
248
|
+
export default function App({ children }) {
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
LinkzlySDK.configure('YOUR_SDK_KEY', Environment.PRODUCTION, {
|
|
251
|
+
autoTrackPageViews: true, // enable for Next.js SPA navigation
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return () => LinkzlySDK.destroy();
|
|
255
|
+
}, []);
|
|
256
|
+
|
|
257
|
+
return <>{children}</>;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Vue 3
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// main.ts
|
|
265
|
+
import { createApp } from 'vue';
|
|
266
|
+
import LinkzlySDK from '@linkzly/web-sdk';
|
|
267
|
+
import App from './App.vue';
|
|
268
|
+
|
|
269
|
+
LinkzlySDK.configure('YOUR_SDK_KEY');
|
|
270
|
+
createApp(App).mount('#app');
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Vanilla JS
|
|
274
|
+
|
|
275
|
+
```html
|
|
276
|
+
<script type="module">
|
|
277
|
+
import LinkzlySDK from '@linkzly/web-sdk';
|
|
278
|
+
|
|
279
|
+
LinkzlySDK.configure('YOUR_SDK_KEY');
|
|
280
|
+
|
|
281
|
+
document.getElementById('buy-btn').addEventListener('click', () => {
|
|
282
|
+
LinkzlySDK.trackEvent('button_click', { button: 'buy' });
|
|
283
|
+
});
|
|
284
|
+
</script>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Auto-Tracked Web Features
|
|
288
|
+
|
|
289
|
+
These features use the existing event payload structure (`customData` field) — no database changes required.
|
|
290
|
+
|
|
291
|
+
| Feature | Event Name | Data Collected |
|
|
292
|
+
|---------|-----------|----------------|
|
|
293
|
+
| **Scroll Depth** | `scroll_depth` | `max_depth_percent`, `page_url` |
|
|
294
|
+
| **Time on Page** | `time_on_page` | `duration_seconds`, `page_url` |
|
|
295
|
+
| **Outbound Clicks** | `outbound_click` | `destination_url`, `link_text`, `page_url` |
|
|
296
|
+
|
|
297
|
+
Scroll depth and time on page are sent automatically on page unload via `sendBeacon`. Outbound clicks are tracked in real-time.
|
|
298
|
+
|
|
299
|
+
## Event Delivery
|
|
300
|
+
|
|
301
|
+
- **Batching**: Up to 20 events per batch (configurable), max 100 per network request
|
|
302
|
+
- **Queue**: Events persisted in localStorage (up to 500, 24h expiry)
|
|
303
|
+
- **Flush triggers**: Batch size reached, 30s timer, `flushEvents()`, page unload
|
|
304
|
+
- **Unload**: `navigator.sendBeacon()` for reliable delivery on page close
|
|
305
|
+
- **Retry**: Exponential backoff (1s, 2s, 4s) with max 3 retries on 5xx/429
|
|
306
|
+
|
|
307
|
+
## Data Collected
|
|
308
|
+
|
|
309
|
+
When tracking is enabled, the SDK collects:
|
|
310
|
+
- Browser information (user agent, language, screen size, viewport, timezone)
|
|
311
|
+
- Page URL and referrer (external only)
|
|
312
|
+
- UTM parameters from URL
|
|
313
|
+
- Smart link / click attribution IDs
|
|
314
|
+
- Custom event data (provided by your application)
|
|
315
|
+
|
|
316
|
+
**Not collected:**
|
|
317
|
+
- Personal user information (unless explicitly provided via `setUserID`)
|
|
318
|
+
- Location data (beyond IP-based geo-location server-side)
|
|
319
|
+
|
|
320
|
+
> **Note:** For affiliate attribution tracking (cookie-based click IDs, checkout form helpers, S2S conversion payloads), use the separate [`@linkzly/affiliate-web`](../linkzly-affiliate-web) package.
|
|
321
|
+
|
|
322
|
+
## License
|
|
323
|
+
|
|
324
|
+
MIT
|
|
325
|
+
|
|
326
|
+
## Support
|
|
327
|
+
|
|
328
|
+
- Documentation: https://docs.linkzly.com
|
|
329
|
+
- Issues: https://github.com/linkzly/linkzly-web-sdk/issues
|
|
330
|
+
- Email: support@linkzly.com
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var x=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var G=Object.prototype.hasOwnProperty;var W=(r,e)=>{for(var t in e)x(r,t,{get:e[t],enumerable:!0})},j=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Y(e))!G.call(r,i)&&i!==t&&x(r,i,{get:()=>e[i],enumerable:!(n=X(e,i))||n.enumerable});return r};var Z=r=>j(x({},"__esModule",{value:!0}),r);var De={};W(De,{Environment:()=>f,LinkzlyWebSDK:()=>v,addDeepLinkListener:()=>he,clearUserID:()=>ve,configure:()=>ie,default:()=>Ee,destroy:()=>Ie,endSession:()=>we,flushEvents:()=>ce,getPendingEventCount:()=>de,getSessionId:()=>Se,getUserID:()=>me,getVisitorID:()=>ke,handleSmartLink:()=>ge,isTrackingEnabled:()=>Le,removeAllListeners:()=>pe,resetVisitorID:()=>be,setTrackingEnabled:()=>Te,setUserID:()=>fe,startSession:()=>ye,trackEvent:()=>ae,trackEventBatch:()=>ue,trackFirstVisit:()=>re,trackOpen:()=>se,trackPageView:()=>oe,trackPurchase:()=>le});module.exports=Z(De);var f=(n=>(n[n.PRODUCTION=0]="PRODUCTION",n[n.STAGING=1]="STAGING",n[n.DEVELOPMENT=2]="DEVELOPMENT",n))(f||{});var ee={0:"https://ske.linkzly.com",1:"https://ske-staging.linkzly.com",2:"https://ske-dev.linkzly.com"},d={autoTrackPageViews:!1,autoTrackSessions:!0,autoExtractUTM:!0,autoCaptureReferrer:!0,respectDNT:!0,batchSize:20,flushIntervalMs:3e4,sessionTimeoutMs:3e4,debug:!1};function N(r,e,t){return{sdkKey:r,environment:e,baseURL:ee[e],autoTrackPageViews:t?.autoTrackPageViews??d.autoTrackPageViews,autoTrackSessions:t?.autoTrackSessions??d.autoTrackSessions,autoExtractUTM:t?.autoExtractUTM??d.autoExtractUTM,autoCaptureReferrer:t?.autoCaptureReferrer??d.autoCaptureReferrer,respectDNT:t?.respectDNT??d.respectDNT,batchSize:t?.batchSize??d.batchSize,flushIntervalMs:t?.flushIntervalMs??d.flushIntervalMs,sessionTimeoutMs:t?.sessionTimeoutMs??d.sessionTimeoutMs,debug:t?.debug??e===2}}var y=class{constructor(e){this.enabled=e}info(...e){this.enabled&&console.log("[LinkzlySDK]",...e)}debug(...e){this.enabled&&console.debug("[LinkzlySDK]",...e)}warn(...e){this.enabled&&console.warn("[LinkzlySDK]",...e)}error(...e){console.error("[LinkzlySDK]",...e)}};function z(){let r=typeof navigator<"u"?navigator:null,e=typeof window<"u"?window:null,t=typeof document<"u"?document:null,n=typeof window<"u"?window.screen:null;return{userAgent:r?.userAgent??"unknown",language:r?.language??"unknown",languages:r?.languages?.join(",")??"",screenSize:n?`${n.width}x${n.height}`:"unknown",viewportSize:e?`${e.innerWidth}x${e.innerHeight}`:"unknown",timezone:Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.timeZone??"unknown",timezoneOffset:new Date().getTimezoneOffset(),colorDepth:n?.colorDepth??0,pixelRatio:e?.devicePixelRatio??1,platform:r?.platform??"unknown",cookiesEnabled:r?.cookieEnabled??!1,online:r?.onLine??!0,referrer:t?.referrer??"",pageUrl:e?.location?.href??""}}function O(r){return{userAgent:r.userAgent,language:r.language,languages:r.languages,screenSize:r.screenSize,viewportSize:r.viewportSize,timezone:r.timezone,timezoneOffset:r.timezoneOffset,colorDepth:r.colorDepth,pixelRatio:r.pixelRatio,platform:r.platform,cookiesEnabled:r.cookiesEnabled,online:r.online,referrer:r.referrer,pageUrl:r.pageUrl}}function l(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,r=>{let e=Math.random()*16|0;return(r==="x"?e:e&3|8).toString(16)})}function m(r){if(!r)return!1;try{let e="__lz_test__";return r.setItem(e,"1"),r.removeItem(e),!0}catch{return!1}}function h(r){if(typeof localStorage>"u"||!m(localStorage))return null;try{return localStorage.getItem("lz_"+r)}catch{return null}}function c(r,e){if(!(typeof localStorage>"u"||!m(localStorage)))try{localStorage.setItem("lz_"+r,e)}catch{}}function q(r){if(!(typeof localStorage>"u"||!m(localStorage)))try{localStorage.removeItem("lz_"+r)}catch{}}function p(r){if(typeof sessionStorage>"u"||!m(sessionStorage))return null;try{return sessionStorage.getItem("lz_"+r)}catch{return null}}function u(r,e){if(!(typeof sessionStorage>"u"||!m(sessionStorage)))try{sessionStorage.setItem("lz_"+r,e)}catch{}}function R(r){if(!(typeof sessionStorage>"u"||!m(sessionStorage)))try{sessionStorage.removeItem("lz_"+r)}catch{}}function B(r){let e=h(r);if(!e)return null;try{return JSON.parse(e)}catch{return null}}function H(r,e){try{c(r,JSON.stringify(e))}catch{}}var w=3,F=1e3,te="Bearer linkzly-production-shared-secret-key-2024-secure-min-64",S=class{constructor(e,t){this.config=e,this.logger=t}async sendEvent(e){let t=`${this.config.baseURL}/v1/sdk/events`;return this.logger.debug("Sending event:",e.eventType,e.eventName??""),this.postJSON(t,e)}async sendBatch(e){if(e.length===0)return{success:!0};let t=`${this.config.baseURL}/v1/sdk/events/batch`,n={type:"sdk_event",events:e,batchId:l(),clientTimestamp:Date.now()};return this.logger.debug(`Sending batch of ${e.length} events`),this.postJSON(t,n)}sendBeacon(e){if(e.length===0)return!0;if(typeof navigator>"u"||!navigator.sendBeacon)return!1;let t=`${this.config.baseURL}/v1/sdk/events/batch`,n={type:"sdk_event",events:e,batchId:l(),clientTimestamp:Date.now()};try{let i=new Blob([JSON.stringify(n)],{type:"application/json"}),o=navigator.sendBeacon(t,i);return this.logger.debug(`Beacon ${o?"accepted":"rejected"}: ${e.length} events`),o}catch(i){return this.logger.error("sendBeacon failed:",i),!1}}async postJSON(e,t,n=0){try{let i=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","X-Linkzly-SDK":"web/1.0.0",Authorization:te},body:JSON.stringify(t),keepalive:!0});if(!i.ok){if((i.status>=500||i.status===429)&&n<w){let a=F*Math.pow(2,n);return this.logger.warn(`HTTP ${i.status}, retrying in ${a}ms (attempt ${n+1}/${w})`),await this.sleep(a),this.postJSON(e,t,n+1)}return this.logger.error(`HTTP ${i.status}: ${i.statusText}`),{success:!1,message:`HTTP ${i.status}`}}let o=await i.json().catch(()=>({}));return this.logger.debug("Response:",i.status),{success:!0,...o}}catch(i){if(n<w){let o=F*Math.pow(2,n);return this.logger.warn(`Network error, retrying in ${o}ms (attempt ${n+1}/${w}):`,i),await this.sleep(o),this.postJSON(e,t,n+1)}return this.logger.error("Network request failed after retries:",i),{success:!1,message:String(i)}}}sleep(e){return new Promise(t=>setTimeout(t,e))}};var V="event_queue",C=500,$=100,T=class{constructor(e,t,n){this.queue=[];this.flushTimer=null;this.isFlushing=!1;this.config=e,this.logger=t,this.network=n,this.restoreQueue(),this.startFlushTimer(),typeof window<"u"&&(window.addEventListener("beforeunload",()=>this.flushBeacon()),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flushBeacon()}))}enqueue(e){let t={id:l(),payload:e,createdAt:Date.now(),retryCount:0};this.queue.push(t),this.queue.length>C&&(this.queue=this.queue.slice(-C)),this.persistQueue(),this.logger.debug(`Event queued (${this.queue.length} pending): ${e.eventType}/${e.eventName??""}`),this.queue.length>=this.config.batchSize&&this.flush().catch(n=>this.logger.error("Auto-flush failed:",n))}async flush(){if(this.isFlushing||this.queue.length===0)return!0;this.isFlushing=!0;try{let e=this.queue.slice(0,$),t=e.map(i=>i.payload);if((await this.network.sendBatch(t)).success){let i=new Set(e.map(o=>o.id));return this.queue=this.queue.filter(o=>!i.has(o.id)),this.persistQueue(),this.logger.debug(`Flushed ${e.length} events, ${this.queue.length} remaining`),!0}return this.logger.warn(`Batch send failed, ${this.queue.length} events remain in queue`),!1}finally{this.isFlushing=!1}}getPendingCount(){return this.queue.length}destroy(){this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null)}startFlushTimer(){this.flushTimer||(this.flushTimer=setInterval(()=>{this.flush().catch(e=>this.logger.error("Periodic flush failed:",e))},this.config.flushIntervalMs))}flushBeacon(){if(this.queue.length===0)return;let e=this.queue.slice(0,$),t=e.map(i=>i.payload);if(this.network.sendBeacon(t)){let i=new Set(e.map(o=>o.id));this.queue=this.queue.filter(o=>!i.has(o.id)),this.persistQueue()}}persistQueue(){let e=this.queue.slice(-C);H(V,e)}restoreQueue(){let e=B(V);if(e&&Array.isArray(e)){let t=Date.now()-864e5;this.queue=e.filter(n=>n.createdAt>t),this.queue.length!==e.length&&(this.logger.debug(`Pruned ${e.length-this.queue.length} expired events from queue`),this.persistQueue()),this.queue.length>0&&this.logger.debug(`Restored ${this.queue.length} queued events from storage`)}}};var U="session_id",L="session_last_active",K="first_visit_done",I=class{constructor(e,t){this.callback=null;this.visibilityHandler=null;this.config=e,this.logger=t,this.lastActiveTime=Date.now();let n=p(U),i=p(L);n&&i?Date.now()-parseInt(i,10)<this.config.sessionTimeoutMs?(this.sessionId=n,this.lastActiveTime=parseInt(i,10),this.logger.debug("Resumed session:",this.sessionId)):this.sessionId=this.createNewSession():this.sessionId=this.createNewSession(),this.config.autoTrackSessions&&this.setupVisibilityTracking()}getSessionId(){return this.sessionId}isFirstVisit(){return h(K)===null}markFirstVisitDone(){c(K,String(Date.now()))}onSessionEvent(e){this.callback=e}touch(){this.lastActiveTime=Date.now(),u(L,String(this.lastActiveTime))}startSession(){return this.callback&&this.callback("end",this.sessionId),this.sessionId=this.createNewSession(),this.sessionId}endSession(){this.callback&&this.callback("end",this.sessionId),R(U),R(L)}destroy(){this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null)}createNewSession(){let e=l();return u(U,e),this.lastActiveTime=Date.now(),u(L,String(this.lastActiveTime)),this.logger.debug("New session started:",e),this.callback&&this.callback("start",e),e}setupVisibilityTracking(){typeof document>"u"||(this.visibilityHandler=()=>{if(document.visibilityState==="visible"){let e=Date.now()-this.lastActiveTime;e>=this.config.sessionTimeoutMs?(this.logger.debug(`Session timeout (${Math.round(e/1e3)}s idle), starting new session`),this.sessionId=this.createNewSession()):this.touch()}else this.touch()},document.addEventListener("visibilitychange",this.visibilityHandler))}};var Q="utm_",E="referrer",J="landing_page",ne=[{key:"utmSource",urlParam:"utm_source"},{key:"utmMedium",urlParam:"utm_medium"},{key:"utmCampaign",urlParam:"utm_campaign"},{key:"utmTerm",urlParam:"utm_term"},{key:"utmContent",urlParam:"utm_content"}],D=class{constructor(e,t){this.smartLinkId=null;this.clickId=null;this.deepLinkListeners=new Set;this.popStateHandler=null;this.config=e,this.logger=t,this.utm=this.initUTM(),this.referrer=this.initReferrer(),this.landingPage=this.initLandingPage(),this.initSmartLinkParams(),this.config.autoTrackPageViews&&this.setupSPATracking()}getUTM(){return{...this.utm}}getReferrer(){return this.referrer}getLandingPage(){return this.landingPage}getSmartLinkId(){return this.smartLinkId}getClickId(){return this.clickId}getAttributionData(){return{utm:this.getUTM(),referrer:this.referrer,smartLinkId:this.smartLinkId,clickId:this.clickId,landingPage:this.landingPage}}parseDeepLink(e){let t=e??(typeof window<"u"?window.location.href:""),n={},i="/";try{let g=new URL(t,"https://placeholder.com");i=g.pathname,g.searchParams.forEach((b,k)=>{n[k]=b})}catch{let g=t.split("?");g[0]&&(i=g[0].match(/^[^:]+:\/\/[^/]+(\/.*)?$/)?.[1]??"/"),g[1]&&g[1].split("&").forEach(b=>{let[k,P]=b.split("=");if(k&&P)try{n[decodeURIComponent(k)]=decodeURIComponent(P)}catch{n[k]=P}})}let o=n.slid??n.smartLinkId??null,a=n.cid??n.clickId??null;return delete n.slid,delete n.smartLinkId,delete n.cid,delete n.clickId,{url:t,path:i,parameters:n,smartLinkId:o,clickId:a}}addDeepLinkListener(e){return this.deepLinkListeners.add(e),()=>{this.deepLinkListeners.delete(e)}}removeAllListeners(){this.deepLinkListeners.clear()}destroy(){this.deepLinkListeners.clear(),typeof window<"u"&&this.popStateHandler&&window.removeEventListener("popstate",this.popStateHandler)}initUTM(){let e={utmSource:null,utmMedium:null,utmCampaign:null,utmTerm:null,utmContent:null};if(!this.config.autoExtractUTM)return e;let t=typeof window<"u"?new URLSearchParams(window.location.search):null;for(let{key:n,urlParam:i}of ne){let o=t?.get(i)??null;o?(e[n]=o,u(Q+i,o)):e[n]=p(Q+i)}return e.utmSource&&this.logger.debug("UTM parameters captured:",e),e}initReferrer(){if(!this.config.autoCaptureReferrer)return null;let e=p(E);if(e!==null)return e||null;let t=typeof document<"u"?document.referrer:"";if(t){try{let n=new URL(t).hostname,i=typeof window<"u"?window.location.hostname:"";if(n===i)return u(E,""),null}catch{}return u(E,t),this.logger.debug("Referrer captured:",t),t}return u(E,""),null}initLandingPage(){let e=p(J);if(e)return e;let t=typeof window<"u"?window.location.href:null;return t&&u(J,t),t}initSmartLinkParams(){if(typeof window>"u")return;let e=new URLSearchParams(window.location.search);this.smartLinkId=e.get("slid")??e.get("smartLinkId")??null,this.clickId=e.get("cid")??e.get("clickId")??null,this.smartLinkId&&this.logger.debug("Smart link ID captured:",this.smartLinkId)}setupSPATracking(){if(typeof window>"u")return;let e=history.pushState.bind(history),t=history.replaceState.bind(history),n=()=>{let i=this.parseDeepLink();this.deepLinkListeners.forEach(o=>{try{o(i)}catch(a){this.logger.error("Deep link listener error:",a)}})};history.pushState=(...i)=>{e(...i),n()},history.replaceState=(...i)=>{t(...i),n()},this.popStateHandler=n,window.addEventListener("popstate",this.popStateHandler)}};var _="visitor_id",M="user_id",A="tracking_enabled",v=class{constructor(){this.config=null;this.isConfigured=!1;this.scrollDepthHandler=null;this.maxScrollDepth=0;this.pageEntryTime=0;this.outboundClickHandler=null}configure(e,t=0,n){if(this.isConfigured){this.logger?.warn("SDK already configured, ignoring reconfiguration");return}this.config=N(e,t,n),this.logger=new y(this.config.debug),this.config.respectDNT&&this.isDNTEnabled()&&(this.logger.info("DNT/GPC detected \u2014 tracking disabled by default"),c(A,"false")),this.network=new S(this.config,this.logger),this.eventQueue=new T(this.config,this.logger,this.network),this.session=new I(this.config,this.logger),this.attribution=new D(this.config,this.logger),this.session.onSessionEvent((i,o)=>{i==="start"&&(this.logger.debug("Session started, auto-tracking open"),this.trackOpen().catch(a=>this.logger.error("Auto trackOpen failed:",a)))}),this.getVisitorID(),this.isConfigured=!0,this.logger.info(`SDK configured (env: ${f[t]}, key: ${e.substring(0,8)}...)`),this.session.isFirstVisit()&&(this.trackFirstVisit().catch(i=>this.logger.error("trackFirstVisit failed:",i)),this.session.markFirstVisitDone()),this.config.autoTrackPageViews&&this.trackPageView().catch(i=>this.logger.error("Auto trackPageView failed:",i)),this.setupScrollDepthTracking(),this.setupOutboundClickTracking(),this.pageEntryTime=Date.now()}async trackFirstVisit(){this.ensureConfigured();let e=this.buildPayload("install",null);this.eventQueue.enqueue(e),this.logger.info("First visit tracked")}async trackOpen(){this.ensureConfigured();let e=this.buildPayload("open",null);this.eventQueue.enqueue(e),this.logger.debug("App open tracked")}async trackPageView(e,t){this.ensureConfigured();let n=e??(typeof window<"u"?window.location.href:""),i=this.buildPayload("page_view","page_view",{page_url:n,page_title:typeof document<"u"?document.title:"",...t});this.eventQueue.enqueue(i),this.session.touch(),this.logger.debug("Page view tracked:",n)}async trackEvent(e,t){this.ensureConfigured();let n=this.buildPayload("custom",e,t);this.eventQueue.enqueue(n),this.session.touch(),this.logger.debug("Event tracked:",e)}async trackPurchase(e){this.ensureConfigured();let t=this.buildPayload("purchase","purchase",e);this.eventQueue.enqueue(t),this.session.touch(),this.logger.debug("Purchase tracked")}async trackEventBatch(e){this.ensureConfigured();for(let t of e){let n=this.buildPayload("custom",t.eventName,t.parameters);this.eventQueue.enqueue(n)}return this.session.touch(),this.logger.debug(`Batch of ${e.length} events queued`),!0}async flushEvents(){return this.ensureConfigured(),this.eventQueue.flush()}getPendingEventCount(){return this.ensureConfigured(),this.eventQueue.getPendingCount()}handleSmartLink(e){return this.ensureConfigured(),this.attribution.parseDeepLink(e)}addDeepLinkListener(e){return this.ensureConfigured(),this.attribution.addDeepLinkListener(e)}removeAllListeners(){this.attribution&&this.attribution.removeAllListeners()}setUserID(e){c(M,e),this.logger?.debug("User ID set:",e)}getUserID(){return h(M)}clearUserID(){q(M),this.logger?.debug("User ID cleared")}getVisitorID(){let e=h(_);return e||(e=l(),c(_,e),this.logger?.debug("New visitor ID generated:",e)),e}resetVisitorID(){let e=l();return c(_,e),this.logger?.debug("Visitor ID reset:",e),e}startSession(){return this.ensureConfigured(),this.session.startSession()}endSession(){this.ensureConfigured(),this.trackTimeOnPage(),this.session.endSession()}getSessionId(){return this.ensureConfigured(),this.session.getSessionId()}setTrackingEnabled(e){c(A,String(e)),this.logger?.info("Tracking",e?"enabled":"disabled")}isTrackingEnabled(){return h(A)!=="false"}setupScrollDepthTracking(){typeof window>"u"||typeof document>"u"||(this.maxScrollDepth=0,this.scrollDepthHandler=()=>{let e=window.scrollY||document.documentElement.scrollTop,t=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)-window.innerHeight;if(t<=0)return;let n=Math.round(e/t*100);n>this.maxScrollDepth&&(this.maxScrollDepth=n)},window.addEventListener("scroll",this.scrollDepthHandler,{passive:!0}),window.addEventListener("beforeunload",()=>{if(this.maxScrollDepth>0&&this.isTrackingEnabled()){let e=this.buildPayload("custom","scroll_depth",{max_depth_percent:this.maxScrollDepth,page_url:window.location.href});this.network?.sendBeacon([e])}}))}trackTimeOnPage(){if(!this.pageEntryTime||!this.isTrackingEnabled())return;let e=Math.round((Date.now()-this.pageEntryTime)/1e3);if(e>0){let t=this.buildPayload("custom","time_on_page",{duration_seconds:e,page_url:typeof window<"u"?window.location.href:""});this.eventQueue?.enqueue(t)}}setupOutboundClickTracking(){typeof document>"u"||typeof window>"u"||(this.outboundClickHandler=e=>{let t=e.target?.closest?.("a");if(t?.href)try{let n=new URL(t.href).hostname,i=window.location.hostname;if(n!==i&&this.isTrackingEnabled()){let o=this.buildPayload("custom","outbound_click",{destination_url:t.href,link_text:t.textContent?.trim()?.substring(0,100)??"",page_url:window.location.href});this.eventQueue?.enqueue(o)}}catch{}},document.addEventListener("click",this.outboundClickHandler))}destroy(){this.trackTimeOnPage(),this.scrollDepthHandler&&typeof window<"u"&&(window.removeEventListener("scroll",this.scrollDepthHandler),this.scrollDepthHandler=null),this.outboundClickHandler&&typeof document<"u"&&(document.removeEventListener("click",this.outboundClickHandler),this.outboundClickHandler=null),this.eventQueue?.destroy(),this.session?.destroy(),this.attribution?.destroy(),this.isConfigured=!1,this.logger?.info("SDK destroyed")}ensureConfigured(){if(!this.isConfigured||!this.config)throw new Error("LinkzlySDK is not configured. Call LinkzlySDK.configure() first.")}buildPayload(e,t,n){if(!this.config)throw new Error("SDK not configured");if(!this.isTrackingEnabled())return{type:"sdk_event",sdkKey:this.config.sdkKey,eventType:e,eventName:t,appUserId:null,sessionId:"",visitorId:"",platform:"web",deviceFingerprint:{},deepLinkMatched:!1,deepLinkData:null,customData:null,utmSource:null,utmMedium:null,utmCampaign:null,utmTerm:null,utmContent:null,smartLinkId:null,clickId:null,timestamp:Date.now()};let i=z(),o=this.attribution.getUTM(),a=this.attribution.getAttributionData();return{type:"sdk_event",sdkKey:this.config.sdkKey,eventType:e,eventName:t,appUserId:this.getUserID(),sessionId:this.session.getSessionId(),visitorId:this.getVisitorID(),platform:"web",deviceFingerprint:O(i),deepLinkMatched:!!(a.smartLinkId||a.clickId),deepLinkData:a.smartLinkId||a.clickId?{...a.smartLinkId?{slid:a.smartLinkId}:{},...a.clickId?{cid:a.clickId}:{}}:null,customData:n??null,utmSource:o.utmSource,utmMedium:o.utmMedium,utmCampaign:o.utmCampaign,utmTerm:o.utmTerm,utmContent:o.utmContent,smartLinkId:a.smartLinkId,clickId:a.clickId,timestamp:Date.now()}}isDNTEnabled(){return typeof navigator>"u"?!1:navigator.doNotTrack==="1"||navigator.globalPrivacyControl===!0}};var s=new v,ie=s.configure.bind(s),re=s.trackFirstVisit.bind(s),se=s.trackOpen.bind(s),oe=s.trackPageView.bind(s),ae=s.trackEvent.bind(s),le=s.trackPurchase.bind(s),ue=s.trackEventBatch.bind(s),ce=s.flushEvents.bind(s),de=s.getPendingEventCount.bind(s),ge=s.handleSmartLink.bind(s),he=s.addDeepLinkListener.bind(s),pe=s.removeAllListeners.bind(s),fe=s.setUserID.bind(s),me=s.getUserID.bind(s),ve=s.clearUserID.bind(s),ke=s.getVisitorID.bind(s),be=s.resetVisitorID.bind(s),ye=s.startSession.bind(s),we=s.endSession.bind(s),Se=s.getSessionId.bind(s),Te=s.setTrackingEnabled.bind(s),Le=s.isTrackingEnabled.bind(s),Ie=s.destroy.bind(s),Ee=s;0&&(module.exports={Environment,LinkzlyWebSDK,addDeepLinkListener,clearUserID,configure,destroy,endSession,flushEvents,getPendingEventCount,getSessionId,getUserID,getVisitorID,handleSmartLink,isTrackingEnabled,removeAllListeners,resetVisitorID,setTrackingEnabled,setUserID,startSession,trackEvent,trackEventBatch,trackFirstVisit,trackOpen,trackPageView,trackPurchase});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
declare enum Environment {
|
|
2
|
+
PRODUCTION = 0,
|
|
3
|
+
STAGING = 1,
|
|
4
|
+
DEVELOPMENT = 2
|
|
5
|
+
}
|
|
6
|
+
interface LinkzlyWebConfig {
|
|
7
|
+
/** Auto-track page views on SPA navigation (default: false — industry standard opt-in) */
|
|
8
|
+
autoTrackPageViews?: boolean;
|
|
9
|
+
/** Auto-track session start/end via visibility API (default: true) */
|
|
10
|
+
autoTrackSessions?: boolean;
|
|
11
|
+
/** Auto-extract UTM parameters from URL (default: true) */
|
|
12
|
+
autoExtractUTM?: boolean;
|
|
13
|
+
/** Auto-capture referrer (default: true) */
|
|
14
|
+
autoCaptureReferrer?: boolean;
|
|
15
|
+
/** Respect navigator.doNotTrack / globalPrivacyControl (default: true) */
|
|
16
|
+
respectDNT?: boolean;
|
|
17
|
+
/** Event batch size before auto-flush (default: 20) */
|
|
18
|
+
batchSize?: number;
|
|
19
|
+
/** Flush interval in milliseconds (default: 30000) */
|
|
20
|
+
flushIntervalMs?: number;
|
|
21
|
+
/** Session timeout in milliseconds (default: 30000 — 30s, matching mobile SDKs) */
|
|
22
|
+
sessionTimeoutMs?: number;
|
|
23
|
+
/** Enable debug logging (default: false) */
|
|
24
|
+
debug?: boolean;
|
|
25
|
+
}
|
|
26
|
+
type EventType = 'install' | 'open' | 'page_view' | 'purchase' | 'custom';
|
|
27
|
+
interface EventParameters {
|
|
28
|
+
[key: string]: string | number | boolean | null | undefined | EventParameters | Array<string | number | boolean>;
|
|
29
|
+
}
|
|
30
|
+
interface BatchEvent {
|
|
31
|
+
eventName: string;
|
|
32
|
+
parameters?: EventParameters;
|
|
33
|
+
}
|
|
34
|
+
interface TrackingPayload {
|
|
35
|
+
type: 'sdk_event';
|
|
36
|
+
sdkKey: string;
|
|
37
|
+
eventType: EventType;
|
|
38
|
+
eventName: string | null;
|
|
39
|
+
appUserId: string | null;
|
|
40
|
+
sessionId: string;
|
|
41
|
+
visitorId: string;
|
|
42
|
+
platform: 'web';
|
|
43
|
+
deviceFingerprint: Record<string, string | number | boolean | null>;
|
|
44
|
+
deepLinkMatched: boolean;
|
|
45
|
+
deepLinkData: Record<string, string> | null;
|
|
46
|
+
customData: EventParameters | null;
|
|
47
|
+
utmSource: string | null;
|
|
48
|
+
utmMedium: string | null;
|
|
49
|
+
utmCampaign: string | null;
|
|
50
|
+
utmTerm: string | null;
|
|
51
|
+
utmContent: string | null;
|
|
52
|
+
smartLinkId: string | null;
|
|
53
|
+
clickId: string | null;
|
|
54
|
+
timestamp: number;
|
|
55
|
+
}
|
|
56
|
+
interface DeepLinkData {
|
|
57
|
+
url: string;
|
|
58
|
+
path: string;
|
|
59
|
+
parameters: Record<string, string>;
|
|
60
|
+
smartLinkId: string | null;
|
|
61
|
+
clickId: string | null;
|
|
62
|
+
}
|
|
63
|
+
type DeepLinkListener = (data: DeepLinkData) => void;
|
|
64
|
+
interface UTMParameters {
|
|
65
|
+
utmSource: string | null;
|
|
66
|
+
utmMedium: string | null;
|
|
67
|
+
utmCampaign: string | null;
|
|
68
|
+
utmTerm: string | null;
|
|
69
|
+
utmContent: string | null;
|
|
70
|
+
}
|
|
71
|
+
interface AttributionData {
|
|
72
|
+
utm: UTMParameters;
|
|
73
|
+
referrer: string | null;
|
|
74
|
+
smartLinkId: string | null;
|
|
75
|
+
clickId: string | null;
|
|
76
|
+
landingPage: string | null;
|
|
77
|
+
}
|
|
78
|
+
interface BrowserFingerprint {
|
|
79
|
+
userAgent: string;
|
|
80
|
+
language: string;
|
|
81
|
+
languages: string;
|
|
82
|
+
screenSize: string;
|
|
83
|
+
viewportSize: string;
|
|
84
|
+
timezone: string;
|
|
85
|
+
timezoneOffset: number;
|
|
86
|
+
colorDepth: number;
|
|
87
|
+
pixelRatio: number;
|
|
88
|
+
platform: string;
|
|
89
|
+
cookiesEnabled: boolean;
|
|
90
|
+
online: boolean;
|
|
91
|
+
referrer: string;
|
|
92
|
+
pageUrl: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
declare class LinkzlyWebSDK {
|
|
96
|
+
private config;
|
|
97
|
+
private logger;
|
|
98
|
+
private network;
|
|
99
|
+
private eventQueue;
|
|
100
|
+
private session;
|
|
101
|
+
private attribution;
|
|
102
|
+
private isConfigured;
|
|
103
|
+
private scrollDepthHandler;
|
|
104
|
+
private maxScrollDepth;
|
|
105
|
+
private pageEntryTime;
|
|
106
|
+
private outboundClickHandler;
|
|
107
|
+
configure(sdkKey: string, environment?: Environment, options?: LinkzlyWebConfig): void;
|
|
108
|
+
trackFirstVisit(): Promise<void>;
|
|
109
|
+
trackOpen(): Promise<void>;
|
|
110
|
+
trackPageView(pageUrl?: string, params?: EventParameters): Promise<void>;
|
|
111
|
+
trackEvent(eventName: string, parameters?: EventParameters): Promise<void>;
|
|
112
|
+
trackPurchase(parameters?: EventParameters): Promise<void>;
|
|
113
|
+
trackEventBatch(events: BatchEvent[]): Promise<boolean>;
|
|
114
|
+
flushEvents(): Promise<boolean>;
|
|
115
|
+
getPendingEventCount(): number;
|
|
116
|
+
handleSmartLink(url?: string): DeepLinkData;
|
|
117
|
+
addDeepLinkListener(listener: DeepLinkListener): () => void;
|
|
118
|
+
removeAllListeners(): void;
|
|
119
|
+
setUserID(userID: string): void;
|
|
120
|
+
getUserID(): string | null;
|
|
121
|
+
clearUserID(): void;
|
|
122
|
+
getVisitorID(): string;
|
|
123
|
+
resetVisitorID(): string;
|
|
124
|
+
startSession(): string;
|
|
125
|
+
endSession(): void;
|
|
126
|
+
getSessionId(): string;
|
|
127
|
+
setTrackingEnabled(enabled: boolean): void;
|
|
128
|
+
isTrackingEnabled(): boolean;
|
|
129
|
+
private setupScrollDepthTracking;
|
|
130
|
+
private trackTimeOnPage;
|
|
131
|
+
private setupOutboundClickTracking;
|
|
132
|
+
destroy(): void;
|
|
133
|
+
private ensureConfigured;
|
|
134
|
+
private buildPayload;
|
|
135
|
+
private isDNTEnabled;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
declare const instance: LinkzlyWebSDK;
|
|
139
|
+
declare const configure: (sdkKey: string, environment?: Environment, options?: LinkzlyWebConfig) => void;
|
|
140
|
+
declare const trackFirstVisit: () => Promise<void>;
|
|
141
|
+
declare const trackOpen: () => Promise<void>;
|
|
142
|
+
declare const trackPageView: (pageUrl?: string, params?: EventParameters) => Promise<void>;
|
|
143
|
+
declare const trackEvent: (eventName: string, parameters?: EventParameters) => Promise<void>;
|
|
144
|
+
declare const trackPurchase: (parameters?: EventParameters) => Promise<void>;
|
|
145
|
+
declare const trackEventBatch: (events: BatchEvent[]) => Promise<boolean>;
|
|
146
|
+
declare const flushEvents: () => Promise<boolean>;
|
|
147
|
+
declare const getPendingEventCount: () => number;
|
|
148
|
+
declare const handleSmartLink: (url?: string) => DeepLinkData;
|
|
149
|
+
declare const addDeepLinkListener: (listener: DeepLinkListener) => () => void;
|
|
150
|
+
declare const removeAllListeners: () => void;
|
|
151
|
+
declare const setUserID: (userID: string) => void;
|
|
152
|
+
declare const getUserID: () => string | null;
|
|
153
|
+
declare const clearUserID: () => void;
|
|
154
|
+
declare const getVisitorID: () => string;
|
|
155
|
+
declare const resetVisitorID: () => string;
|
|
156
|
+
declare const startSession: () => string;
|
|
157
|
+
declare const endSession: () => void;
|
|
158
|
+
declare const getSessionId: () => string;
|
|
159
|
+
declare const setTrackingEnabled: (enabled: boolean) => void;
|
|
160
|
+
declare const isTrackingEnabled: () => boolean;
|
|
161
|
+
declare const destroy: () => void;
|
|
162
|
+
|
|
163
|
+
export { type AttributionData, type BatchEvent, type BrowserFingerprint, type DeepLinkData, type DeepLinkListener, Environment, type EventParameters, type EventType, type LinkzlyWebConfig, LinkzlyWebSDK, type TrackingPayload, type UTMParameters, addDeepLinkListener, clearUserID, configure, instance as default, destroy, endSession, flushEvents, getPendingEventCount, getSessionId, getUserID, getVisitorID, handleSmartLink, isTrackingEnabled, removeAllListeners, resetVisitorID, setTrackingEnabled, setUserID, startSession, trackEvent, trackEventBatch, trackFirstVisit, trackOpen, trackPageView, trackPurchase };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
declare enum Environment {
|
|
2
|
+
PRODUCTION = 0,
|
|
3
|
+
STAGING = 1,
|
|
4
|
+
DEVELOPMENT = 2
|
|
5
|
+
}
|
|
6
|
+
interface LinkzlyWebConfig {
|
|
7
|
+
/** Auto-track page views on SPA navigation (default: false — industry standard opt-in) */
|
|
8
|
+
autoTrackPageViews?: boolean;
|
|
9
|
+
/** Auto-track session start/end via visibility API (default: true) */
|
|
10
|
+
autoTrackSessions?: boolean;
|
|
11
|
+
/** Auto-extract UTM parameters from URL (default: true) */
|
|
12
|
+
autoExtractUTM?: boolean;
|
|
13
|
+
/** Auto-capture referrer (default: true) */
|
|
14
|
+
autoCaptureReferrer?: boolean;
|
|
15
|
+
/** Respect navigator.doNotTrack / globalPrivacyControl (default: true) */
|
|
16
|
+
respectDNT?: boolean;
|
|
17
|
+
/** Event batch size before auto-flush (default: 20) */
|
|
18
|
+
batchSize?: number;
|
|
19
|
+
/** Flush interval in milliseconds (default: 30000) */
|
|
20
|
+
flushIntervalMs?: number;
|
|
21
|
+
/** Session timeout in milliseconds (default: 30000 — 30s, matching mobile SDKs) */
|
|
22
|
+
sessionTimeoutMs?: number;
|
|
23
|
+
/** Enable debug logging (default: false) */
|
|
24
|
+
debug?: boolean;
|
|
25
|
+
}
|
|
26
|
+
type EventType = 'install' | 'open' | 'page_view' | 'purchase' | 'custom';
|
|
27
|
+
interface EventParameters {
|
|
28
|
+
[key: string]: string | number | boolean | null | undefined | EventParameters | Array<string | number | boolean>;
|
|
29
|
+
}
|
|
30
|
+
interface BatchEvent {
|
|
31
|
+
eventName: string;
|
|
32
|
+
parameters?: EventParameters;
|
|
33
|
+
}
|
|
34
|
+
interface TrackingPayload {
|
|
35
|
+
type: 'sdk_event';
|
|
36
|
+
sdkKey: string;
|
|
37
|
+
eventType: EventType;
|
|
38
|
+
eventName: string | null;
|
|
39
|
+
appUserId: string | null;
|
|
40
|
+
sessionId: string;
|
|
41
|
+
visitorId: string;
|
|
42
|
+
platform: 'web';
|
|
43
|
+
deviceFingerprint: Record<string, string | number | boolean | null>;
|
|
44
|
+
deepLinkMatched: boolean;
|
|
45
|
+
deepLinkData: Record<string, string> | null;
|
|
46
|
+
customData: EventParameters | null;
|
|
47
|
+
utmSource: string | null;
|
|
48
|
+
utmMedium: string | null;
|
|
49
|
+
utmCampaign: string | null;
|
|
50
|
+
utmTerm: string | null;
|
|
51
|
+
utmContent: string | null;
|
|
52
|
+
smartLinkId: string | null;
|
|
53
|
+
clickId: string | null;
|
|
54
|
+
timestamp: number;
|
|
55
|
+
}
|
|
56
|
+
interface DeepLinkData {
|
|
57
|
+
url: string;
|
|
58
|
+
path: string;
|
|
59
|
+
parameters: Record<string, string>;
|
|
60
|
+
smartLinkId: string | null;
|
|
61
|
+
clickId: string | null;
|
|
62
|
+
}
|
|
63
|
+
type DeepLinkListener = (data: DeepLinkData) => void;
|
|
64
|
+
interface UTMParameters {
|
|
65
|
+
utmSource: string | null;
|
|
66
|
+
utmMedium: string | null;
|
|
67
|
+
utmCampaign: string | null;
|
|
68
|
+
utmTerm: string | null;
|
|
69
|
+
utmContent: string | null;
|
|
70
|
+
}
|
|
71
|
+
interface AttributionData {
|
|
72
|
+
utm: UTMParameters;
|
|
73
|
+
referrer: string | null;
|
|
74
|
+
smartLinkId: string | null;
|
|
75
|
+
clickId: string | null;
|
|
76
|
+
landingPage: string | null;
|
|
77
|
+
}
|
|
78
|
+
interface BrowserFingerprint {
|
|
79
|
+
userAgent: string;
|
|
80
|
+
language: string;
|
|
81
|
+
languages: string;
|
|
82
|
+
screenSize: string;
|
|
83
|
+
viewportSize: string;
|
|
84
|
+
timezone: string;
|
|
85
|
+
timezoneOffset: number;
|
|
86
|
+
colorDepth: number;
|
|
87
|
+
pixelRatio: number;
|
|
88
|
+
platform: string;
|
|
89
|
+
cookiesEnabled: boolean;
|
|
90
|
+
online: boolean;
|
|
91
|
+
referrer: string;
|
|
92
|
+
pageUrl: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
declare class LinkzlyWebSDK {
|
|
96
|
+
private config;
|
|
97
|
+
private logger;
|
|
98
|
+
private network;
|
|
99
|
+
private eventQueue;
|
|
100
|
+
private session;
|
|
101
|
+
private attribution;
|
|
102
|
+
private isConfigured;
|
|
103
|
+
private scrollDepthHandler;
|
|
104
|
+
private maxScrollDepth;
|
|
105
|
+
private pageEntryTime;
|
|
106
|
+
private outboundClickHandler;
|
|
107
|
+
configure(sdkKey: string, environment?: Environment, options?: LinkzlyWebConfig): void;
|
|
108
|
+
trackFirstVisit(): Promise<void>;
|
|
109
|
+
trackOpen(): Promise<void>;
|
|
110
|
+
trackPageView(pageUrl?: string, params?: EventParameters): Promise<void>;
|
|
111
|
+
trackEvent(eventName: string, parameters?: EventParameters): Promise<void>;
|
|
112
|
+
trackPurchase(parameters?: EventParameters): Promise<void>;
|
|
113
|
+
trackEventBatch(events: BatchEvent[]): Promise<boolean>;
|
|
114
|
+
flushEvents(): Promise<boolean>;
|
|
115
|
+
getPendingEventCount(): number;
|
|
116
|
+
handleSmartLink(url?: string): DeepLinkData;
|
|
117
|
+
addDeepLinkListener(listener: DeepLinkListener): () => void;
|
|
118
|
+
removeAllListeners(): void;
|
|
119
|
+
setUserID(userID: string): void;
|
|
120
|
+
getUserID(): string | null;
|
|
121
|
+
clearUserID(): void;
|
|
122
|
+
getVisitorID(): string;
|
|
123
|
+
resetVisitorID(): string;
|
|
124
|
+
startSession(): string;
|
|
125
|
+
endSession(): void;
|
|
126
|
+
getSessionId(): string;
|
|
127
|
+
setTrackingEnabled(enabled: boolean): void;
|
|
128
|
+
isTrackingEnabled(): boolean;
|
|
129
|
+
private setupScrollDepthTracking;
|
|
130
|
+
private trackTimeOnPage;
|
|
131
|
+
private setupOutboundClickTracking;
|
|
132
|
+
destroy(): void;
|
|
133
|
+
private ensureConfigured;
|
|
134
|
+
private buildPayload;
|
|
135
|
+
private isDNTEnabled;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
declare const instance: LinkzlyWebSDK;
|
|
139
|
+
declare const configure: (sdkKey: string, environment?: Environment, options?: LinkzlyWebConfig) => void;
|
|
140
|
+
declare const trackFirstVisit: () => Promise<void>;
|
|
141
|
+
declare const trackOpen: () => Promise<void>;
|
|
142
|
+
declare const trackPageView: (pageUrl?: string, params?: EventParameters) => Promise<void>;
|
|
143
|
+
declare const trackEvent: (eventName: string, parameters?: EventParameters) => Promise<void>;
|
|
144
|
+
declare const trackPurchase: (parameters?: EventParameters) => Promise<void>;
|
|
145
|
+
declare const trackEventBatch: (events: BatchEvent[]) => Promise<boolean>;
|
|
146
|
+
declare const flushEvents: () => Promise<boolean>;
|
|
147
|
+
declare const getPendingEventCount: () => number;
|
|
148
|
+
declare const handleSmartLink: (url?: string) => DeepLinkData;
|
|
149
|
+
declare const addDeepLinkListener: (listener: DeepLinkListener) => () => void;
|
|
150
|
+
declare const removeAllListeners: () => void;
|
|
151
|
+
declare const setUserID: (userID: string) => void;
|
|
152
|
+
declare const getUserID: () => string | null;
|
|
153
|
+
declare const clearUserID: () => void;
|
|
154
|
+
declare const getVisitorID: () => string;
|
|
155
|
+
declare const resetVisitorID: () => string;
|
|
156
|
+
declare const startSession: () => string;
|
|
157
|
+
declare const endSession: () => void;
|
|
158
|
+
declare const getSessionId: () => string;
|
|
159
|
+
declare const setTrackingEnabled: (enabled: boolean) => void;
|
|
160
|
+
declare const isTrackingEnabled: () => boolean;
|
|
161
|
+
declare const destroy: () => void;
|
|
162
|
+
|
|
163
|
+
export { type AttributionData, type BatchEvent, type BrowserFingerprint, type DeepLinkData, type DeepLinkListener, Environment, type EventParameters, type EventType, type LinkzlyWebConfig, LinkzlyWebSDK, type TrackingPayload, type UTMParameters, addDeepLinkListener, clearUserID, configure, instance as default, destroy, endSession, flushEvents, getPendingEventCount, getSessionId, getUserID, getVisitorID, handleSmartLink, isTrackingEnabled, removeAllListeners, resetVisitorID, setTrackingEnabled, setUserID, startSession, trackEvent, trackEventBatch, trackFirstVisit, trackOpen, trackPageView, trackPurchase };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var v=(n=>(n[n.PRODUCTION=0]="PRODUCTION",n[n.STAGING=1]="STAGING",n[n.DEVELOPMENT=2]="DEVELOPMENT",n))(v||{});var J={0:"https://ske.linkzly.com",1:"https://ske-staging.linkzly.com",2:"https://ske-dev.linkzly.com"},d={autoTrackPageViews:!1,autoTrackSessions:!0,autoExtractUTM:!0,autoCaptureReferrer:!0,respectDNT:!0,batchSize:20,flushIntervalMs:3e4,sessionTimeoutMs:3e4,debug:!1};function A(r,e,t){return{sdkKey:r,environment:e,baseURL:J[e],autoTrackPageViews:t?.autoTrackPageViews??d.autoTrackPageViews,autoTrackSessions:t?.autoTrackSessions??d.autoTrackSessions,autoExtractUTM:t?.autoExtractUTM??d.autoExtractUTM,autoCaptureReferrer:t?.autoCaptureReferrer??d.autoCaptureReferrer,respectDNT:t?.respectDNT??d.respectDNT,batchSize:t?.batchSize??d.batchSize,flushIntervalMs:t?.flushIntervalMs??d.flushIntervalMs,sessionTimeoutMs:t?.sessionTimeoutMs??d.sessionTimeoutMs,debug:t?.debug??e===2}}var y=class{constructor(e){this.enabled=e}info(...e){this.enabled&&console.log("[LinkzlySDK]",...e)}debug(...e){this.enabled&&console.debug("[LinkzlySDK]",...e)}warn(...e){this.enabled&&console.warn("[LinkzlySDK]",...e)}error(...e){console.error("[LinkzlySDK]",...e)}};function N(){let r=typeof navigator<"u"?navigator:null,e=typeof window<"u"?window:null,t=typeof document<"u"?document:null,n=typeof window<"u"?window.screen:null;return{userAgent:r?.userAgent??"unknown",language:r?.language??"unknown",languages:r?.languages?.join(",")??"",screenSize:n?`${n.width}x${n.height}`:"unknown",viewportSize:e?`${e.innerWidth}x${e.innerHeight}`:"unknown",timezone:Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.timeZone??"unknown",timezoneOffset:new Date().getTimezoneOffset(),colorDepth:n?.colorDepth??0,pixelRatio:e?.devicePixelRatio??1,platform:r?.platform??"unknown",cookiesEnabled:r?.cookieEnabled??!1,online:r?.onLine??!0,referrer:t?.referrer??"",pageUrl:e?.location?.href??""}}function z(r){return{userAgent:r.userAgent,language:r.language,languages:r.languages,screenSize:r.screenSize,viewportSize:r.viewportSize,timezone:r.timezone,timezoneOffset:r.timezoneOffset,colorDepth:r.colorDepth,pixelRatio:r.pixelRatio,platform:r.platform,cookiesEnabled:r.cookiesEnabled,online:r.online,referrer:r.referrer,pageUrl:r.pageUrl}}function l(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,r=>{let e=Math.random()*16|0;return(r==="x"?e:e&3|8).toString(16)})}function f(r){if(!r)return!1;try{let e="__lz_test__";return r.setItem(e,"1"),r.removeItem(e),!0}catch{return!1}}function h(r){if(typeof localStorage>"u"||!f(localStorage))return null;try{return localStorage.getItem("lz_"+r)}catch{return null}}function c(r,e){if(!(typeof localStorage>"u"||!f(localStorage)))try{localStorage.setItem("lz_"+r,e)}catch{}}function O(r){if(!(typeof localStorage>"u"||!f(localStorage)))try{localStorage.removeItem("lz_"+r)}catch{}}function p(r){if(typeof sessionStorage>"u"||!f(sessionStorage))return null;try{return sessionStorage.getItem("lz_"+r)}catch{return null}}function u(r,e){if(!(typeof sessionStorage>"u"||!f(sessionStorage)))try{sessionStorage.setItem("lz_"+r,e)}catch{}}function x(r){if(!(typeof sessionStorage>"u"||!f(sessionStorage)))try{sessionStorage.removeItem("lz_"+r)}catch{}}function q(r){let e=h(r);if(!e)return null;try{return JSON.parse(e)}catch{return null}}function B(r,e){try{c(r,JSON.stringify(e))}catch{}}var w=3,H=1e3,X="Bearer linkzly-production-shared-secret-key-2024-secure-min-64",S=class{constructor(e,t){this.config=e,this.logger=t}async sendEvent(e){let t=`${this.config.baseURL}/v1/sdk/events`;return this.logger.debug("Sending event:",e.eventType,e.eventName??""),this.postJSON(t,e)}async sendBatch(e){if(e.length===0)return{success:!0};let t=`${this.config.baseURL}/v1/sdk/events/batch`,n={type:"sdk_event",events:e,batchId:l(),clientTimestamp:Date.now()};return this.logger.debug(`Sending batch of ${e.length} events`),this.postJSON(t,n)}sendBeacon(e){if(e.length===0)return!0;if(typeof navigator>"u"||!navigator.sendBeacon)return!1;let t=`${this.config.baseURL}/v1/sdk/events/batch`,n={type:"sdk_event",events:e,batchId:l(),clientTimestamp:Date.now()};try{let i=new Blob([JSON.stringify(n)],{type:"application/json"}),o=navigator.sendBeacon(t,i);return this.logger.debug(`Beacon ${o?"accepted":"rejected"}: ${e.length} events`),o}catch(i){return this.logger.error("sendBeacon failed:",i),!1}}async postJSON(e,t,n=0){try{let i=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","X-Linkzly-SDK":"web/1.0.0",Authorization:X},body:JSON.stringify(t),keepalive:!0});if(!i.ok){if((i.status>=500||i.status===429)&&n<w){let a=H*Math.pow(2,n);return this.logger.warn(`HTTP ${i.status}, retrying in ${a}ms (attempt ${n+1}/${w})`),await this.sleep(a),this.postJSON(e,t,n+1)}return this.logger.error(`HTTP ${i.status}: ${i.statusText}`),{success:!1,message:`HTTP ${i.status}`}}let o=await i.json().catch(()=>({}));return this.logger.debug("Response:",i.status),{success:!0,...o}}catch(i){if(n<w){let o=H*Math.pow(2,n);return this.logger.warn(`Network error, retrying in ${o}ms (attempt ${n+1}/${w}):`,i),await this.sleep(o),this.postJSON(e,t,n+1)}return this.logger.error("Network request failed after retries:",i),{success:!1,message:String(i)}}}sleep(e){return new Promise(t=>setTimeout(t,e))}};var F="event_queue",R=500,V=100,T=class{constructor(e,t,n){this.queue=[];this.flushTimer=null;this.isFlushing=!1;this.config=e,this.logger=t,this.network=n,this.restoreQueue(),this.startFlushTimer(),typeof window<"u"&&(window.addEventListener("beforeunload",()=>this.flushBeacon()),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flushBeacon()}))}enqueue(e){let t={id:l(),payload:e,createdAt:Date.now(),retryCount:0};this.queue.push(t),this.queue.length>R&&(this.queue=this.queue.slice(-R)),this.persistQueue(),this.logger.debug(`Event queued (${this.queue.length} pending): ${e.eventType}/${e.eventName??""}`),this.queue.length>=this.config.batchSize&&this.flush().catch(n=>this.logger.error("Auto-flush failed:",n))}async flush(){if(this.isFlushing||this.queue.length===0)return!0;this.isFlushing=!0;try{let e=this.queue.slice(0,V),t=e.map(i=>i.payload);if((await this.network.sendBatch(t)).success){let i=new Set(e.map(o=>o.id));return this.queue=this.queue.filter(o=>!i.has(o.id)),this.persistQueue(),this.logger.debug(`Flushed ${e.length} events, ${this.queue.length} remaining`),!0}return this.logger.warn(`Batch send failed, ${this.queue.length} events remain in queue`),!1}finally{this.isFlushing=!1}}getPendingCount(){return this.queue.length}destroy(){this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null)}startFlushTimer(){this.flushTimer||(this.flushTimer=setInterval(()=>{this.flush().catch(e=>this.logger.error("Periodic flush failed:",e))},this.config.flushIntervalMs))}flushBeacon(){if(this.queue.length===0)return;let e=this.queue.slice(0,V),t=e.map(i=>i.payload);if(this.network.sendBeacon(t)){let i=new Set(e.map(o=>o.id));this.queue=this.queue.filter(o=>!i.has(o.id)),this.persistQueue()}}persistQueue(){let e=this.queue.slice(-R);B(F,e)}restoreQueue(){let e=q(F);if(e&&Array.isArray(e)){let t=Date.now()-864e5;this.queue=e.filter(n=>n.createdAt>t),this.queue.length!==e.length&&(this.logger.debug(`Pruned ${e.length-this.queue.length} expired events from queue`),this.persistQueue()),this.queue.length>0&&this.logger.debug(`Restored ${this.queue.length} queued events from storage`)}}};var C="session_id",L="session_last_active",$="first_visit_done",I=class{constructor(e,t){this.callback=null;this.visibilityHandler=null;this.config=e,this.logger=t,this.lastActiveTime=Date.now();let n=p(C),i=p(L);n&&i?Date.now()-parseInt(i,10)<this.config.sessionTimeoutMs?(this.sessionId=n,this.lastActiveTime=parseInt(i,10),this.logger.debug("Resumed session:",this.sessionId)):this.sessionId=this.createNewSession():this.sessionId=this.createNewSession(),this.config.autoTrackSessions&&this.setupVisibilityTracking()}getSessionId(){return this.sessionId}isFirstVisit(){return h($)===null}markFirstVisitDone(){c($,String(Date.now()))}onSessionEvent(e){this.callback=e}touch(){this.lastActiveTime=Date.now(),u(L,String(this.lastActiveTime))}startSession(){return this.callback&&this.callback("end",this.sessionId),this.sessionId=this.createNewSession(),this.sessionId}endSession(){this.callback&&this.callback("end",this.sessionId),x(C),x(L)}destroy(){this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null)}createNewSession(){let e=l();return u(C,e),this.lastActiveTime=Date.now(),u(L,String(this.lastActiveTime)),this.logger.debug("New session started:",e),this.callback&&this.callback("start",e),e}setupVisibilityTracking(){typeof document>"u"||(this.visibilityHandler=()=>{if(document.visibilityState==="visible"){let e=Date.now()-this.lastActiveTime;e>=this.config.sessionTimeoutMs?(this.logger.debug(`Session timeout (${Math.round(e/1e3)}s idle), starting new session`),this.sessionId=this.createNewSession()):this.touch()}else this.touch()},document.addEventListener("visibilitychange",this.visibilityHandler))}};var K="utm_",E="referrer",Q="landing_page",Y=[{key:"utmSource",urlParam:"utm_source"},{key:"utmMedium",urlParam:"utm_medium"},{key:"utmCampaign",urlParam:"utm_campaign"},{key:"utmTerm",urlParam:"utm_term"},{key:"utmContent",urlParam:"utm_content"}],D=class{constructor(e,t){this.smartLinkId=null;this.clickId=null;this.deepLinkListeners=new Set;this.popStateHandler=null;this.config=e,this.logger=t,this.utm=this.initUTM(),this.referrer=this.initReferrer(),this.landingPage=this.initLandingPage(),this.initSmartLinkParams(),this.config.autoTrackPageViews&&this.setupSPATracking()}getUTM(){return{...this.utm}}getReferrer(){return this.referrer}getLandingPage(){return this.landingPage}getSmartLinkId(){return this.smartLinkId}getClickId(){return this.clickId}getAttributionData(){return{utm:this.getUTM(),referrer:this.referrer,smartLinkId:this.smartLinkId,clickId:this.clickId,landingPage:this.landingPage}}parseDeepLink(e){let t=e??(typeof window<"u"?window.location.href:""),n={},i="/";try{let g=new URL(t,"https://placeholder.com");i=g.pathname,g.searchParams.forEach((b,m)=>{n[m]=b})}catch{let g=t.split("?");g[0]&&(i=g[0].match(/^[^:]+:\/\/[^/]+(\/.*)?$/)?.[1]??"/"),g[1]&&g[1].split("&").forEach(b=>{let[m,P]=b.split("=");if(m&&P)try{n[decodeURIComponent(m)]=decodeURIComponent(P)}catch{n[m]=P}})}let o=n.slid??n.smartLinkId??null,a=n.cid??n.clickId??null;return delete n.slid,delete n.smartLinkId,delete n.cid,delete n.clickId,{url:t,path:i,parameters:n,smartLinkId:o,clickId:a}}addDeepLinkListener(e){return this.deepLinkListeners.add(e),()=>{this.deepLinkListeners.delete(e)}}removeAllListeners(){this.deepLinkListeners.clear()}destroy(){this.deepLinkListeners.clear(),typeof window<"u"&&this.popStateHandler&&window.removeEventListener("popstate",this.popStateHandler)}initUTM(){let e={utmSource:null,utmMedium:null,utmCampaign:null,utmTerm:null,utmContent:null};if(!this.config.autoExtractUTM)return e;let t=typeof window<"u"?new URLSearchParams(window.location.search):null;for(let{key:n,urlParam:i}of Y){let o=t?.get(i)??null;o?(e[n]=o,u(K+i,o)):e[n]=p(K+i)}return e.utmSource&&this.logger.debug("UTM parameters captured:",e),e}initReferrer(){if(!this.config.autoCaptureReferrer)return null;let e=p(E);if(e!==null)return e||null;let t=typeof document<"u"?document.referrer:"";if(t){try{let n=new URL(t).hostname,i=typeof window<"u"?window.location.hostname:"";if(n===i)return u(E,""),null}catch{}return u(E,t),this.logger.debug("Referrer captured:",t),t}return u(E,""),null}initLandingPage(){let e=p(Q);if(e)return e;let t=typeof window<"u"?window.location.href:null;return t&&u(Q,t),t}initSmartLinkParams(){if(typeof window>"u")return;let e=new URLSearchParams(window.location.search);this.smartLinkId=e.get("slid")??e.get("smartLinkId")??null,this.clickId=e.get("cid")??e.get("clickId")??null,this.smartLinkId&&this.logger.debug("Smart link ID captured:",this.smartLinkId)}setupSPATracking(){if(typeof window>"u")return;let e=history.pushState.bind(history),t=history.replaceState.bind(history),n=()=>{let i=this.parseDeepLink();this.deepLinkListeners.forEach(o=>{try{o(i)}catch(a){this.logger.error("Deep link listener error:",a)}})};history.pushState=(...i)=>{e(...i),n()},history.replaceState=(...i)=>{t(...i),n()},this.popStateHandler=n,window.addEventListener("popstate",this.popStateHandler)}};var U="visitor_id",_="user_id",M="tracking_enabled",k=class{constructor(){this.config=null;this.isConfigured=!1;this.scrollDepthHandler=null;this.maxScrollDepth=0;this.pageEntryTime=0;this.outboundClickHandler=null}configure(e,t=0,n){if(this.isConfigured){this.logger?.warn("SDK already configured, ignoring reconfiguration");return}this.config=A(e,t,n),this.logger=new y(this.config.debug),this.config.respectDNT&&this.isDNTEnabled()&&(this.logger.info("DNT/GPC detected \u2014 tracking disabled by default"),c(M,"false")),this.network=new S(this.config,this.logger),this.eventQueue=new T(this.config,this.logger,this.network),this.session=new I(this.config,this.logger),this.attribution=new D(this.config,this.logger),this.session.onSessionEvent((i,o)=>{i==="start"&&(this.logger.debug("Session started, auto-tracking open"),this.trackOpen().catch(a=>this.logger.error("Auto trackOpen failed:",a)))}),this.getVisitorID(),this.isConfigured=!0,this.logger.info(`SDK configured (env: ${v[t]}, key: ${e.substring(0,8)}...)`),this.session.isFirstVisit()&&(this.trackFirstVisit().catch(i=>this.logger.error("trackFirstVisit failed:",i)),this.session.markFirstVisitDone()),this.config.autoTrackPageViews&&this.trackPageView().catch(i=>this.logger.error("Auto trackPageView failed:",i)),this.setupScrollDepthTracking(),this.setupOutboundClickTracking(),this.pageEntryTime=Date.now()}async trackFirstVisit(){this.ensureConfigured();let e=this.buildPayload("install",null);this.eventQueue.enqueue(e),this.logger.info("First visit tracked")}async trackOpen(){this.ensureConfigured();let e=this.buildPayload("open",null);this.eventQueue.enqueue(e),this.logger.debug("App open tracked")}async trackPageView(e,t){this.ensureConfigured();let n=e??(typeof window<"u"?window.location.href:""),i=this.buildPayload("page_view","page_view",{page_url:n,page_title:typeof document<"u"?document.title:"",...t});this.eventQueue.enqueue(i),this.session.touch(),this.logger.debug("Page view tracked:",n)}async trackEvent(e,t){this.ensureConfigured();let n=this.buildPayload("custom",e,t);this.eventQueue.enqueue(n),this.session.touch(),this.logger.debug("Event tracked:",e)}async trackPurchase(e){this.ensureConfigured();let t=this.buildPayload("purchase","purchase",e);this.eventQueue.enqueue(t),this.session.touch(),this.logger.debug("Purchase tracked")}async trackEventBatch(e){this.ensureConfigured();for(let t of e){let n=this.buildPayload("custom",t.eventName,t.parameters);this.eventQueue.enqueue(n)}return this.session.touch(),this.logger.debug(`Batch of ${e.length} events queued`),!0}async flushEvents(){return this.ensureConfigured(),this.eventQueue.flush()}getPendingEventCount(){return this.ensureConfigured(),this.eventQueue.getPendingCount()}handleSmartLink(e){return this.ensureConfigured(),this.attribution.parseDeepLink(e)}addDeepLinkListener(e){return this.ensureConfigured(),this.attribution.addDeepLinkListener(e)}removeAllListeners(){this.attribution&&this.attribution.removeAllListeners()}setUserID(e){c(_,e),this.logger?.debug("User ID set:",e)}getUserID(){return h(_)}clearUserID(){O(_),this.logger?.debug("User ID cleared")}getVisitorID(){let e=h(U);return e||(e=l(),c(U,e),this.logger?.debug("New visitor ID generated:",e)),e}resetVisitorID(){let e=l();return c(U,e),this.logger?.debug("Visitor ID reset:",e),e}startSession(){return this.ensureConfigured(),this.session.startSession()}endSession(){this.ensureConfigured(),this.trackTimeOnPage(),this.session.endSession()}getSessionId(){return this.ensureConfigured(),this.session.getSessionId()}setTrackingEnabled(e){c(M,String(e)),this.logger?.info("Tracking",e?"enabled":"disabled")}isTrackingEnabled(){return h(M)!=="false"}setupScrollDepthTracking(){typeof window>"u"||typeof document>"u"||(this.maxScrollDepth=0,this.scrollDepthHandler=()=>{let e=window.scrollY||document.documentElement.scrollTop,t=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)-window.innerHeight;if(t<=0)return;let n=Math.round(e/t*100);n>this.maxScrollDepth&&(this.maxScrollDepth=n)},window.addEventListener("scroll",this.scrollDepthHandler,{passive:!0}),window.addEventListener("beforeunload",()=>{if(this.maxScrollDepth>0&&this.isTrackingEnabled()){let e=this.buildPayload("custom","scroll_depth",{max_depth_percent:this.maxScrollDepth,page_url:window.location.href});this.network?.sendBeacon([e])}}))}trackTimeOnPage(){if(!this.pageEntryTime||!this.isTrackingEnabled())return;let e=Math.round((Date.now()-this.pageEntryTime)/1e3);if(e>0){let t=this.buildPayload("custom","time_on_page",{duration_seconds:e,page_url:typeof window<"u"?window.location.href:""});this.eventQueue?.enqueue(t)}}setupOutboundClickTracking(){typeof document>"u"||typeof window>"u"||(this.outboundClickHandler=e=>{let t=e.target?.closest?.("a");if(t?.href)try{let n=new URL(t.href).hostname,i=window.location.hostname;if(n!==i&&this.isTrackingEnabled()){let o=this.buildPayload("custom","outbound_click",{destination_url:t.href,link_text:t.textContent?.trim()?.substring(0,100)??"",page_url:window.location.href});this.eventQueue?.enqueue(o)}}catch{}},document.addEventListener("click",this.outboundClickHandler))}destroy(){this.trackTimeOnPage(),this.scrollDepthHandler&&typeof window<"u"&&(window.removeEventListener("scroll",this.scrollDepthHandler),this.scrollDepthHandler=null),this.outboundClickHandler&&typeof document<"u"&&(document.removeEventListener("click",this.outboundClickHandler),this.outboundClickHandler=null),this.eventQueue?.destroy(),this.session?.destroy(),this.attribution?.destroy(),this.isConfigured=!1,this.logger?.info("SDK destroyed")}ensureConfigured(){if(!this.isConfigured||!this.config)throw new Error("LinkzlySDK is not configured. Call LinkzlySDK.configure() first.")}buildPayload(e,t,n){if(!this.config)throw new Error("SDK not configured");if(!this.isTrackingEnabled())return{type:"sdk_event",sdkKey:this.config.sdkKey,eventType:e,eventName:t,appUserId:null,sessionId:"",visitorId:"",platform:"web",deviceFingerprint:{},deepLinkMatched:!1,deepLinkData:null,customData:null,utmSource:null,utmMedium:null,utmCampaign:null,utmTerm:null,utmContent:null,smartLinkId:null,clickId:null,timestamp:Date.now()};let i=N(),o=this.attribution.getUTM(),a=this.attribution.getAttributionData();return{type:"sdk_event",sdkKey:this.config.sdkKey,eventType:e,eventName:t,appUserId:this.getUserID(),sessionId:this.session.getSessionId(),visitorId:this.getVisitorID(),platform:"web",deviceFingerprint:z(i),deepLinkMatched:!!(a.smartLinkId||a.clickId),deepLinkData:a.smartLinkId||a.clickId?{...a.smartLinkId?{slid:a.smartLinkId}:{},...a.clickId?{cid:a.clickId}:{}}:null,customData:n??null,utmSource:o.utmSource,utmMedium:o.utmMedium,utmCampaign:o.utmCampaign,utmTerm:o.utmTerm,utmContent:o.utmContent,smartLinkId:a.smartLinkId,clickId:a.clickId,timestamp:Date.now()}}isDNTEnabled(){return typeof navigator>"u"?!1:navigator.doNotTrack==="1"||navigator.globalPrivacyControl===!0}};var s=new k,Te=s.configure.bind(s),Le=s.trackFirstVisit.bind(s),Ie=s.trackOpen.bind(s),Ee=s.trackPageView.bind(s),De=s.trackEvent.bind(s),Pe=s.trackPurchase.bind(s),xe=s.trackEventBatch.bind(s),Re=s.flushEvents.bind(s),Ce=s.getPendingEventCount.bind(s),Ue=s.handleSmartLink.bind(s),_e=s.addDeepLinkListener.bind(s),Me=s.removeAllListeners.bind(s),Ae=s.setUserID.bind(s),Ne=s.getUserID.bind(s),ze=s.clearUserID.bind(s),Oe=s.getVisitorID.bind(s),qe=s.resetVisitorID.bind(s),Be=s.startSession.bind(s),He=s.endSession.bind(s),Fe=s.getSessionId.bind(s),Ve=s.setTrackingEnabled.bind(s),$e=s.isTrackingEnabled.bind(s),Ke=s.destroy.bind(s),Qe=s;export{v as Environment,k as LinkzlyWebSDK,_e as addDeepLinkListener,ze as clearUserID,Te as configure,Qe as default,Ke as destroy,He as endSession,Re as flushEvents,Ce as getPendingEventCount,Fe as getSessionId,Ne as getUserID,Oe as getVisitorID,Ue as handleSmartLink,$e as isTrackingEnabled,Me as removeAllListeners,qe as resetVisitorID,Ve as setTrackingEnabled,Ae as setUserID,Be as startSession,De as trackEvent,xe as trackEventBatch,Le as trackFirstVisit,Ie as trackOpen,Ee as trackPageView,Pe as trackPurchase};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@linkzly/web-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Full-featured browser SDK for Linkzly — event tracking, attribution, sessions, deep linking, and affiliate tracking",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --minify",
|
|
22
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.4.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"linkzly",
|
|
31
|
+
"analytics",
|
|
32
|
+
"attribution",
|
|
33
|
+
"tracking",
|
|
34
|
+
"deep-linking",
|
|
35
|
+
"affiliate",
|
|
36
|
+
"events",
|
|
37
|
+
"sessions",
|
|
38
|
+
"web-sdk"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/linkzly/linkzly-sdks"
|
|
44
|
+
}
|
|
45
|
+
}
|