userpath-js 0.0.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +609 -0
  2. package/dist/index.js +997 -0
  3. package/dist/server/index.js +45 -0
  4. package/dist/types/client/src/base/api.d.ts +12 -0
  5. package/dist/types/client/src/base/api.d.ts.map +1 -0
  6. package/dist/types/client/src/base/events.d.ts +184 -0
  7. package/dist/types/client/src/base/events.d.ts.map +1 -0
  8. package/dist/types/client/src/base/session.d.ts +68 -0
  9. package/dist/types/client/src/base/session.d.ts.map +1 -0
  10. package/dist/types/client/src/index.d.ts +57 -0
  11. package/dist/types/client/src/index.d.ts.map +1 -0
  12. package/dist/types/client/src/pixel.d.ts +2 -0
  13. package/dist/types/client/src/pixel.d.ts.map +1 -0
  14. package/dist/types/client/src/schemas/config.d.ts +32 -0
  15. package/dist/types/client/src/schemas/config.d.ts.map +1 -0
  16. package/dist/types/client/src/schemas/events.d.ts +58 -0
  17. package/dist/types/client/src/schemas/events.d.ts.map +1 -0
  18. package/dist/types/client/src/schemas/identity.d.ts +7 -0
  19. package/dist/types/client/src/schemas/identity.d.ts.map +1 -0
  20. package/dist/types/client/src/server/index.d.ts +42 -0
  21. package/dist/types/client/src/server/index.d.ts.map +1 -0
  22. package/dist/types/core/src/browser/index.d.ts +4 -0
  23. package/dist/types/core/src/browser/index.d.ts.map +1 -0
  24. package/dist/types/core/src/browser/libs/LocalStorage.d.ts +11 -0
  25. package/dist/types/core/src/browser/libs/LocalStorage.d.ts.map +1 -0
  26. package/dist/types/core/src/browser/libs/cookie.d.ts +27 -0
  27. package/dist/types/core/src/browser/libs/cookie.d.ts.map +1 -0
  28. package/dist/types/core/src/browser/libs/memory.d.ts +4 -0
  29. package/dist/types/core/src/browser/libs/memory.d.ts.map +1 -0
  30. package/dist/types/src/base/api.d.ts +12 -0
  31. package/dist/types/src/base/api.d.ts.map +1 -0
  32. package/dist/types/src/base/events.d.ts +184 -0
  33. package/dist/types/src/base/events.d.ts.map +1 -0
  34. package/dist/types/src/base/session.d.ts +64 -0
  35. package/dist/types/src/base/session.d.ts.map +1 -0
  36. package/dist/types/src/index.d.ts +57 -0
  37. package/dist/types/src/index.d.ts.map +1 -0
  38. package/dist/types/src/pixel.d.ts +2 -0
  39. package/dist/types/src/pixel.d.ts.map +1 -0
  40. package/dist/types/src/schemas/config.d.ts +32 -0
  41. package/dist/types/src/schemas/config.d.ts.map +1 -0
  42. package/dist/types/src/schemas/events.d.ts +58 -0
  43. package/dist/types/src/schemas/events.d.ts.map +1 -0
  44. package/dist/types/src/schemas/identity.d.ts +7 -0
  45. package/dist/types/src/schemas/identity.d.ts.map +1 -0
  46. package/dist/types/src/server/index.d.ts +42 -0
  47. package/dist/types/src/server/index.d.ts.map +1 -0
  48. package/package.json +26 -4
  49. package/dist/pixel.js +0 -1
  50. package/examples/usage.ts +0 -83
  51. package/src/clients/api.ts +0 -92
  52. package/src/clients/events.ts +0 -55
  53. package/src/clients/identity.ts +0 -54
  54. package/src/clients/session.ts +0 -47
  55. package/src/index.ts +0 -72
  56. package/src/pixel.ts +0 -28
  57. package/src/schemas/config.ts +0 -4
  58. package/src/schemas/events.ts +0 -31
  59. package/src/schemas/identity.ts +0 -6
package/dist/index.js ADDED
@@ -0,0 +1,997 @@
1
+ // package.json
2
+ var package_default = {
3
+ name: "userpath-js",
4
+ version: "0.0.0",
5
+ private: false,
6
+ main: "dist/index.js",
7
+ types: "dist/types/src/index.d.ts",
8
+ exports: {
9
+ ".": {
10
+ types: "./dist/types/src/index.d.ts",
11
+ default: "./dist/index.js"
12
+ },
13
+ "./server": {
14
+ types: "./dist/types/src/server/index.d.ts",
15
+ default: "./dist/server/index.js"
16
+ }
17
+ },
18
+ files: [
19
+ "dist/index.js",
20
+ "dist/types",
21
+ "dist/server"
22
+ ],
23
+ scripts: {
24
+ dev: "bun build src/index.ts --outdir ./dist --watch",
25
+ types: "tsc",
26
+ build: "bun run build.client && bun run build.pixel && bun run build.server",
27
+ "build.prod": "bun run build.client.prod && bun run build.pixel.prod && bun run build.server.prod",
28
+ "build.client": "bun run types && bun build src/index.ts --outdir ./dist --no-cache",
29
+ "build.client.prod": "bun run types && bun build src/index.ts --outdir ./dist --minify",
30
+ "build.pixel": "bun build src/pixel.ts --outdir ./dist",
31
+ "build.pixel.prod": "bun build src/pixel.ts --outdir ./dist --minify",
32
+ "build.server": "bun run types && bun build src/server/index.ts --outdir ./dist/server",
33
+ "build.server.prod": "bun run types && bun build src/server/index.ts --outdir ./dist/server --minify",
34
+ prepublish: "bun run build.prod"
35
+ },
36
+ devDependencies: {
37
+ "bun-types": "latest",
38
+ typescript: "^5.0.0"
39
+ }
40
+ };
41
+
42
+ // src/schemas/config.ts
43
+ var DEFAULT_CONFIG = {
44
+ appId: "",
45
+ serverUrl: "https://api.userpath.co",
46
+ version: 1,
47
+ flushIntervalMs: 5000,
48
+ autoTrack: {
49
+ errors: true,
50
+ clicks: true,
51
+ scrolling: true,
52
+ forms: true,
53
+ videos: true,
54
+ pageVisibility: true,
55
+ pageInactivity: true
56
+ }
57
+ };
58
+
59
+ // src/base/api.ts
60
+ class ApiClient {
61
+ serverUrl;
62
+ appId;
63
+ version;
64
+ constructor(config) {
65
+ this.serverUrl = config.serverUrl || "";
66
+ this.appId = config.appId;
67
+ this.version = config.version || DEFAULT_CONFIG.version;
68
+ }
69
+ buildUrl(path) {
70
+ return `${this.serverUrl}/v${this.version}/${path}`;
71
+ }
72
+ }
73
+
74
+ // src/base/events.ts
75
+ var isServer = typeof window === "undefined";
76
+ var h = {
77
+ pushState: () => {},
78
+ replaceState: () => {},
79
+ go: () => {},
80
+ back: () => {},
81
+ forward: () => {}
82
+ };
83
+ if (isServer) {
84
+ global.history = h;
85
+ }
86
+ var w = {
87
+ location: {
88
+ href: ""
89
+ },
90
+ addEventListener: () => {},
91
+ removeEventListener: () => {},
92
+ scrollTo: () => {},
93
+ scrollBy: () => {},
94
+ setTimeout: () => {},
95
+ clearTimeout: () => {},
96
+ setInterval: () => {},
97
+ clearInterval: () => {}
98
+ };
99
+ if (isServer) {
100
+ global.window = w;
101
+ }
102
+ var d = {
103
+ hidden: false,
104
+ addEventListener: () => {},
105
+ removeEventListener: () => {},
106
+ querySelector: () => null,
107
+ closest: () => null,
108
+ getAttribute: () => null,
109
+ tagName: "",
110
+ textContent: "",
111
+ classList: {
112
+ contains: () => false
113
+ },
114
+ scrollHeight: 0,
115
+ offsetHeight: 0,
116
+ clientHeight: 0,
117
+ pageYOffset: 0,
118
+ referrer: "",
119
+ title: ""
120
+ };
121
+ if (isServer) {
122
+ global.document = d;
123
+ }
124
+
125
+ class EventsClient {
126
+ #session;
127
+ #trackedUrls;
128
+ #maxScroll;
129
+ #clickHandler;
130
+ #scrollHandler;
131
+ #resizeHandler;
132
+ #inputFocusHandler;
133
+ #inputBlurHandler;
134
+ #formChangeHandler;
135
+ #formSubmitHandler;
136
+ #videoPlayHandler;
137
+ #videoPauseHandler;
138
+ #videoEndHandler;
139
+ #visibilityChangeHandler;
140
+ #beforeUnloadHandler;
141
+ #scrollTimeout;
142
+ #currentUrl;
143
+ #formStartTimes;
144
+ #inputInitialValues;
145
+ #videoStartTimes;
146
+ #pageStartTime;
147
+ #pageVisibleTime;
148
+ #lastVisibilityChange;
149
+ #isPageVisible;
150
+ #lastActivityTime;
151
+ #inactivityTimeout;
152
+ #isUserInactive;
153
+ #activityHandler;
154
+ #inactivityThreshold = 60000;
155
+ #inactivityStartTime;
156
+ #errorHandlerInstalled = false;
157
+ #errorHandler;
158
+ #rejectionHandler;
159
+ #config;
160
+ constructor(session, config = DEFAULT_CONFIG) {
161
+ this.#session = session;
162
+ this.#config = config;
163
+ this.#trackedUrls = new Set;
164
+ this.#maxScroll = 0;
165
+ this.#scrollTimeout = null;
166
+ this.#currentUrl = window.location.href;
167
+ this.#formStartTimes = new Map;
168
+ this.#inputInitialValues = new WeakMap;
169
+ this.#videoStartTimes = new WeakMap;
170
+ this.#pageStartTime = Date.now();
171
+ this.#pageVisibleTime = 0;
172
+ this.#lastVisibilityChange = this.#pageStartTime;
173
+ this.#isPageVisible = document.hidden;
174
+ this.#lastActivityTime = Date.now();
175
+ this.#inactivityTimeout = null;
176
+ this.#isUserInactive = false;
177
+ this.#inactivityStartTime = null;
178
+ this.#clickHandler = this.handleClick.bind(this);
179
+ this.#scrollHandler = this.handleScroll.bind(this);
180
+ this.#resizeHandler = this.handleScroll.bind(this);
181
+ this.#inputFocusHandler = this.handleInputFocus.bind(this);
182
+ this.#inputBlurHandler = this.handleInputBlur.bind(this);
183
+ this.#formChangeHandler = this.handleFormChange.bind(this);
184
+ this.#formSubmitHandler = this.handleFormSubmit.bind(this);
185
+ this.#videoPlayHandler = this.handleVideoPlay.bind(this);
186
+ this.#videoPauseHandler = this.handleVideoPause.bind(this);
187
+ this.#videoEndHandler = this.handleVideoEnd.bind(this);
188
+ this.#visibilityChangeHandler = this.handleVisibilityChange.bind(this);
189
+ this.#beforeUnloadHandler = this.handleBeforeUnload.bind(this);
190
+ this.#activityHandler = this.handleUserActivity.bind(this);
191
+ this.#errorHandler = this.handleError.bind(this);
192
+ this.#rejectionHandler = this.handleRejection.bind(this);
193
+ const autoTrack = this.#config.autoTrack || DEFAULT_CONFIG.autoTrack;
194
+ if (autoTrack.clicks !== false) {
195
+ this.setupClickTracking();
196
+ }
197
+ if (autoTrack.scrolling !== false) {
198
+ this.setupScrollTracking();
199
+ }
200
+ if (autoTrack.forms !== false) {
201
+ this.setupFormTracking();
202
+ }
203
+ if (autoTrack.videos !== false) {
204
+ this.setupVideoTracking();
205
+ }
206
+ if (autoTrack.pageVisibility !== false) {
207
+ this.setupPageTimeTracking();
208
+ }
209
+ if (autoTrack.pageInactivity !== false) {
210
+ this.setupInactivityTracking();
211
+ }
212
+ }
213
+ destroy() {
214
+ document.removeEventListener("click", this.#clickHandler);
215
+ window.removeEventListener("scroll", this.#scrollHandler);
216
+ window.removeEventListener("resize", this.#resizeHandler);
217
+ document.removeEventListener("focus", this.#inputFocusHandler, true);
218
+ document.removeEventListener("blur", this.#inputBlurHandler, true);
219
+ document.removeEventListener("change", this.#formChangeHandler, true);
220
+ document.removeEventListener("submit", this.#formSubmitHandler, true);
221
+ document.removeEventListener("play", this.#videoPlayHandler, true);
222
+ document.removeEventListener("pause", this.#videoPauseHandler, true);
223
+ document.removeEventListener("ended", this.#videoEndHandler, true);
224
+ document.removeEventListener("visibilitychange", this.#visibilityChangeHandler);
225
+ window.removeEventListener("beforeunload", this.#beforeUnloadHandler);
226
+ if (this.#scrollTimeout) {
227
+ window.clearTimeout(this.#scrollTimeout);
228
+ }
229
+ ["mousemove", "mousedown", "keydown", "touchstart", "scroll"].forEach((eventType) => {
230
+ document.removeEventListener(eventType, this.#activityHandler, true);
231
+ });
232
+ if (this.#inactivityTimeout) {
233
+ window.clearTimeout(this.#inactivityTimeout);
234
+ }
235
+ this.uninstallErrorHandler();
236
+ }
237
+ setupClickTracking() {
238
+ document.addEventListener("click", this.#clickHandler);
239
+ }
240
+ handleClick(event) {
241
+ const element = event.target;
242
+ if (!element)
243
+ return;
244
+ const label = this.extractClickLabel(element);
245
+ if (!label)
246
+ return;
247
+ this.trackCustomEvent("click", {
248
+ label,
249
+ element: element.tagName.toLowerCase(),
250
+ id: element.id || undefined
251
+ });
252
+ }
253
+ handleScroll() {
254
+ const newUrl = window.location.href;
255
+ if (newUrl !== this.#currentUrl) {
256
+ this.#currentUrl = newUrl;
257
+ this.#maxScroll = 0;
258
+ this.#trackedUrls.clear();
259
+ }
260
+ if (this.#scrollTimeout) {
261
+ window.clearTimeout(this.#scrollTimeout);
262
+ }
263
+ this.#scrollTimeout = window.setTimeout(() => {
264
+ const scrollDepth = this.calculateScrollDepth();
265
+ if (scrollDepth > this.#maxScroll) {
266
+ this.#maxScroll = scrollDepth;
267
+ this.checkAndTrackScrollDepth(scrollDepth);
268
+ }
269
+ }, 100);
270
+ }
271
+ setupScrollTracking() {
272
+ window.addEventListener("scroll", this.#scrollHandler);
273
+ window.addEventListener("resize", this.#resizeHandler);
274
+ }
275
+ calculateScrollDepth() {
276
+ const windowHeight = window.innerHeight;
277
+ const documentHeight = Math.max(document.documentElement.scrollHeight, document.documentElement.offsetHeight, document.documentElement.clientHeight);
278
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
279
+ return Math.round((scrollTop + windowHeight) / documentHeight * 100);
280
+ }
281
+ checkAndTrackScrollDepth(currentDepth) {
282
+ const url = window.location.href;
283
+ if (currentDepth > 50 && !this.#trackedUrls.has(url)) {
284
+ this.#trackedUrls.add(url);
285
+ this.trackCustomEvent("scroll");
286
+ }
287
+ }
288
+ extractClickLabel(element) {
289
+ const ariaLabel = element.getAttribute("aria-label");
290
+ if (ariaLabel)
291
+ return `${ariaLabel}`;
292
+ if (element instanceof HTMLButtonElement || element instanceof HTMLAnchorElement) {
293
+ const text = element.textContent?.trim();
294
+ if (text)
295
+ return `${text}`;
296
+ }
297
+ if (element instanceof HTMLInputElement) {
298
+ if (element.type === "submit" && element.value) {
299
+ return `${element.value}`;
300
+ }
301
+ if (element.type === "checkbox") {
302
+ const label = this.findInputLabel(element);
303
+ const action = element.checked ? "Checked" : "Unchecked";
304
+ return label ? `${action} ${label}` : null;
305
+ }
306
+ }
307
+ const labelElement = element.closest("label");
308
+ if (labelElement) {
309
+ const text = labelElement.textContent?.trim();
310
+ if (text)
311
+ return `${text}`;
312
+ }
313
+ return null;
314
+ }
315
+ findInputLabel(input) {
316
+ const ariaLabel = input.getAttribute("aria-label");
317
+ if (ariaLabel)
318
+ return ariaLabel;
319
+ if (input.id) {
320
+ const label = document.querySelector(`label[for="${input.id}"]`);
321
+ if (label && label.textContent)
322
+ return label.textContent.trim();
323
+ }
324
+ const parentLabel = input.closest("label");
325
+ if (parentLabel && parentLabel.textContent) {
326
+ const labelText = parentLabel.textContent.trim();
327
+ return labelText.replace(input.value, "").trim();
328
+ }
329
+ return null;
330
+ }
331
+ trackPageView() {
332
+ const event = {
333
+ name: "page_view",
334
+ url: window.location.href,
335
+ referrer: document.referrer,
336
+ title: document.title,
337
+ timestamp: new Date().toISOString()
338
+ };
339
+ this.#session.track(event);
340
+ }
341
+ trackCustomEvent(name, properties = {}, options = {
342
+ immediate: false
343
+ }) {
344
+ const event = {
345
+ name,
346
+ url: window.location.href,
347
+ referrer: document.referrer,
348
+ title: document.title,
349
+ properties,
350
+ timestamp: new Date().toISOString()
351
+ };
352
+ if (options.immediate) {
353
+ this.#session.publish([event]);
354
+ } else {
355
+ this.#session.track(event);
356
+ }
357
+ }
358
+ trackPurchase(params) {
359
+ const event = {
360
+ name: "purchase",
361
+ url: window.location.href,
362
+ referrer: document.referrer,
363
+ title: document.title,
364
+ productId: params.productId,
365
+ price: params.price,
366
+ currency: params.currency,
367
+ properties: {
368
+ ...params.properties
369
+ },
370
+ timestamp: new Date().toISOString()
371
+ };
372
+ this.#session.track(event);
373
+ }
374
+ setupFormTracking() {
375
+ document.addEventListener("focus", this.#inputFocusHandler, true);
376
+ document.addEventListener("blur", this.#inputBlurHandler, true);
377
+ document.addEventListener("change", this.#formChangeHandler, true);
378
+ document.addEventListener("submit", this.#formSubmitHandler, true);
379
+ }
380
+ handleInputFocus(event) {
381
+ const element = event.target;
382
+ if (!element || !(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)) {
383
+ return;
384
+ }
385
+ if (element instanceof HTMLInputElement && element.type === "password") {
386
+ return;
387
+ }
388
+ this.#inputInitialValues.set(element, this.getElementValue(element));
389
+ const form = element.closest("form");
390
+ if (form && !this.#formStartTimes.has(form)) {
391
+ this.#formStartTimes.set(form, Date.now());
392
+ }
393
+ }
394
+ getElementValue(element) {
395
+ if (element instanceof HTMLSelectElement) {
396
+ return element.value;
397
+ }
398
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
399
+ return element.value;
400
+ }
401
+ return "";
402
+ }
403
+ handleInputBlur(event) {
404
+ const element = event.target;
405
+ if (!element || !(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)) {
406
+ return;
407
+ }
408
+ if (element instanceof HTMLInputElement && element.type === "password") {
409
+ return;
410
+ }
411
+ const initialValue = this.#inputInitialValues.get(element) || "";
412
+ const currentValue = this.getElementValue(element);
413
+ if (currentValue !== initialValue && currentValue.trim() !== "") {
414
+ const inputType = element instanceof HTMLSelectElement ? "select" : element.type;
415
+ const label = this.findFormElementLabel(element) || this.getInputIdentifier(element);
416
+ if (!label)
417
+ return;
418
+ this.trackCustomEvent("input_fill", {
419
+ input_type: inputType,
420
+ label,
421
+ form_name: this.getFormName(element)
422
+ });
423
+ }
424
+ this.#inputInitialValues.delete(element);
425
+ }
426
+ getFormName(element) {
427
+ const form = element.closest("form");
428
+ if (!form)
429
+ return;
430
+ return form.getAttribute("name") || form.getAttribute("id") || form.getAttribute("aria-label") || undefined;
431
+ }
432
+ getInputIdentifier(element) {
433
+ return element.getAttribute("name") || element.getAttribute("id") || element.getAttribute("placeholder") || null;
434
+ }
435
+ findFormElementLabel(element) {
436
+ const ariaLabel = element.getAttribute("aria-label");
437
+ if (ariaLabel)
438
+ return ariaLabel;
439
+ if (element.id) {
440
+ const label = document.querySelector(`label[for="${element.id}"]`);
441
+ if (label && label.textContent)
442
+ return label.textContent.trim();
443
+ }
444
+ const parentLabel = element.closest("label");
445
+ if (parentLabel && parentLabel.textContent) {
446
+ const labelText = parentLabel.textContent.trim();
447
+ const value = element instanceof HTMLSelectElement ? element.selectedOptions[0]?.text : element.value;
448
+ return value ? labelText.replace(value, "").trim() : labelText;
449
+ }
450
+ return null;
451
+ }
452
+ handleFormChange(event) {
453
+ const element = event.target;
454
+ if (!element || !(element instanceof HTMLSelectElement || element instanceof HTMLInputElement)) {
455
+ return;
456
+ }
457
+ if (element instanceof HTMLSelectElement) {
458
+ this.trackSelectChange(element);
459
+ } else if (element instanceof HTMLInputElement) {
460
+ if (element.type === "radio") {
461
+ this.trackRadioChange(element);
462
+ } else if (element.type === "checkbox") {
463
+ this.trackCheckboxChange(element);
464
+ } else if (element.type === "range") {
465
+ this.trackRangeChange(element);
466
+ }
467
+ }
468
+ }
469
+ trackSelectChange(element) {
470
+ const label = this.findFormElementLabel(element) || this.getInputIdentifier(element);
471
+ if (!label)
472
+ return;
473
+ const selectedOption = element.selectedOptions[0];
474
+ this.trackCustomEvent("input_select", {
475
+ input_type: "select",
476
+ label,
477
+ option_label: selectedOption?.text || "",
478
+ form_name: this.getFormName(element)
479
+ });
480
+ }
481
+ trackRadioChange(element) {
482
+ const label = this.findFormElementLabel(element) || this.getInputIdentifier(element);
483
+ if (!label)
484
+ return;
485
+ const groupName = element.name || label;
486
+ this.trackCustomEvent("input_radio", {
487
+ input_type: "radio",
488
+ group: groupName,
489
+ label,
490
+ form_name: this.getFormName(element)
491
+ });
492
+ }
493
+ trackCheckboxChange(element) {
494
+ const label = this.findFormElementLabel(element) || this.getInputIdentifier(element);
495
+ if (!label)
496
+ return;
497
+ this.trackCustomEvent("input_checkbox", {
498
+ input_type: "checkbox",
499
+ label,
500
+ checked: element.checked,
501
+ form_name: this.getFormName(element)
502
+ });
503
+ }
504
+ trackRangeChange(element) {
505
+ const label = this.findFormElementLabel(element) || this.getInputIdentifier(element);
506
+ if (!label)
507
+ return;
508
+ this.trackCustomEvent("input_range", {
509
+ input_type: "range",
510
+ label,
511
+ form_name: this.getFormName(element)
512
+ });
513
+ }
514
+ handleFormSubmit(event) {
515
+ const form = event.target;
516
+ if (!form || !(form instanceof HTMLFormElement))
517
+ return;
518
+ const startTime = this.#formStartTimes.get(form) || Date.now();
519
+ const timeToComplete = Date.now() - startTime;
520
+ const formName = this.getFormName(form) || "unknown";
521
+ this.trackCustomEvent("form_submit", {
522
+ form_name: formName,
523
+ time_to_complete: timeToComplete
524
+ });
525
+ this.#formStartTimes.delete(form);
526
+ }
527
+ setupVideoTracking() {
528
+ document.addEventListener("play", this.#videoPlayHandler, true);
529
+ document.addEventListener("pause", this.#videoPauseHandler, true);
530
+ document.addEventListener("ended", this.#videoEndHandler, true);
531
+ }
532
+ getVideoMetadata(video) {
533
+ return {
534
+ duration: Math.round(video.duration),
535
+ current_time: Math.round(video.currentTime),
536
+ src: video.currentSrc || video.src || undefined,
537
+ title: video.title || undefined,
538
+ video_id: video.id || undefined
539
+ };
540
+ }
541
+ handleVideoPlay(event) {
542
+ const video = event.target;
543
+ if (!video || !(video instanceof HTMLVideoElement))
544
+ return;
545
+ this.#videoStartTimes.set(video, Date.now());
546
+ this.trackCustomEvent("video_play", this.getVideoMetadata(video));
547
+ }
548
+ handleVideoPause(event) {
549
+ const video = event.target;
550
+ if (!video || !(video instanceof HTMLVideoElement))
551
+ return;
552
+ const startTime = this.#videoStartTimes.get(video);
553
+ const metadata = this.getVideoMetadata(video);
554
+ if (startTime) {
555
+ metadata.watch_time = Math.round((Date.now() - startTime) / 1000);
556
+ this.#videoStartTimes.delete(video);
557
+ }
558
+ this.trackCustomEvent("video_pause", metadata);
559
+ }
560
+ handleVideoEnd(event) {
561
+ const video = event.target;
562
+ if (!video || !(video instanceof HTMLVideoElement))
563
+ return;
564
+ const startTime = this.#videoStartTimes.get(video);
565
+ const metadata = this.getVideoMetadata(video);
566
+ if (startTime) {
567
+ metadata.watch_time = Math.round((Date.now() - startTime) / 1000);
568
+ this.#videoStartTimes.delete(video);
569
+ }
570
+ this.trackCustomEvent("video_complete", metadata);
571
+ }
572
+ setupPageTimeTracking() {
573
+ document.addEventListener("visibilitychange", this.#visibilityChangeHandler);
574
+ window.addEventListener("beforeunload", this.#beforeUnloadHandler);
575
+ }
576
+ handleVisibilityChange() {
577
+ const now = Date.now();
578
+ const wasVisible = this.#isPageVisible;
579
+ this.#isPageVisible = !document.hidden;
580
+ if (wasVisible) {
581
+ this.#pageVisibleTime += now - this.#lastVisibilityChange;
582
+ }
583
+ this.#lastVisibilityChange = now;
584
+ this.trackCustomEvent("page_visibility", {
585
+ visible: this.#isPageVisible,
586
+ total_time: Math.round((now - this.#pageStartTime) / 1000),
587
+ visible_time: Math.round(this.#pageVisibleTime / 1000)
588
+ });
589
+ }
590
+ handleBeforeUnload() {
591
+ const now = Date.now();
592
+ if (this.#isPageVisible) {
593
+ this.#pageVisibleTime += now - this.#lastVisibilityChange;
594
+ }
595
+ this.trackCustomEvent("page_exit", {
596
+ total_time: Math.round((now - this.#pageStartTime) / 1000),
597
+ visible_time: Math.round(this.#pageVisibleTime / 1000)
598
+ }, {
599
+ immediate: true
600
+ });
601
+ }
602
+ setupInactivityTracking() {
603
+ ["mousemove", "mousedown", "keydown", "touchstart", "scroll"].forEach((eventType) => {
604
+ document.addEventListener(eventType, this.#activityHandler, true);
605
+ });
606
+ this.checkInactivity();
607
+ }
608
+ handleUserActivity() {
609
+ this.#lastActivityTime = Date.now();
610
+ if (this.#isUserInactive && this.#inactivityStartTime) {
611
+ this.#isUserInactive = false;
612
+ this.trackCustomEvent("user_active", {
613
+ inactive_duration: Math.round((Date.now() - this.#inactivityStartTime) / 1000),
614
+ url: window.location.href,
615
+ title: document.title
616
+ });
617
+ this.#inactivityStartTime = null;
618
+ }
619
+ this.checkInactivity();
620
+ }
621
+ checkInactivity() {
622
+ if (this.#inactivityTimeout) {
623
+ window.clearTimeout(this.#inactivityTimeout);
624
+ }
625
+ this.#inactivityTimeout = window.setTimeout(() => {
626
+ const now = Date.now();
627
+ const inactiveDuration = now - this.#lastActivityTime;
628
+ if (inactiveDuration >= this.#inactivityThreshold && !this.#isUserInactive) {
629
+ this.#isUserInactive = true;
630
+ this.#inactivityStartTime = now;
631
+ this.trackCustomEvent("user_inactive");
632
+ }
633
+ }, this.#inactivityThreshold);
634
+ }
635
+ installErrorHandler() {
636
+ if (this.#errorHandlerInstalled || typeof window === "undefined") {
637
+ return;
638
+ }
639
+ window.addEventListener("error", this.#errorHandler);
640
+ window.addEventListener("unhandledrejection", this.#rejectionHandler);
641
+ this.#errorHandlerInstalled = true;
642
+ }
643
+ uninstallErrorHandler() {
644
+ if (!this.#errorHandlerInstalled || typeof window === "undefined") {
645
+ return;
646
+ }
647
+ window.removeEventListener("error", this.#errorHandler);
648
+ window.removeEventListener("unhandledrejection", this.#rejectionHandler);
649
+ this.#errorHandlerInstalled = false;
650
+ }
651
+ isErrorHandlerInstalled() {
652
+ return this.#errorHandlerInstalled;
653
+ }
654
+ handleError(event) {
655
+ if (event instanceof ErrorEvent) {
656
+ this.trackError(new Error(event.message), {
657
+ filename: event.filename,
658
+ lineno: event.lineno,
659
+ colno: event.colno,
660
+ source: "window.onerror"
661
+ });
662
+ }
663
+ }
664
+ handleRejection(event) {
665
+ const error = event.reason instanceof Error ? event.reason : new Error(String(event.reason || "Unhandled Promise rejection"));
666
+ this.trackError(error, { source: "unhandledrejection" });
667
+ }
668
+ trackError(error, properties = {}, options = {
669
+ immediate: false
670
+ }) {
671
+ const {
672
+ filename,
673
+ lineno,
674
+ colno,
675
+ source,
676
+ url,
677
+ referrer,
678
+ userAgent,
679
+ ...cleanProperties
680
+ } = properties;
681
+ const event = {
682
+ name: "error",
683
+ message: error.message,
684
+ stack: error.stack,
685
+ source: source || "client",
686
+ lineno,
687
+ colno,
688
+ filename,
689
+ properties: cleanProperties,
690
+ timestamp: new Date().toISOString(),
691
+ url: url || window.location.href,
692
+ referrer: referrer || document.referrer,
693
+ title: document.title
694
+ };
695
+ if (options.immediate) {
696
+ this.#session.publish([event]);
697
+ } else {
698
+ this.#session.track(event);
699
+ }
700
+ }
701
+ }
702
+
703
+ // ../core/src/browser/libs/LocalStorage.ts
704
+ class LocalStorage {
705
+ namespace;
706
+ constructor({ namespace }) {
707
+ this.namespace = namespace;
708
+ }
709
+ getItem(key) {
710
+ const item = localStorage.getItem(`${this.namespace}.${key}`) || "";
711
+ try {
712
+ return JSON.parse(item);
713
+ } catch (_) {
714
+ return null;
715
+ }
716
+ }
717
+ setItem(key, value) {
718
+ localStorage.setItem(`${this.namespace}.${key}`, JSON.stringify(value));
719
+ }
720
+ removeItem(key) {
721
+ localStorage.removeItem(`${this.namespace}.${key}`);
722
+ }
723
+ }
724
+ // ../core/src/browser/libs/memory.ts
725
+ var memory = new LocalStorage({ namespace: "userpath" });
726
+ // ../core/src/browser/libs/cookie.ts
727
+ function setCookie(name, value, options = {}) {
728
+ const { days = 7, path = "/", domain, secure, sameSite = "strict" } = options;
729
+ const expires = days ? new Date(Date.now() + days * 86400000).toUTCString() : "";
730
+ let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
731
+ if (expires)
732
+ cookieString += `; expires=${expires}`;
733
+ if (path)
734
+ cookieString += `; path=${path}`;
735
+ if (domain)
736
+ cookieString += `; domain=${domain}`;
737
+ if (secure)
738
+ cookieString += "; secure";
739
+ if (sameSite)
740
+ cookieString += `; samesite=${sameSite}`;
741
+ document.cookie = cookieString;
742
+ }
743
+ function getCookie(name) {
744
+ const nameEQ = encodeURIComponent(name) + "=";
745
+ const cookies = document.cookie.split(";");
746
+ for (let i = 0;i < cookies.length; i++) {
747
+ let cookie = cookies[i];
748
+ while (cookie.charAt(0) === " ") {
749
+ cookie = cookie.substring(1, cookie.length);
750
+ }
751
+ if (cookie.indexOf(nameEQ) === 0) {
752
+ return decodeURIComponent(cookie.substring(nameEQ.length, cookie.length));
753
+ }
754
+ }
755
+ return null;
756
+ }
757
+ // src/base/session.ts
758
+ class SessionClient {
759
+ #api;
760
+ #queue = [];
761
+ #events = null;
762
+ #userId;
763
+ #appId;
764
+ #currentUrl = "";
765
+ #flushInterval = null;
766
+ #batchFlushTimeout = null;
767
+ #flushIntervalMs = 5000;
768
+ constructor(api, config) {
769
+ this.#api = api;
770
+ this.#userId = config?.userId;
771
+ this.#appId = config?.appId || "";
772
+ this.#flushIntervalMs = config?.flushIntervalMs || DEFAULT_CONFIG.flushIntervalMs || 5000;
773
+ }
774
+ init(events) {
775
+ this.#events = events;
776
+ this.setup();
777
+ }
778
+ destroy() {
779
+ if (this.#flushInterval) {
780
+ window.clearInterval(this.#flushInterval);
781
+ }
782
+ if (this.#batchFlushTimeout) {
783
+ window.clearTimeout(this.#batchFlushTimeout);
784
+ }
785
+ if (this.#queue.length > 0) {
786
+ this.flush();
787
+ }
788
+ }
789
+ setup() {
790
+ if (typeof window === "undefined")
791
+ return;
792
+ this.#currentUrl = window.location.href;
793
+ this.#events.trackPageView();
794
+ this.setupHistoryChange();
795
+ this.#flushInterval = window.setInterval(() => this.flush(), this.#flushIntervalMs);
796
+ }
797
+ setupHistoryChange() {
798
+ if (!this.#events)
799
+ return;
800
+ const originalPushState = history.pushState;
801
+ const originalReplaceState = history.replaceState;
802
+ history.pushState = (...args) => {
803
+ originalPushState.apply(history, args);
804
+ this.trackPageViewIfUrlChanged();
805
+ };
806
+ history.replaceState = (...args) => {
807
+ originalReplaceState.apply(history, args);
808
+ this.trackPageViewIfUrlChanged();
809
+ };
810
+ window.addEventListener("popstate", () => {
811
+ this.trackPageViewIfUrlChanged();
812
+ });
813
+ }
814
+ trackPageViewIfUrlChanged() {
815
+ const newUrl = window.location.href;
816
+ if (newUrl !== this.#currentUrl) {
817
+ this.#currentUrl = newUrl;
818
+ this.#events.trackPageView();
819
+ }
820
+ }
821
+ getUserId() {
822
+ return this.#userId;
823
+ }
824
+ setUserId(userId) {
825
+ this.#userId = userId;
826
+ }
827
+ getAppId() {
828
+ return this.#appId;
829
+ }
830
+ setAppId(appId) {
831
+ this.#appId = appId;
832
+ }
833
+ track(event) {
834
+ this.#queue.push(event);
835
+ if (this.#queue.length >= 20) {
836
+ this.flush();
837
+ return;
838
+ }
839
+ if (!this.#batchFlushTimeout) {
840
+ this.#batchFlushTimeout = window.setTimeout(() => {
841
+ this.flush();
842
+ this.#batchFlushTimeout = null;
843
+ }, this.#flushIntervalMs);
844
+ }
845
+ }
846
+ async identify(identity) {
847
+ this.setUserId(identity.id);
848
+ await fetch(this.#api.buildUrl("identify"), {
849
+ method: "POST",
850
+ headers: {
851
+ "Content-Type": "application/json",
852
+ "X-UserPath-App": this.#appId,
853
+ "X-UserPath-Session": this.getSessionId(),
854
+ ...this.#userId && { "X-UserPath-User": this.#userId }
855
+ },
856
+ body: JSON.stringify(identity),
857
+ keepalive: true
858
+ });
859
+ }
860
+ deduplicateEvents(events) {
861
+ const seen = new Set;
862
+ return events.filter((event) => {
863
+ const key = `${event.name}-${event.timestamp}`;
864
+ if (seen.has(key)) {
865
+ return false;
866
+ }
867
+ seen.add(key);
868
+ return true;
869
+ });
870
+ }
871
+ flush() {
872
+ if (this.#queue.length === 0)
873
+ return;
874
+ if (this.#batchFlushTimeout) {
875
+ window.clearTimeout(this.#batchFlushTimeout);
876
+ this.#batchFlushTimeout = null;
877
+ }
878
+ const events = [...this.#queue];
879
+ this.#queue = [];
880
+ const uniqueEvents = this.deduplicateEvents(events);
881
+ this.publish(uniqueEvents).catch((error) => {
882
+ this.#queue = [...uniqueEvents, ...this.#queue].slice(0, 100);
883
+ console.error("Error sending analytics data:", error);
884
+ });
885
+ }
886
+ async publish(events) {
887
+ return fetch(this.#api.buildUrl("log"), {
888
+ method: "POST",
889
+ headers: {
890
+ "Content-Type": "application/json",
891
+ "X-UserPath-App": this.#appId,
892
+ "X-UserPath-Session": this.getSessionId(),
893
+ ...this.#userId && { "X-UserPath-User": this.#userId }
894
+ },
895
+ body: JSON.stringify({
896
+ userAgent: navigator.userAgent,
897
+ screenWidth: window.screen.width,
898
+ screenHeight: window.screen.height,
899
+ language: navigator.language,
900
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
901
+ events: events.map((event) => {
902
+ if (!event.timestamp) {
903
+ event.timestamp = new Date().toISOString();
904
+ }
905
+ return {
906
+ ...event,
907
+ name: event.name || event.type,
908
+ properties: event.properties || {}
909
+ };
910
+ })
911
+ }),
912
+ credentials: "include",
913
+ keepalive: true
914
+ });
915
+ }
916
+ getSessionId() {
917
+ let sessionId = getCookie("userpath.session_id");
918
+ if (!sessionId) {
919
+ sessionId = this.generateId();
920
+ setCookie("userpath.session_id", sessionId, { days: 365 });
921
+ }
922
+ return sessionId;
923
+ }
924
+ generateId() {
925
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
926
+ const r = Math.random() * 16 | 0;
927
+ const v = c === "x" ? r : r & 3 | 8;
928
+ return v.toString(16);
929
+ });
930
+ }
931
+ }
932
+
933
+ // src/index.ts
934
+ class UserPath {
935
+ #api;
936
+ #session;
937
+ #events;
938
+ #version = DEFAULT_CONFIG.version;
939
+ constructor(config2) {
940
+ this.#api = new ApiClient(config2);
941
+ this.#session = new SessionClient(this.#api, config2);
942
+ this.#events = new EventsClient(this.#session, config2);
943
+ this.#version = config2.version || this.#version;
944
+ this.#session.init(this.#events);
945
+ this.#session.setAppId(config2.appId);
946
+ const shouldTrackErrors = config2.autoTrack?.errors !== undefined ? config2.autoTrack.errors : true;
947
+ if (shouldTrackErrors) {
948
+ this.installErrorHandler();
949
+ }
950
+ console.debug(`UserPath v${package_default.version} initialized`);
951
+ }
952
+ getAppId() {
953
+ return this.#session.getAppId();
954
+ }
955
+ setAppId(appId) {
956
+ this.#session.setAppId(appId);
957
+ }
958
+ installErrorHandler() {
959
+ this.#events.installErrorHandler();
960
+ }
961
+ uninstallErrorHandler() {
962
+ this.#events.uninstallErrorHandler();
963
+ }
964
+ trackError(error, properties = {}) {
965
+ if (!this.#events.isErrorHandlerInstalled() && properties.source !== "manual") {
966
+ return;
967
+ }
968
+ this.#events.trackError(error, properties);
969
+ }
970
+ setUserId(userId) {
971
+ this.#session.setUserId(userId);
972
+ }
973
+ getUserId() {
974
+ return this.#session.getUserId();
975
+ }
976
+ async identify(identity2) {
977
+ await this.#session.identify(identity2);
978
+ }
979
+ track(eventType, params) {
980
+ if (eventType === "page_view") {
981
+ this.#events.trackPageView();
982
+ } else if (eventType === "event") {
983
+ this.#events.trackCustomEvent(params.name, params.properties || {});
984
+ } else if (eventType === "purchase") {
985
+ this.#events.trackPurchase({
986
+ productId: params.properties?.product_id || "",
987
+ price: params.price,
988
+ currency: params.currency,
989
+ properties: params.properties
990
+ });
991
+ }
992
+ }
993
+ }
994
+ export {
995
+ UserPath,
996
+ DEFAULT_CONFIG
997
+ };