@zaplier/sdk 1.4.2 → 1.6.2

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/dist/index.cjs CHANGED
@@ -19016,14 +19016,20 @@ var n;
19016
19016
  * Based on official rrweb example for maximum compatibility
19017
19017
  */
19018
19018
  class SessionReplayEngine {
19019
- constructor(sessionId, config = {}) {
19019
+ constructor(sessionId, visitorId, config = {}) {
19020
19020
  this.events = [];
19021
19021
  this.isActive = false;
19022
+ this.isPaused = false;
19023
+ this.lastActivityTime = Date.now();
19024
+ this.sessionStartTime = Date.now();
19022
19025
  this.sessionId = sessionId;
19026
+ this.visitorId = visitorId;
19023
19027
  this.config = {
19024
19028
  enabled: true,
19025
19029
  sampleRate: 1.0,
19026
19030
  batchInterval: 10000, // 10 seconds like official example
19031
+ inactivityTimeout: 30000, // 30 seconds of inactivity
19032
+ pauseOnInactive: true, // Pause recording during inactivity
19027
19033
  ...config,
19028
19034
  };
19029
19035
  }
@@ -19042,13 +19048,18 @@ class SessionReplayEngine {
19042
19048
  // Simple rrweb recording configuration like official example
19043
19049
  this.rrwebStopRecord = record({
19044
19050
  emit: (event) => {
19045
- // Simple event capture
19046
- this.events.push(event);
19051
+ // Update activity time on any event
19052
+ this.onActivity();
19053
+ // Only capture events if not paused
19054
+ if (!this.isPaused) {
19055
+ this.events.push(event);
19056
+ }
19047
19057
  },
19048
19058
  });
19049
19059
  this.isActive = true;
19050
19060
  this.startBatchTimer();
19051
- console.log("[Zaplier] Session replay started - simple mode");
19061
+ this.startInactivityTracking();
19062
+ console.log("[Zaplier] Session replay started - with inactivity detection");
19052
19063
  return true;
19053
19064
  }
19054
19065
  catch (error) {
@@ -19072,6 +19083,10 @@ class SessionReplayEngine {
19072
19083
  clearInterval(this.batchTimer);
19073
19084
  this.batchTimer = undefined;
19074
19085
  }
19086
+ if (this.inactivityTimer) {
19087
+ clearTimeout(this.inactivityTimer);
19088
+ this.inactivityTimer = undefined;
19089
+ }
19075
19090
  // Send final batch
19076
19091
  this.sendBatch();
19077
19092
  }
@@ -19090,17 +19105,21 @@ class SessionReplayEngine {
19090
19105
  if (this.events.length === 0) {
19091
19106
  return;
19092
19107
  }
19093
- // Simple payload structure like official example
19108
+ // Enhanced payload structure with visitor linking
19094
19109
  const payload = {
19095
19110
  sessionId: this.sessionId,
19111
+ visitorId: this.visitorId,
19096
19112
  events: [...this.events], // Copy events array
19097
19113
  metadata: {
19098
19114
  userAgent: navigator.userAgent,
19099
19115
  timestamp: Date.now(),
19100
19116
  startUrl: window.location.href,
19101
- duration: Date.now() - (performance.timeOrigin || Date.now()),
19117
+ duration: Date.now() - this.sessionStartTime,
19118
+ activeTime: this.getActiveTime(),
19102
19119
  funnelSteps: [],
19103
19120
  hasConversion: false,
19121
+ eventsCount: this.events.length,
19122
+ isPaused: this.isPaused,
19104
19123
  },
19105
19124
  };
19106
19125
  // Reset events array like official example
@@ -19149,6 +19168,714 @@ class SessionReplayEngine {
19149
19168
  isRecording() {
19150
19169
  return this.isActive;
19151
19170
  }
19171
+ /**
19172
+ * Check if recording is paused
19173
+ */
19174
+ isPausedState() {
19175
+ return this.isPaused;
19176
+ }
19177
+ /**
19178
+ * Start inactivity tracking
19179
+ */
19180
+ startInactivityTracking() {
19181
+ if (!this.config.pauseOnInactive)
19182
+ return;
19183
+ // Listen for user activity events
19184
+ const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
19185
+ activityEvents.forEach(event => {
19186
+ document.addEventListener(event, this.onActivity.bind(this), true);
19187
+ });
19188
+ this.resetInactivityTimer();
19189
+ }
19190
+ /**
19191
+ * Handle activity event
19192
+ */
19193
+ onActivity() {
19194
+ this.lastActivityTime = Date.now();
19195
+ // Resume if paused
19196
+ if (this.isPaused) {
19197
+ this.resume();
19198
+ }
19199
+ this.resetInactivityTimer();
19200
+ }
19201
+ /**
19202
+ * Reset inactivity timer
19203
+ */
19204
+ resetInactivityTimer() {
19205
+ if (this.inactivityTimer) {
19206
+ clearTimeout(this.inactivityTimer);
19207
+ }
19208
+ this.inactivityTimer = window.setTimeout(() => {
19209
+ this.pauseForInactivity();
19210
+ }, this.config.inactivityTimeout);
19211
+ }
19212
+ /**
19213
+ * Pause recording due to inactivity
19214
+ */
19215
+ pauseForInactivity() {
19216
+ if (this.isPaused)
19217
+ return;
19218
+ this.isPaused = true;
19219
+ console.log("[Zaplier] Session replay paused due to inactivity");
19220
+ }
19221
+ /**
19222
+ * Resume recording after activity
19223
+ */
19224
+ resume() {
19225
+ if (!this.isPaused)
19226
+ return;
19227
+ this.isPaused = false;
19228
+ console.log("[Zaplier] Session replay resumed after activity");
19229
+ }
19230
+ /**
19231
+ * Get total active time (excluding paused periods)
19232
+ */
19233
+ getActiveTime() {
19234
+ const totalTime = Date.now() - this.sessionStartTime;
19235
+ // For now, return total time. In production, track pause periods accurately
19236
+ return totalTime;
19237
+ }
19238
+ /**
19239
+ * Get visitor ID
19240
+ */
19241
+ getVisitorId() {
19242
+ return this.visitorId;
19243
+ }
19244
+ }
19245
+
19246
+ /**
19247
+ * Auto Tracker - Sistema de tracking automático via data-* attributes
19248
+ *
19249
+ * Suporta:
19250
+ * - data-track-click="event-name"
19251
+ * - data-track-scroll="event-name"
19252
+ * - data-track-view="event-name"
19253
+ * - data-track-hover="event-name"
19254
+ * - data-track-form="event-name"
19255
+ */
19256
+ class AutoTracker {
19257
+ constructor(sdkInstance, config = {}) {
19258
+ this.observedElements = new Set();
19259
+ /**
19260
+ * Event handlers (bound to this)
19261
+ */
19262
+ this.handleClick = (event) => {
19263
+ const target = event.target;
19264
+ if (target.hasAttribute("data-track-click")) ;
19265
+ };
19266
+ this.handleScroll = () => {
19267
+ // Scroll is handled by element-specific listeners
19268
+ };
19269
+ this.handleHover = (event) => {
19270
+ // Hover is handled by element-specific listeners
19271
+ };
19272
+ this.handleForm = (event) => {
19273
+ // Form is handled by element-specific listeners
19274
+ };
19275
+ this.sdkInstance = sdkInstance;
19276
+ this.config = {
19277
+ enabled: true,
19278
+ trackClicks: true,
19279
+ trackScrolls: true,
19280
+ trackViews: true,
19281
+ trackHovers: true,
19282
+ trackForms: true,
19283
+ debug: false,
19284
+ ...config,
19285
+ };
19286
+ }
19287
+ /**
19288
+ * Inicializar auto tracking
19289
+ */
19290
+ start() {
19291
+ if (!this.config.enabled)
19292
+ return;
19293
+ if (this.config.debug) {
19294
+ console.log("[Zaplier AutoTracker] Iniciando auto tracking");
19295
+ }
19296
+ // Observer para novos elementos no DOM
19297
+ this.observeDOM();
19298
+ // Processar elementos já existentes
19299
+ this.processExistingElements();
19300
+ // Setup intersection observer para views
19301
+ if (this.config.trackViews) {
19302
+ this.setupViewTracking();
19303
+ }
19304
+ }
19305
+ /**
19306
+ * Parar auto tracking
19307
+ */
19308
+ stop() {
19309
+ document.removeEventListener("click", this.handleClick);
19310
+ document.removeEventListener("scroll", this.handleScroll);
19311
+ document.removeEventListener("mouseover", this.handleHover);
19312
+ document.removeEventListener("submit", this.handleForm);
19313
+ if (this.intersectionObserver) {
19314
+ this.intersectionObserver.disconnect();
19315
+ }
19316
+ this.observedElements.clear();
19317
+ }
19318
+ /**
19319
+ * Observar mudanças no DOM para novos elementos
19320
+ */
19321
+ observeDOM() {
19322
+ const observer = new MutationObserver((mutations) => {
19323
+ mutations.forEach((mutation) => {
19324
+ mutation.addedNodes.forEach((node) => {
19325
+ if (node.nodeType === Node.ELEMENT_NODE) {
19326
+ this.processElement(node);
19327
+ }
19328
+ });
19329
+ });
19330
+ });
19331
+ observer.observe(document.body, {
19332
+ childList: true,
19333
+ subtree: true,
19334
+ });
19335
+ }
19336
+ /**
19337
+ * Processar elementos já existentes no DOM
19338
+ */
19339
+ processExistingElements() {
19340
+ // Buscar todos os elementos com data-track-*
19341
+ const trackElements = document.querySelectorAll("[data-track-click], [data-track-scroll], [data-track-view], [data-track-hover], [data-track-form]");
19342
+ trackElements.forEach((element) => {
19343
+ this.processElement(element);
19344
+ });
19345
+ }
19346
+ /**
19347
+ * Processar um elemento específico
19348
+ */
19349
+ processElement(element) {
19350
+ // Click tracking
19351
+ if (this.config.trackClicks && element.hasAttribute("data-track-click")) {
19352
+ this.setupClickTracking(element);
19353
+ }
19354
+ // Scroll tracking
19355
+ if (this.config.trackScrolls && element.hasAttribute("data-track-scroll")) {
19356
+ this.setupScrollTracking(element);
19357
+ }
19358
+ // View tracking
19359
+ if (this.config.trackViews && element.hasAttribute("data-track-view")) {
19360
+ this.setupElementViewTracking(element);
19361
+ }
19362
+ // Hover tracking
19363
+ if (this.config.trackHovers && element.hasAttribute("data-track-hover")) {
19364
+ this.setupHoverTracking(element);
19365
+ }
19366
+ // Form tracking
19367
+ if (this.config.trackForms && element.hasAttribute("data-track-form")) {
19368
+ this.setupFormTracking(element);
19369
+ }
19370
+ }
19371
+ /**
19372
+ * Setup click tracking
19373
+ */
19374
+ setupClickTracking(element) {
19375
+ element.addEventListener("click", (event) => {
19376
+ const eventName = element.getAttribute("data-track-click");
19377
+ if (!eventName)
19378
+ return;
19379
+ const metadata = this.extractMetadata(element);
19380
+ this.trackEvent(eventName, {
19381
+ type: "click",
19382
+ element: element.tagName.toLowerCase(),
19383
+ ...metadata,
19384
+ });
19385
+ if (this.config.debug) {
19386
+ console.log(`[AutoTracker] Click tracked: ${eventName}`, metadata);
19387
+ }
19388
+ });
19389
+ }
19390
+ /**
19391
+ * Setup scroll tracking
19392
+ */
19393
+ setupScrollTracking(element) {
19394
+ let hasTriggered = false;
19395
+ const threshold = parseFloat(element.getAttribute("data-scroll-threshold") || "0.5");
19396
+ const handleScroll = () => {
19397
+ if (hasTriggered)
19398
+ return;
19399
+ const rect = element.getBoundingClientRect();
19400
+ const elementHeight = rect.height;
19401
+ const visibleHeight = Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0);
19402
+ const visibilityRatio = Math.max(0, visibleHeight) / elementHeight;
19403
+ if (visibilityRatio >= threshold) {
19404
+ hasTriggered = true;
19405
+ const eventName = element.getAttribute("data-track-scroll");
19406
+ if (!eventName)
19407
+ return;
19408
+ const metadata = this.extractMetadata(element);
19409
+ this.trackEvent(eventName, {
19410
+ type: "scroll",
19411
+ element: element.tagName.toLowerCase(),
19412
+ threshold,
19413
+ scrollDepth: window.scrollY,
19414
+ ...metadata,
19415
+ });
19416
+ if (this.config.debug) {
19417
+ console.log(`[AutoTracker] Scroll tracked: ${eventName}`, metadata);
19418
+ }
19419
+ }
19420
+ };
19421
+ document.addEventListener("scroll", handleScroll, { passive: true });
19422
+ // Check immediately in case element is already in view
19423
+ setTimeout(handleScroll, 100);
19424
+ }
19425
+ /**
19426
+ * Setup view tracking usando Intersection Observer
19427
+ */
19428
+ setupViewTracking() {
19429
+ this.intersectionObserver = new IntersectionObserver((entries) => {
19430
+ entries.forEach((entry) => {
19431
+ if (entry.isIntersecting) {
19432
+ const element = entry.target;
19433
+ const eventName = element.getAttribute("data-track-view");
19434
+ if (!eventName)
19435
+ return;
19436
+ const metadata = this.extractMetadata(element);
19437
+ this.trackEvent(eventName, {
19438
+ type: "view",
19439
+ element: element.tagName.toLowerCase(),
19440
+ intersectionRatio: entry.intersectionRatio,
19441
+ ...metadata,
19442
+ });
19443
+ if (this.config.debug) {
19444
+ console.log(`[AutoTracker] View tracked: ${eventName}`, metadata);
19445
+ }
19446
+ // Remove from observation after first trigger
19447
+ this.intersectionObserver?.unobserve(element);
19448
+ }
19449
+ });
19450
+ }, {
19451
+ threshold: 0.5, // 50% visible
19452
+ rootMargin: "0px",
19453
+ });
19454
+ }
19455
+ /**
19456
+ * Setup view tracking para elemento específico
19457
+ */
19458
+ setupElementViewTracking(element) {
19459
+ if (!this.intersectionObserver)
19460
+ return;
19461
+ if (!this.observedElements.has(element)) {
19462
+ this.intersectionObserver.observe(element);
19463
+ this.observedElements.add(element);
19464
+ }
19465
+ }
19466
+ /**
19467
+ * Setup hover tracking
19468
+ */
19469
+ setupHoverTracking(element) {
19470
+ let hoverStartTime;
19471
+ const minHoverTime = parseInt(element.getAttribute("data-hover-time") || "1000");
19472
+ element.addEventListener("mouseenter", () => {
19473
+ hoverStartTime = Date.now();
19474
+ });
19475
+ element.addEventListener("mouseleave", () => {
19476
+ const hoverDuration = Date.now() - hoverStartTime;
19477
+ if (hoverDuration >= minHoverTime) {
19478
+ const eventName = element.getAttribute("data-track-hover");
19479
+ if (!eventName)
19480
+ return;
19481
+ const metadata = this.extractMetadata(element);
19482
+ this.trackEvent(eventName, {
19483
+ type: "hover",
19484
+ element: element.tagName.toLowerCase(),
19485
+ hoverDuration,
19486
+ minHoverTime,
19487
+ ...metadata,
19488
+ });
19489
+ if (this.config.debug) {
19490
+ console.log(`[AutoTracker] Hover tracked: ${eventName}`, metadata);
19491
+ }
19492
+ }
19493
+ });
19494
+ }
19495
+ /**
19496
+ * Setup form tracking
19497
+ */
19498
+ setupFormTracking(element) {
19499
+ if (element.tagName.toLowerCase() !== "form")
19500
+ return;
19501
+ element.addEventListener("submit", (event) => {
19502
+ const eventName = element.getAttribute("data-track-form");
19503
+ if (!eventName)
19504
+ return;
19505
+ const formData = new FormData(element);
19506
+ const metadata = this.extractMetadata(element);
19507
+ // Extract form fields (without sensitive data)
19508
+ const fields = {};
19509
+ for (const [key, value] of formData.entries()) {
19510
+ // Skip sensitive fields
19511
+ if (!this.isSensitiveField(key)) {
19512
+ fields[key] =
19513
+ typeof value === "string" ? value.substring(0, 100) : "file";
19514
+ }
19515
+ }
19516
+ this.trackEvent(eventName, {
19517
+ type: "form_submit",
19518
+ element: "form",
19519
+ fieldCount: Array.from(formData.entries()).length,
19520
+ fields,
19521
+ ...metadata,
19522
+ });
19523
+ if (this.config.debug) {
19524
+ console.log(`[AutoTracker] Form submit tracked: ${eventName}`, metadata);
19525
+ }
19526
+ });
19527
+ }
19528
+ /**
19529
+ * Extrair metadata do elemento
19530
+ */
19531
+ extractMetadata(element) {
19532
+ const metadata = {};
19533
+ // Extrair todos os data-meta-* attributes
19534
+ Array.from(element.attributes).forEach((attr) => {
19535
+ if (attr.name.startsWith("data-meta-")) {
19536
+ const key = attr.name.replace("data-meta-", "");
19537
+ metadata[key] = attr.value;
19538
+ }
19539
+ });
19540
+ // Adicionar informações básicas
19541
+ if (element.id)
19542
+ metadata.elementId = element.id;
19543
+ if (element.className)
19544
+ metadata.elementClass = element.className;
19545
+ // Text content (limitado)
19546
+ const textContent = element.textContent?.trim();
19547
+ if (textContent && textContent.length > 0) {
19548
+ metadata.textContent = textContent.substring(0, 50);
19549
+ }
19550
+ // Position info
19551
+ const rect = element.getBoundingClientRect();
19552
+ metadata.elementPosition = {
19553
+ x: Math.round(rect.left),
19554
+ y: Math.round(rect.top),
19555
+ width: Math.round(rect.width),
19556
+ height: Math.round(rect.height),
19557
+ };
19558
+ return metadata;
19559
+ }
19560
+ /**
19561
+ * Verificar se um campo é sensível
19562
+ */
19563
+ isSensitiveField(fieldName) {
19564
+ const sensitivePatterns = [
19565
+ /password/i,
19566
+ /pass/i,
19567
+ /pwd/i,
19568
+ /secret/i,
19569
+ /token/i,
19570
+ /api[_-]?key/i,
19571
+ /credit[_-]?card/i,
19572
+ /ssn/i,
19573
+ /social/i,
19574
+ /tax/i,
19575
+ ];
19576
+ return sensitivePatterns.some((pattern) => pattern.test(fieldName));
19577
+ }
19578
+ /**
19579
+ * Enviar evento para o SDK
19580
+ */
19581
+ trackEvent(eventName, metadata) {
19582
+ if (!this.sdkInstance)
19583
+ return;
19584
+ try {
19585
+ this.sdkInstance.trackCustomEvent(eventName, {
19586
+ autoTracked: true,
19587
+ timestamp: Date.now(),
19588
+ url: window.location.href,
19589
+ ...metadata,
19590
+ });
19591
+ }
19592
+ catch (error) {
19593
+ if (this.config.debug) {
19594
+ console.error("[AutoTracker] Error sending event:", error);
19595
+ }
19596
+ }
19597
+ }
19598
+ /**
19599
+ * Configurar tracking
19600
+ */
19601
+ configure(config) {
19602
+ this.config = { ...this.config, ...config };
19603
+ }
19604
+ /**
19605
+ * Obter estatísticas
19606
+ */
19607
+ getStats() {
19608
+ return {
19609
+ observedElements: this.observedElements.size,
19610
+ config: this.config,
19611
+ };
19612
+ }
19613
+ }
19614
+
19615
+ /**
19616
+ * Visitor Persistence Manager for SDK
19617
+ * Implements client-side visitor identification with localStorage camouflage
19618
+ */
19619
+ // Camuflaged storage keys to avoid detection/blocking
19620
+ const STORAGE_KEYS = {
19621
+ session: "__zp_s", // Session identifier
19622
+ visitor: "__zp_v", // Visitor identifier (camuflado)
19623
+ device: "__zp_d", // Device fingerprint cache
19624
+ prefs: "__zp_p", // User preferences (decoy storage)
19625
+ analytics: "__zp_a", // Analytics preferences (additional decoy)
19626
+ };
19627
+ /**
19628
+ * Multi-layer persistence manager with fallbacks
19629
+ */
19630
+ class PersistenceManager {
19631
+ // 1. LocalStorage (primary) - Most persistent
19632
+ static setLocal(key, value) {
19633
+ try {
19634
+ if (typeof window !== "undefined" && window.localStorage) {
19635
+ localStorage.setItem(key, value);
19636
+ return true;
19637
+ }
19638
+ }
19639
+ catch (e) {
19640
+ // Storage disabled/private mode
19641
+ }
19642
+ return false;
19643
+ }
19644
+ static getLocal(key) {
19645
+ try {
19646
+ if (typeof window !== "undefined" && window.localStorage) {
19647
+ return localStorage.getItem(key);
19648
+ }
19649
+ }
19650
+ catch (e) {
19651
+ // Storage disabled/private mode
19652
+ }
19653
+ return null;
19654
+ }
19655
+ // 2. SessionStorage (secondary) - Session-only
19656
+ static setSession(key, value) {
19657
+ try {
19658
+ if (typeof window !== "undefined" && window.sessionStorage) {
19659
+ sessionStorage.setItem(key, value);
19660
+ return true;
19661
+ }
19662
+ }
19663
+ catch (e) {
19664
+ // Storage disabled/private mode
19665
+ }
19666
+ return false;
19667
+ }
19668
+ static getSession(key) {
19669
+ try {
19670
+ if (typeof window !== "undefined" && window.sessionStorage) {
19671
+ return sessionStorage.getItem(key);
19672
+ }
19673
+ }
19674
+ catch (e) {
19675
+ // Storage disabled/private mode
19676
+ }
19677
+ return null;
19678
+ }
19679
+ // 3. Cookie (tertiary) - Cross-session with expiration
19680
+ static setCookie(key, value, days = 365) {
19681
+ try {
19682
+ if (typeof document !== "undefined") {
19683
+ const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
19684
+ document.cookie = `${key}=${value}; expires=${expires}; path=/; SameSite=Lax`;
19685
+ return true;
19686
+ }
19687
+ }
19688
+ catch (e) {
19689
+ // Cookies disabled
19690
+ }
19691
+ return false;
19692
+ }
19693
+ static getCookie(key) {
19694
+ try {
19695
+ if (typeof document !== "undefined") {
19696
+ const name = key + "=";
19697
+ const decodedCookie = decodeURIComponent(document.cookie);
19698
+ const ca = decodedCookie.split(";");
19699
+ for (let i = 0; i < ca.length; i++) {
19700
+ let c = ca[i];
19701
+ if (c) {
19702
+ while (c.charAt(0) === " ") {
19703
+ c = c.substring(1);
19704
+ }
19705
+ if (c.indexOf(name) === 0) {
19706
+ return c.substring(name.length, c.length);
19707
+ }
19708
+ }
19709
+ }
19710
+ }
19711
+ }
19712
+ catch (e) {
19713
+ // Cookies disabled
19714
+ }
19715
+ return null;
19716
+ }
19717
+ // 4. Memory (fallback) - Current session only
19718
+ static setMemory(key, value) {
19719
+ this.memoryStore.set(key, value);
19720
+ return true;
19721
+ }
19722
+ static getMemory(key) {
19723
+ return this.memoryStore.get(key) || null;
19724
+ }
19725
+ // Multi-layer get with fallbacks
19726
+ static get(key) {
19727
+ // Try localStorage first (most persistent)
19728
+ let value = this.getLocal(key);
19729
+ if (value)
19730
+ return { value, method: "localStorage" };
19731
+ // Try sessionStorage (session-only)
19732
+ value = this.getSession(key);
19733
+ if (value)
19734
+ return { value, method: "sessionStorage" };
19735
+ // Try cookies (cross-session)
19736
+ value = this.getCookie(key);
19737
+ if (value)
19738
+ return { value, method: "cookie" };
19739
+ // Try memory (current session)
19740
+ value = this.getMemory(key);
19741
+ if (value)
19742
+ return { value, method: "memory" };
19743
+ return { value: null, method: "none" };
19744
+ }
19745
+ // Multi-layer set with fallbacks
19746
+ static set(key, value) {
19747
+ // Try localStorage first (most persistent)
19748
+ if (this.setLocal(key, value)) {
19749
+ return { success: true, method: "localStorage" };
19750
+ }
19751
+ // Try sessionStorage (session-only)
19752
+ if (this.setSession(key, value)) {
19753
+ return { success: true, method: "sessionStorage" };
19754
+ }
19755
+ // Try cookies (cross-session)
19756
+ if (this.setCookie(key, value)) {
19757
+ return { success: true, method: "cookie" };
19758
+ }
19759
+ // Fallback to memory (current session)
19760
+ this.setMemory(key, value);
19761
+ return { success: true, method: "memory" };
19762
+ }
19763
+ }
19764
+ PersistenceManager.memoryStore = new Map();
19765
+ /**
19766
+ * Advanced Visitor Identity Manager with Camouflaged Storage
19767
+ */
19768
+ class VisitorIdentityManager {
19769
+ generateVisitorId() {
19770
+ return "vis_" + Array.from(crypto.getRandomValues(new Uint8Array(16)))
19771
+ .map(b => b.toString(16).padStart(2, '0'))
19772
+ .join('');
19773
+ }
19774
+ generateSessionId() {
19775
+ return "ses_" + Array.from(crypto.getRandomValues(new Uint8Array(8)))
19776
+ .map(b => b.toString(16).padStart(2, '0'))
19777
+ .join('') + "_" + Date.now().toString(36);
19778
+ }
19779
+ createCamouflageData(visitorId, sessionId, stableCoreHash) {
19780
+ return {
19781
+ theme: "auto",
19782
+ lang: (navigator.language || "en-US").substring(0, 2),
19783
+ tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
19784
+ analytics: true,
19785
+ cookies: true,
19786
+ _v: visitorId, // Hidden visitor ID
19787
+ _s: sessionId, // Hidden session ID
19788
+ _sc: stableCoreHash, // Hidden stable core
19789
+ ts: Date.now(),
19790
+ };
19791
+ }
19792
+ extractFromCamouflage(data) {
19793
+ try {
19794
+ const parsed = JSON.parse(data);
19795
+ if (parsed._v && parsed._s && parsed._sc) {
19796
+ return {
19797
+ visitorId: parsed._v,
19798
+ sessionId: parsed._s,
19799
+ stableCoreHash: parsed._sc,
19800
+ persistenceMethod: "localStorage",
19801
+ confidence: 0.95,
19802
+ createdAt: parsed.ts || Date.now(),
19803
+ lastSeen: Date.now(),
19804
+ reused: true,
19805
+ };
19806
+ }
19807
+ }
19808
+ catch (e) {
19809
+ // Invalid data
19810
+ }
19811
+ return null;
19812
+ }
19813
+ async getOrCreateVisitorIdentity(params) {
19814
+ const { stableCoreHash } = params;
19815
+ // Try to recover existing identity from camouflaged storage
19816
+ const { value: storedData, method } = PersistenceManager.get(STORAGE_KEYS.prefs);
19817
+ if (storedData) {
19818
+ const existingIdentity = this.extractFromCamouflage(storedData);
19819
+ if (existingIdentity && existingIdentity.stableCoreHash === stableCoreHash) {
19820
+ // Update last seen time
19821
+ const updatedCamouflage = this.createCamouflageData(existingIdentity.visitorId, existingIdentity.sessionId, stableCoreHash);
19822
+ PersistenceManager.set(STORAGE_KEYS.prefs, JSON.stringify(updatedCamouflage));
19823
+ return {
19824
+ ...existingIdentity,
19825
+ persistenceMethod: method,
19826
+ lastSeen: Date.now(),
19827
+ reused: true,
19828
+ };
19829
+ }
19830
+ }
19831
+ // Create new visitor identity
19832
+ const newVisitorId = this.generateVisitorId();
19833
+ const newSessionId = params.sessionId || this.generateSessionId();
19834
+ const newIdentity = {
19835
+ visitorId: newVisitorId,
19836
+ sessionId: newSessionId,
19837
+ stableCoreHash,
19838
+ deviceFingerprint: params.deviceFingerprint,
19839
+ persistenceMethod: "localStorage",
19840
+ confidence: 0.90,
19841
+ createdAt: Date.now(),
19842
+ lastSeen: Date.now(),
19843
+ reused: false,
19844
+ };
19845
+ // Store in camouflaged format
19846
+ const camouflageData = this.createCamouflageData(newVisitorId, newSessionId, stableCoreHash);
19847
+ const { method: storageMethod } = PersistenceManager.set(STORAGE_KEYS.prefs, JSON.stringify(camouflageData));
19848
+ newIdentity.persistenceMethod = storageMethod;
19849
+ // Also store device fingerprint cache for faster lookups
19850
+ PersistenceManager.set(STORAGE_KEYS.device, params.deviceFingerprint);
19851
+ return newIdentity;
19852
+ }
19853
+ // Get current visitor ID without creating new one
19854
+ getCurrentVisitorId() {
19855
+ const { value: storedData } = PersistenceManager.get(STORAGE_KEYS.prefs);
19856
+ if (storedData) {
19857
+ const identity = this.extractFromCamouflage(storedData);
19858
+ return identity?.visitorId || null;
19859
+ }
19860
+ return null;
19861
+ }
19862
+ // Clear all stored identity data
19863
+ clearIdentity() {
19864
+ // Remove from all storage layers
19865
+ try {
19866
+ PersistenceManager.setLocal(STORAGE_KEYS.prefs, "");
19867
+ PersistenceManager.setLocal(STORAGE_KEYS.device, "");
19868
+ PersistenceManager.setSession(STORAGE_KEYS.prefs, "");
19869
+ PersistenceManager.setSession(STORAGE_KEYS.device, "");
19870
+ PersistenceManager.setCookie(STORAGE_KEYS.prefs, "", -1); // Expire immediately
19871
+ PersistenceManager.setCookie(STORAGE_KEYS.device, "", -1);
19872
+ PersistenceManager.setMemory(STORAGE_KEYS.prefs, "");
19873
+ PersistenceManager.setMemory(STORAGE_KEYS.device, "");
19874
+ }
19875
+ catch (e) {
19876
+ // Silent fail
19877
+ }
19878
+ }
19152
19879
  }
19153
19880
 
19154
19881
  /**
@@ -19182,7 +19909,7 @@ const DEFAULT_CONFIG = {
19182
19909
  */
19183
19910
  class ZaplierSDK {
19184
19911
  constructor(userConfig) {
19185
- this.version = "1.3.7";
19912
+ this.version = "1.6.0";
19186
19913
  this.isInitialized = false;
19187
19914
  this.eventQueue = [];
19188
19915
  /**
@@ -19253,8 +19980,10 @@ class ZaplierSDK {
19253
19980
  }
19254
19981
  const sessionId = this.sessionId;
19255
19982
  // When explicitly enabled, use 100% sample rate
19256
- this.replayEngine = new SessionReplayEngine(sessionId, {
19983
+ this.replayEngine = new SessionReplayEngine(sessionId, this.backendVisitorId || 'unknown', {
19257
19984
  sampleRate: 1.0, // Force 100% when explicitly enabled
19985
+ inactivityTimeout: 30000,
19986
+ pauseOnInactive: true,
19258
19987
  });
19259
19988
  // Connect to anti-adblock manager
19260
19989
  if (this.antiAdblockManager) {
@@ -19279,8 +20008,10 @@ class ZaplierSDK {
19279
20008
  this.sessionId = this.generateSessionId();
19280
20009
  }
19281
20010
  const sessionId = this.sessionId;
19282
- this.replayEngine = new SessionReplayEngine(sessionId, {
20011
+ this.replayEngine = new SessionReplayEngine(sessionId, this.backendVisitorId || 'unknown', {
19283
20012
  sampleRate: 1.0, // Force 100% when explicitly enabled
20013
+ inactivityTimeout: 30000,
20014
+ pauseOnInactive: true,
19284
20015
  });
19285
20016
  // Connect to anti-adblock manager
19286
20017
  if (this.antiAdblockManager) {
@@ -19319,8 +20050,10 @@ class ZaplierSDK {
19319
20050
  this.sessionId = this.generateSessionId();
19320
20051
  }
19321
20052
  const sessionId = this.sessionId;
19322
- this.replayEngine = new SessionReplayEngine(sessionId, {
20053
+ this.replayEngine = new SessionReplayEngine(sessionId, this.backendVisitorId || 'unknown', {
19323
20054
  sampleRate: 1.0, // Force recording when manually started
20055
+ inactivityTimeout: 30000,
20056
+ pauseOnInactive: true,
19324
20057
  });
19325
20058
  // Connect to anti-adblock manager
19326
20059
  if (this.antiAdblockManager) {
@@ -19352,6 +20085,45 @@ class ZaplierSDK {
19352
20085
  this.trackConversion("funnel_conversion", data.value, data.currency, data);
19353
20086
  },
19354
20087
  };
20088
+ /**
20089
+ * Auto Tracker API
20090
+ */
20091
+ this.autoTrack = {
20092
+ enable: () => {
20093
+ if (!this.autoTracker) {
20094
+ this.initializeAutoTracker();
20095
+ }
20096
+ else {
20097
+ this.autoTracker.configure({ enabled: true });
20098
+ }
20099
+ if (this.config.debug) {
20100
+ console.log("[Zaplier] Auto tracking enabled");
20101
+ }
20102
+ },
20103
+ disable: () => {
20104
+ if (this.autoTracker) {
20105
+ this.autoTracker.stop();
20106
+ this.autoTracker = undefined;
20107
+ }
20108
+ if (this.config.debug) {
20109
+ console.log("[Zaplier] Auto tracking disabled");
20110
+ }
20111
+ },
20112
+ configure: (config) => {
20113
+ if (this.autoTracker) {
20114
+ this.autoTracker.configure(config);
20115
+ }
20116
+ if (this.config.debug) {
20117
+ console.log("[Zaplier] Auto tracking configured:", config);
20118
+ }
20119
+ },
20120
+ getStats: () => {
20121
+ return this.autoTracker ? this.autoTracker.getStats() : null;
20122
+ },
20123
+ isEnabled: () => {
20124
+ return !!this.autoTracker;
20125
+ },
20126
+ };
19355
20127
  // Validate required config
19356
20128
  if (!userConfig.token) {
19357
20129
  throw new Error("Zaplier: token is required");
@@ -19393,6 +20165,8 @@ class ZaplierSDK {
19393
20165
  this.initializeAntiAdblock();
19394
20166
  // Then initialize tracking engines (they'll connect to anti-adblock)
19395
20167
  this.initializeTrackingEngines();
20168
+ // Initialize auto tracker
20169
+ this.initializeAutoTracker();
19396
20170
  // Process queued events
19397
20171
  this.processEventQueue();
19398
20172
  if (this.config.debug) {
@@ -19423,8 +20197,10 @@ class ZaplierSDK {
19423
20197
  // When replay is explicitly enabled, use 100% sample rate to ensure recording
19424
20198
  // The default replaySampling (0.1) is only for automatic/production sampling
19425
20199
  const sampleRate = this.config.replay === true ? 1.0 : this.config.replaySampling;
19426
- this.replayEngine = new SessionReplayEngine(this.sessionId, {
20200
+ this.replayEngine = new SessionReplayEngine(this.sessionId, this.backendVisitorId || 'unknown', {
19427
20201
  sampleRate: sampleRate,
20202
+ inactivityTimeout: 30000,
20203
+ pauseOnInactive: true,
19428
20204
  });
19429
20205
  // Connect SDK instance for transport
19430
20206
  this.replayEngine.setSDKInstance(this);
@@ -19459,6 +20235,29 @@ class ZaplierSDK {
19459
20235
  console.error("[Zaplier] Failed to initialize tracking engines:", error);
19460
20236
  }
19461
20237
  }
20238
+ /**
20239
+ * Initialize Auto Tracker for data-* attributes
20240
+ */
20241
+ initializeAutoTracker() {
20242
+ try {
20243
+ this.autoTracker = new AutoTracker(this, {
20244
+ enabled: true,
20245
+ trackClicks: true,
20246
+ trackScrolls: true,
20247
+ trackViews: true,
20248
+ trackHovers: true,
20249
+ trackForms: true,
20250
+ debug: this.config.debug,
20251
+ });
20252
+ this.autoTracker.start();
20253
+ if (this.config.debug) {
20254
+ console.log("[Zaplier] Auto Tracker initialized and started");
20255
+ }
20256
+ }
20257
+ catch (error) {
20258
+ console.error("[Zaplier] Failed to initialize Auto Tracker:", error);
20259
+ }
20260
+ }
19462
20261
  /**
19463
20262
  * Generate session ID
19464
20263
  */
@@ -19514,10 +20313,12 @@ class ZaplierSDK {
19514
20313
  }
19515
20314
  /**
19516
20315
  * Collect fingerprint and generate visitor ID (IMPROVED - 2024)
19517
- * Enhanced with better incognito detection
20316
+ * Enhanced with better incognito detection and localStorage persistence
19518
20317
  */
19519
20318
  async collectFingerprint() {
19520
20319
  try {
20320
+ // Initialize visitor identity manager
20321
+ this.visitorIdentityManager = new VisitorIdentityManager();
19521
20322
  // Use appropriate fingerprinting based on GDPR mode
19522
20323
  const result = this.config.gdprMode
19523
20324
  ? await getLightweightFingerprint()
@@ -19535,6 +20336,26 @@ class ZaplierSDK {
19535
20336
  sdkVersion: this.version,
19536
20337
  };
19537
20338
  }
20339
+ // Generate or retrieve visitor identity using persistent storage
20340
+ const visitorIdentity = await this.visitorIdentityManager.getOrCreateVisitorIdentity({
20341
+ fingerprintHash: result.data.hash,
20342
+ stableCoreHash: result.data.stableCoreHash,
20343
+ deviceFingerprint: result.data.hash,
20344
+ sessionId: this.sessionId,
20345
+ userAgent: navigator.userAgent,
20346
+ ipAddress: undefined, // Will be determined server-side
20347
+ });
20348
+ // Set the persistent visitor ID
20349
+ this.visitorId = visitorIdentity.visitorId;
20350
+ if (this.config.debug) {
20351
+ console.log("[Zaplier] Visitor identity resolved:", {
20352
+ visitorId: this.visitorId,
20353
+ sessionId: this.sessionId,
20354
+ persistenceMethod: visitorIdentity.persistenceMethod,
20355
+ confidence: visitorIdentity.confidence,
20356
+ isNewVisitor: !visitorIdentity.reused,
20357
+ });
20358
+ }
19538
20359
  if (this.config.debug) {
19539
20360
  console.log("[Zaplier] Fingerprint collected:", {
19540
20361
  components: result.collectedComponents,
@@ -19641,6 +20462,10 @@ class ZaplierSDK {
19641
20462
  try {
19642
20463
  const payload = {
19643
20464
  // workspaceId moved to query parameter
20465
+ // Visitor ID (persistido via localStorage camuflado)
20466
+ visitorId: this.visitorId,
20467
+ // Session ID (gerado por sessão)
20468
+ sessionId: this.sessionId,
19644
20469
  fingerprintHash: this.fingerprint?.hash,
19645
20470
  stableCoreHash: this.fingerprint?.stableCoreHash,
19646
20471
  stableCoreVector: this.fingerprint?.stableCoreVector,
@@ -20401,6 +21226,44 @@ const Zaplier = {
20401
21226
  globalInstance.replay.markConversion(data);
20402
21227
  }
20403
21228
  },
21229
+ /**
21230
+ * Auto Tracker API
21231
+ */
21232
+ autoTrack: {
21233
+ enable: () => {
21234
+ if (!globalInstance) {
21235
+ console.warn('[Zaplier] SDK not initialized. Call Zaplier.init() first');
21236
+ return;
21237
+ }
21238
+ globalInstance.autoTrack.enable();
21239
+ },
21240
+ disable: () => {
21241
+ if (!globalInstance) {
21242
+ console.warn('[Zaplier] SDK not initialized. Call Zaplier.init() first');
21243
+ return;
21244
+ }
21245
+ globalInstance.autoTrack.disable();
21246
+ },
21247
+ configure: (config) => {
21248
+ if (!globalInstance) {
21249
+ console.warn('[Zaplier] SDK not initialized. Call Zaplier.init() first');
21250
+ return;
21251
+ }
21252
+ globalInstance.autoTrack.configure(config);
21253
+ },
21254
+ getStats: () => {
21255
+ if (!globalInstance) {
21256
+ return null;
21257
+ }
21258
+ return globalInstance.autoTrack.getStats();
21259
+ },
21260
+ isEnabled: () => {
21261
+ if (!globalInstance) {
21262
+ return false;
21263
+ }
21264
+ return globalInstance.autoTrack.isEnabled();
21265
+ }
21266
+ },
20404
21267
  /**
20405
21268
  * Debug and utility functions
20406
21269
  */
@@ -20427,7 +21290,7 @@ const Zaplier = {
20427
21290
  /**
20428
21291
  * Version info
20429
21292
  */
20430
- version: '3.0.0'
21293
+ version: '1.6.0'
20431
21294
  };
20432
21295
  /**
20433
21296
  * Auto-initialization from script tag data attributes