UNPKG

@thrivestack/analytics-browser

Version:

ThriveStack Analytics Platform - Comprehensive web analytics tracking with privacy-first approach

2,077 lines 87.9 kB
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ThriveStack = {}));
})(this, (function (exports) { 'use strict';

  /**
   * ThriveStack Analytics Platform
   * Comprehensive web analytics tracking with privacy-first approach
   * @version 2.0.1
   */
  class ThriveStack {
      /**
       * Check if visitor_generated event should be sent
       */
      shouldSendVisitorGeneratedEvent(currentSessionId) {
          // Always send for first time (no previous session tracked)
          if (this.lastSessionId === null) {
              return true;
          }
          // Send if session ID has changed (new session with existing device)
          if (this.lastSessionId !== currentSessionId) {
              return true;
          }
          // Don't send if same session (page refresh, navigation within session)
          return false;
      }
      /**
       * Check if visitor_generated should be sent for new session with existing device
       */
      shouldSendVisitorGeneratedForNewSession() {
          // Check if we have a device ID in cookie (existing device)
          const deviceIdFromCookie = this.getDeviceIdFromCookie();
          if (!deviceIdFromCookie) {
              return false; // No device ID means new visitor, handled elsewhere
          }
          // Check if visitor_generated was already sent for this device
          const visitorGeneratedSent = this.getVisitorGeneratedSentFromCookie();
          if (visitorGeneratedSent) {
              return false; // Already sent for this device
          }
          // Check if we have a last session ID
          const lastSessionId = this.getLastSessionIdFromCookie();
          if (!lastSessionId) {
              return true; // No previous session tracked, send visitor_generated
          }
          // Check if current session is different from last session
          const currentSessionId = this.getSessionId();
          if (lastSessionId !== currentSessionId) {
              return true; // New session detected, send visitor_generated
          }
          return false; // Same session, don't send
      }
      /**
       * Send dedicated fingerprint event with location data
       */
      sendFingerprintEvent(deviceId, fingerprintData, totalGenerationTime) {
          try {
              // Extract location data from fingerprint data or use existing location info
              const locationData = this.locationInfo || {};
              // Prepare visitor_generated event payload with ClickHouse-compatible properties
              const eventPayload = {
                  // Location data for ClickHouse materialized view
                  ip_address: fingerprintData?.ip_address || this.ipAddress || '',
                  country: locationData.country_code || '',
                  region: locationData.region || '',
                  city: locationData.city || '',
              };
              // Create ThriveStackEvent object with ClickHouse-compatible context
              const fingerprintEvent = {
                  event_name: 'visitor_generated',
                  properties: eventPayload,
                  user_id: this.userId || this.getUserIdFromCookie() || '',
                  context: {
                      session_id: this.getSessionId(),
                      device_id: deviceId,
                      group_id: this.groupId || this.getGroupIdFromCookie() || '',
                      source: this.source
                  },
                  timestamp: new Date().toISOString(),
              };
              // Queue the event for sending
              this.queueEvent(fingerprintEvent);
              // Mark as sent and update session tracking
              this.visitorGeneratedSent = true;
              this.lastSessionId = this.getSessionId();
              // Save to cookies
              this.setVisitorGeneratedSentCookie(true);
              this.setLastSessionIdCookie(this.lastSessionId);
              // Fingerprint event sent successfully
          }
          catch (error) {
              // Failed to send fingerprint event
          }
      }
      /**
       * Send fingerprint event for cached device ID (new session mapping)
       */
      sendFingerprintEventFromCache(deviceId) {
          try {
              // Get cached location data if available
              const cachedLocationData = this.getLocationInfoFromCookie();
              // Prepare visitor_generated event payload for cached fingerprint with ClickHouse-compatible properties
              const eventPayload = {
                  // Location data for ClickHouse materialized view
                  ip_address: cachedLocationData?.ip || this.ipAddress || '',
                  country: cachedLocationData?.country_code || this.locationInfo?.country_code || '',
                  region: cachedLocationData?.region || this.locationInfo?.region || '',
                  city: cachedLocationData?.city || this.locationInfo?.city || '',
              };
              // Create ThriveStackEvent object with ClickHouse-compatible context
              const fingerprintEvent = {
                  event_name: 'visitor_generated',
                  properties: eventPayload,
                  user_id: this.userId || this.getUserIdFromCookie() || '',
                  context: {
                      session_id: this.getSessionId(),
                      device_id: deviceId,
                      group_id: this.groupId || this.getGroupIdFromCookie() || '',
                      source: this.source
                  },
                  timestamp: new Date().toISOString(),
              };
              // Queue the event for sending
              this.queueEvent(fingerprintEvent);
              // Mark as sent and update session tracking
              this.visitorGeneratedSent = true;
              this.lastSessionId = this.getSessionId();
              // Save to cookies
              this.setVisitorGeneratedSentCookie(true);
              this.setLastSessionIdCookie(this.lastSessionId);
              // Cached fingerprint event sent for new session mapping
          }
          catch (error) {
              // Failed to send cached fingerprint event
          }
      }
      /**
       * Initialize ThriveStack with configuration options
       */
      constructor(options) {
          // Handle string API key for backward compatibility
          if (typeof options === 'string') {
              options = {
                  apiKey: options,
              };
          }
          // Core settings
          this.apiKey = options.apiKey;
          this.apiEndpoint = options.apiEndpoint || "https://api.app.thrivestack.ai/api";
          this.trackClicks = options.trackClicks !== false;
          this.trackForms = options.trackForms === true;
          this.enableConsent = options.enableConsent === true;
          this.source = options.source || '';
          // Geo IP service URL - defaults to free ipinfo.io service
          this.geoIpServiceUrl = 'https://ipinfo.io/json';
          // IP and location information storage
          this.ipAddress = null;
          this.locationInfo = null;
          // Analytics state
          this.interactionHistory = [];
          this.maxHistoryLength = 20;
          // Consent settings (default to functional only)
          this.consentCategories = {
              functional: true, // Always needed
              analytics: options.defaultConsent === true,
              marketing: options.defaultConsent === true,
          };
          // Load userId and groupId from cookies if available
          this.userId = this.getUserIdFromCookie() || '';
          this.groupId = this.getGroupIdFromCookie() || '';
          // Device ID management
          this.deviceId = null;
          this.deviceIdReady = false;
          this.fpPromise = null;
          this.locationReady = false;
          this.firstPageVisitCaptured = false;
          // Session configuration
          this.sessionTimeout = options.sessionTimeout || 30 * 60 * 1000; // 30 minutes
          this.debounceDelay = options.debounceDelay ?? 200; // 200ms debounce
          this.sessionUpdateTimer = null;
          this.lastClickTime = null;
          this.debugMode = options.debug || false;
          this.enableDeveloperLogs = options.enableDeveloperLogs || false;
          this._hierarchyCache = new WeakMap();
          // Subscription status management
          this.subscriptionStatus = null;
          this.subscriptionCheckPromise = null;
          this.isTrackingEnabled = true; // Default to enabled until we check
          // Capture first page visit immediately
          this.capturePageVisit().catch((error) => {
              // Failed to capture first page visit
          });
          // Visitor generation tracking - load from cookies
          this.visitorGeneratedSent = this.getVisitorGeneratedSentFromCookie();
          this.lastSessionId = this.getLastSessionIdFromCookie();
          // Initialize device ID: check cookie first, generate if needed
          this.initializeDeviceId();
          // Set location as ready with null values initially
          this.locationReady = true;
          this.ipAddress = null;
          this.locationInfo = null;
          // Start subscription check in parallel (non-blocking)
          this.checkSubscriptionStatus().catch((error) => {
              console.warn('Failed to check subscription status, proceeding with tracking:', error);
          });
          // Initialize automatically if tracking is allowed
          if (this.shouldTrack()) {
              // Don't capture page visit immediately - wait for deviceId and location to be ready
              // this.autoCapturePageVisit(); // This will be called after both deviceId and location are ready
              if (this.trackClicks) {
                  this.autoCaptureClickEvents();
              }
              if (this.trackForms) {
                  this.autoCaptureFormEvents();
              }
          }
      }
      /**
       * Check subscription status with 24-hour caching
       */
      async checkSubscriptionStatus() {
          try {
              // Check if we already have this promise running
              if (this.subscriptionCheckPromise) {
                  return this.subscriptionCheckPromise;
              }
              // Check cache first
              const cachedStatus = this.getSubscriptionStatusFromCache();
              if (cachedStatus !== null) {
                  this.subscriptionStatus = cachedStatus.status;
                  this.isTrackingEnabled = !['cancelled', 'suspended'].includes(cachedStatus.status);
                  // Using cached subscription status
                  return Promise.resolve(this.isTrackingEnabled);
              }
              // Create the promise for API call
              this.subscriptionCheckPromise = this.fetchSubscriptionStatus();
              const result = await this.subscriptionCheckPromise;
              // Clear the promise once completed
              this.subscriptionCheckPromise = null;
              return result;
          }
          catch (error) {
              // Failed to check subscription status, default to enabled
              this.isTrackingEnabled = true;
              this.subscriptionCheckPromise = null;
              return true;
          }
      }
      /**
       * Fetch subscription status from API
       */
      async fetchSubscriptionStatus() {
          try {
              const response = await fetch(`${this.apiEndpoint}/subscriptionStatus`, {
                  method: 'GET',
                  headers: {
                      'Content-Type': 'application/json',
                      'x-api-key': `${this.apiKey}`,
                  },
              });
              if (!response.ok) {
                  throw new Error(`HTTP error ${response.status}`);
              }
              const data = await response.json();
              this.subscriptionStatus = data.status;
              // Cache the result for 24 hours
              this.setSubscriptionStatusCache(data.status);
              // Determine if tracking should be enabled
              this.isTrackingEnabled = !['cancelled', 'suspended'].includes(data.status);
              // Fetched subscription status
              return this.isTrackingEnabled;
          }
          catch (error) {
              // Failed to fetch subscription status, default to enabled
              this.isTrackingEnabled = true;
              return true;
          }
      }
      /**
       * Get subscription status from cache
       */
      getSubscriptionStatusFromCache() {
          try {
              const cacheKey = 'thrivestack_subscription_status';
              const cached = localStorage.getItem(cacheKey);
              if (!cached) {
                  return null;
              }
              const cacheData = JSON.parse(cached);
              const now = Date.now();
              const cacheAge = now - cacheData.timestamp;
              const twentyFourHours = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
              // Check if cache is still valid (less than 24 hours old)
              if (cacheAge < twentyFourHours) {
                  return cacheData;
              }
              else {
                  // Cache expired, remove it
                  localStorage.removeItem(cacheKey);
                  return null;
              }
          }
          catch (error) {
              // Failed to read subscription status from cache
              return null;
          }
      }
      /**
       * Set subscription status in cache with 24-hour expiration
       */
      setSubscriptionStatusCache(status) {
          try {
              const cacheKey = 'thrivestack_subscription_status';
              const cacheData = {
                  status: status,
                  timestamp: Date.now(),
              };
              localStorage.setItem(cacheKey, JSON.stringify(cacheData));
              // Subscription status cached
          }
          catch (error) {
              // Failed to cache subscription status
          }
      }
      /**
       * Generate fingerprint using IP address + User Agent hash (Plausible-style approach)
       * This method provides fast and privacy-friendly device identification
       */
      generateSimpleFingerprint() {
          this.startSimpleFingerprintGeneration();
      }
      /**
       * Generate fingerprint in background and send visitor_id_generated event when ready
       */
      async generateFingerprintInBackground() {
          try {
              // Start fingerprint generation immediately
              await this.generateSimpleFingerprintHash();
          }
          catch (error) {
              // Background fingerprint generation failed
          }
      }
      /**
       * Initialize device ID: Check cookie first, generate if needed
       */
      async initializeDeviceId() {
          try {
              // Get current session ID to check if it's a new session
              const currentSessionId = this.getSessionId();
              // First, check if we already have a valid device ID in cookie
              const existingDeviceId = this.getDeviceIdFromCookie();
              if (existingDeviceId && existingDeviceId.trim() !== '') {
                  this.deviceId = existingDeviceId.trim();
                  this.deviceIdReady = true;
                  // Using existing device ID from cookie
                  // Check if this is a new session with existing device ID
                  const shouldSendVisitorGenerated = this.shouldSendVisitorGeneratedEvent(currentSessionId);
                  if (shouldSendVisitorGenerated) {
                      // Send visitor_generated event for new session mapping (even with cached fingerprint)
                      this.sendFingerprintEventFromCache(this.deviceId);
                      this.visitorGeneratedSent = true;
                  }
                  this.lastSessionId = currentSessionId;
                  return;
              }
              // No existing device ID found, start IP fetching to generate one
              this.generateFingerprintInBackground();
          }
          catch (error) {
              // Critical: Device ID initialization failed
              // Fallback to session ID if critical error
              this.deviceId = this.getSessionId();
              this.deviceIdReady = true;
          }
      }
      /**
       * Start simple fingerprint generation immediately as TOP PRIORITY (background process)
       */
      startSimpleFingerprintGeneration() {
          // Start simple fingerprint generation in background
          this.generateSimpleFingerprintHash().catch((error) => {
              // Handle the failure silently
          });
      }
      /**
       * Generate simple fingerprint using IP address + User Agent hash (Plausible-style)
       */
      async generateSimpleFingerprintHash() {
          try {
              // Record timing
              const initStartTime = performance.now();
              // Get IP address with timing
              const ipStartTime = performance.now();
              const ipResult = await this.fetchIPAddress();
              const ipEndTime = performance.now();
              const ipFetchTime = ipEndTime - ipStartTime;
              // Get user agent
              const userAgent = navigator.userAgent;
              // Store location data in cookie and update instance variables if available
              if (ipResult.locationData) {
                  this.storeLocationDataInCookie(ipResult.locationData);
                  // Update instance location variables to avoid duplicate API calls
                  this.ipAddress = ipResult.locationData.ip;
                  this.locationInfo = {
                      city: ipResult.locationData.city || null,
                      region: ipResult.locationData.region || null,
                      country: ipResult.locationData.country || null,
                      country_code: ipResult.locationData.country_code || null,
                      postal: ipResult.locationData.postal || null,
                      loc: ipResult.locationData.latitude && ipResult.locationData.longitude
                          ? `${ipResult.locationData.latitude},${ipResult.locationData.longitude}`
                          : null,
                      timezone: ipResult.locationData.timezone || null,
                  };
                  this.locationReady = true;
                  // Location data used for both fingerprint and location info
              }
              // Create fingerprint data object
              const fingerprintData = {
                  ip_address: ipResult.ip,
                  ip_api_used: ipResult.apiUsed,
                  ip_fetch_time_ms: Math.round(ipFetchTime * 100) / 100,
                  user_agent: userAgent,
                  location_data_available: !!ipResult.locationData,
                  location_source: ipResult.locationData?.source_api || null,
                  timestamp: Date.now(),
              };
              // Generate hash from IP + User Agent using MurmurHash3
              const fingerprintString = `${ipResult.ip}${userAgent}`;
              const finalDeviceId = this.createMurmurHash(fingerprintString);
              // Calculate total generation time
              const totalGenerationTime = performance.now() - initStartTime;
              // Store the previous device ID for logging
              const previousDeviceId = this.deviceId;
              // Set the final device ID
              this.deviceId = finalDeviceId;
              this.deviceIdReady = true;
              this.setDeviceIdCookie(finalDeviceId);
              // Send visitor_generated event (separate from logging)
              this.sendFingerprintEvent(finalDeviceId, fingerprintData, totalGenerationTime);
          }
          catch (error) {
              // If fingerprint generation failed and didn't provide location data,
              // initialize location data separately as fallback
              if (!this.locationReady) {
                  // Fingerprint generation failed, initializing location data separately
                  this.initializeLocationData();
              }
              // Don't throw error - let caller handle the failure gracefully
              throw error;
          }
      }
      /**
       * Fetch IP address with fallback APIs for high availability
       */
      async fetchIPAddress() {
          // List of IP APIs in order of preference (unlimited > high limit > low limit)
          const ipApis = [
              {
                  url: 'https://ipwho.is/?output=json',
                  extractIp: (data) => data.ip,
                  extractLocation: (data) => this.extractIPWHOLocationData(data),
                  name: 'IPWHO',
                  hasLocationData: true,
              },
              {
                  url: 'https://ipinfo.io/json',
                  extractIp: (data) => data.ip,
                  extractLocation: (data) => this.extractIPInfoLocationData(data),
                  name: 'IPinfo Lite',
                  hasLocationData: true,
              },
              {
                  url: 'https://api.ipify.org/?format=json',
                  extractIp: (data) => data.ip,
                  extractLocation: () => null,
                  name: 'ipify',
                  hasLocationData: false,
              },
          ];
          for (const api of ipApis) {
              try {
                  const response = await fetch(api.url, {
                      method: 'GET',
                      headers: {
                          Accept: 'application/json',
                      },
                  });
                  if (!response.ok) {
                      throw new Error(`HTTP error! status: ${response.status}`);
                  }
                  const data = await response.json();
                  const ip = api.extractIp(data);
                  if (!ip || typeof ip !== 'string') {
                      throw new Error(`Invalid IP address response from ${api.name}`);
                  }
                  // Extract location data if available
                  const locationData = api.hasLocationData
                      ? api.extractLocation(data)
                      : null;
                  return {
                      ip: ip.trim(),
                      apiUsed: api.name,
                      locationData,
                      fullResponse: data,
                  };
              }
              catch (error) {
                  console.warn(`Failed to fetch IP from ${api.name}:`, error);
                  // Continue to next API
              }
          }
          throw new Error('Failed to fetch IP address from all available APIs');
      }
      /**
       * Extract location data from IPWHO API response
       */
      extractIPWHOLocationData(data) {
          try {
              return {
                  ip: data.ip,
                  country: data.country,
                  country_code: data.country_code,
                  region: data.region,
                  region_code: data.region_code,
                  city: data.city,
                  postal: data.postal,
                  latitude: data.latitude,
                  longitude: data.longitude,
                  timezone: data.timezone?.id || data.timezone,
                  timezone_offset: data.timezone?.utc,
                  isp: data.connection?.isp,
                  org: data.connection?.org,
                  asn: data.connection?.asn,
                  continent: data.continent,
                  continent_code: data.continent_code,
                  source_api: 'IPWHO',
                  timestamp: Date.now(),
              };
          }
          catch (error) {
              console.warn('Failed to extract IPWHO location data:', error);
              return null;
          }
      }
      /**
       * Extract location data from IPinfo API response
       */
      extractIPInfoLocationData(data) {
          try {
              // Parse loc field (latitude,longitude)
              const [latitude, longitude] = data.loc
                  ? data.loc.split(',').map(Number)
                  : [null, null];
              return {
                  ip: data.ip,
                  country: null, // IPinfo doesn't provide full country name in free tier
                  country_code: data.country,
                  region: data.region,
                  region_code: null,
                  city: data.city,
                  postal: data.postal,
                  latitude,
                  longitude,
                  timezone: data.timezone,
                  timezone_offset: null,
                  isp: null,
                  org: data.org,
                  asn: null,
                  continent: null,
                  continent_code: null,
                  source_api: 'IPinfo Lite',
                  timestamp: Date.now(),
              };
          }
          catch (error) {
              console.warn('Failed to extract IPinfo location data:', error);
              return null;
          }
      }
      /**
       * Store location data from IP API in cookie
       */
      storeLocationDataInCookie(locationData) {
          try {
              // Use the existing setLocationInfoCookie method
              this.setLocationInfoCookie(locationData);
              // Location data stored in cookie
          }
          catch (error) {
              // Failed to store location data in cookie
          }
      }
      /**
       * Create hash from input string using MurmurHash3
       */
      createMurmurHash(input) {
          return this.murmurHash3(input, 0x9747b28c).toString(16).padStart(8, '0');
      }
      /**
       * MurmurHash3 implementation for consistent, fast hashing
       * @param str - Input string to hash
       * @param seed - Hash seed (default: 0x9747b28c)
       * @returns 32-bit hash value
       */
      murmurHash3(str, seed = 0x9747b28c) {
          let h1 = seed;
          let h1b;
          let k1;
          const remainder = str.length & 3; // str.length % 4
          const bytes = str.length - remainder;
          let i = 0;
          const c1 = 0xcc9e2d51;
          const c2 = 0x1b873593;
          while (i < bytes) {
              k1 =
                  (str.charCodeAt(i) & 0xff) |
                      ((str.charCodeAt(++i) & 0xff) << 8) |
                      ((str.charCodeAt(++i) & 0xff) << 16) |
                      ((str.charCodeAt(++i) & 0xff) << 24);
              ++i;
              k1 =
                  ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
                      0xffffffff;
              k1 = (k1 << 15) | (k1 >>> 17);
              k1 =
                  ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
                      0xffffffff;
              h1 ^= k1;
              h1 = (h1 << 13) | (h1 >>> 19);
              h1b =
                  ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
              h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
          }
          k1 = 0;
          switch (remainder) {
              case 3:
                  k1 ^= (str.charCodeAt(i + 2) & 0xff) << 16;
              case 2:
                  k1 ^= (str.charCodeAt(i + 1) & 0xff) << 8;
              case 1:
                  k1 ^= str.charCodeAt(i) & 0xff;
                  k1 =
                      ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
                          0xffffffff;
                  k1 = (k1 << 15) | (k1 >>> 17);
                  k1 =
                      ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
                          0xffffffff;
                  h1 ^= k1;
          }
          h1 ^= str.length;
          h1 ^= h1 >>> 16;
          h1 =
              ((h1 & 0xffff) * 0x85ebca6b +
                  ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
                  0xffffffff;
          h1 ^= h1 >>> 13;
          h1 =
              ((h1 & 0xffff) * 0xc2b2ae35 +
                  ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
                  0xffffffff;
          h1 ^= h1 >>> 16;
          return h1 >>> 0;
      }
      /**
       * Set device ID in cookie
       */
      setDeviceIdCookie(deviceId) {
          if (!deviceId)
              return;
          const cookieName = 'thrivestack_device_id';
          // Set cookie with a 2 year expiration (in seconds)
          const expiryDate = new Date();
          expiryDate.setTime(expiryDate.getTime() + 730 * 24 * 60 * 60 * 1000); // 730 days
          // Set secure and SameSite attributes for better security
          const cookieValue = `${cookieName}=${deviceId};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
          try {
              document.cookie = cookieValue;
          }
          catch (e) {
              // Could not store device ID in cookie
          }
      }
      /**
       * Initialize location data - uses fingerprint data if available, otherwise fetches separately
       */
      async initializeLocationData() {
          try {
              // First check if we have cached data
              const cachedData = this.getLocationInfoFromCookie();
              if (cachedData) {
                  // Use cached data
                  this.ipAddress = cachedData.ip || null;
                  this.locationInfo = {
                      city: cachedData.city || null,
                      region: cachedData.region || null,
                      country: cachedData.country || null,
                      postal: cachedData.postal || null,
                      loc: cachedData.loc || null,
                      timezone: cachedData.timezone || null,
                  };
                  // Using cached IP and location info from cookie
                  this.locationReady = true;
                  return;
              }
              // If no cache, wait a bit for fingerprint generation to complete and provide location data
              // No cached location data, waiting for fingerprint generation to provide location data
              // Wait up to 2 seconds for fingerprint generation to complete
              let attempts = 0;
              const maxAttempts = 20; // 20 * 100ms = 2 seconds
              const checkFingerprintLocation = () => {
                  if (this.locationReady && this.ipAddress && this.locationInfo) {
                      // Location data provided by fingerprint generation
                      return;
                  }
                  attempts++;
                  if (attempts < maxAttempts) {
                      setTimeout(checkFingerprintLocation, 100);
                  }
                  else {
                      // Fallback: fetch location data separately if fingerprint didn't provide it
                      // Fingerprint generation did not provide location data, fetching separately
                      this.fetchIpAndLocationInfo();
                  }
              };
              setTimeout(checkFingerprintLocation, 100);
          }
          catch (error) {
              // Error in location data initialization
              this.locationReady = true;
          }
      }
      /**
       * Fetch IP address and location information with cookie caching
       * NOTE: This is now only used as a fallback when fingerprint generation doesn't provide location data
       */
      async fetchIpAndLocationInfo() {
          try {
              // Check if location data was already set by fingerprint generation
              if (this.locationReady && this.ipAddress && this.locationInfo) {
                  // Location data already available from fingerprint generation, skipping duplicate API call
                  return;
              }
              // First, try to get cached data from cookie
              const cachedData = this.getLocationInfoFromCookie();
              if (cachedData) {
                  // Use cached data
                  this.ipAddress = cachedData.ip || null;
                  this.locationInfo = {
                      city: cachedData.city || null,
                      region: cachedData.region || null,
                      country: cachedData.country || null,
                      postal: cachedData.postal || null,
                      loc: cachedData.loc || null,
                      timezone: cachedData.timezone || null,
                  };
                  // Using cached IP and location info from cookie
                  this.locationReady = true;
                  return;
              }
              // If no cached data and not set by fingerprint, fetch from fallback API
              // No cached location data found, fetching from fallback API
              const response = await fetch(this.geoIpServiceUrl);
              if (!response.ok) {
                  throw new Error(`HTTP error ${response.status}`);
              }
              const data = await response.json();
              // Store IP and location data
              this.ipAddress = data.ip || null;
              this.locationInfo = {
                  city: data.city || null,
                  region: data.region || null,
                  country: data.country || null,
                  postal: data.postal || null,
                  loc: data.loc || null, // Format: "lat,long"
                  timezone: data.timezone || null,
              };
              // Cache the data in cookie for future use
              this.setLocationInfoCookie(data);
              this.locationReady = true;
          }
          catch (error) {
              // Failed to fetch IP and location info
              // Set fallback values
              this.ipAddress = null;
              this.locationInfo = null;
              this.locationReady = true; // Mark as ready even with fallback values
          }
      }
      /**
       * Set location info in cookie with Base64 encoding
       */
      setLocationInfoCookie(locationData) {
          if (!locationData)
              return;
          const cookieName = 'thrivestack_location_info';
          try {
              // Encode data as Base64
              const encodedData = btoa(JSON.stringify(locationData));
              // Set cookie with 2 year expiration (same as device ID cookie)
              const expiryDate = new Date();
              expiryDate.setTime(expiryDate.getTime() + 730 * 24 * 60 * 60 * 1000); // 730 days
              const cookieValue = `${cookieName}=${encodedData};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
              document.cookie = cookieValue;
              // Location info cached in cookie
          }
          catch (e) {
              // Could not store location info in cookie
          }
      }
      /**
       * Get location info from cookie with Base64 decoding
       */
      getLocationInfoFromCookie() {
          const cookieName = 'thrivestack_location_info';
          try {
              const cookies = document.cookie.split(';');
              for (let i = 0; i < cookies.length; i++) {
                  const cookie = cookies[i].trim();
                  if (cookie.indexOf(cookieName + '=') === 0) {
                      const encodedValue = cookie.substring(cookieName.length + 1);
                      // Decode Base64 and parse JSON
                      const decodedValue = atob(encodedValue);
                      const locationData = JSON.parse(decodedValue);
                      return locationData;
                  }
              }
          }
          catch (e) {
              // Could not read location info from cookie
              // If cookie is corrupted, remove it
              this.removeLocationInfoCookie();
          }
          return null;
      }
      /**
       * Remove location info cookie (used when cookie is corrupted)
       */
      removeLocationInfoCookie() {
          const cookieName = 'thrivestack_location_info';
          try {
              document.cookie = `${cookieName}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;SameSite=Lax`;
          }
          catch (e) {
              // Could not remove location info cookie
          }
      }
      /**
       * Initialize ThriveStack and start tracking
       */
      async init(userId = '', source = '') {
          try {
              if (userId) {
                  this.setUserId(userId);
              }
              if (source) {
                  this.source = source;
              }
          }
          catch (error) {
              // Failed to initialize ThriveStack
          }
      }
      /**
       * Check if tracking is allowed based on consent settings
       */
      shouldTrack() {
          // Always allow functional tracking
          return true;
      }
      /**
       * Update consent settings for a tracking category
       */
      setConsent(category, hasConsent) {
          if (this.consentCategories.hasOwnProperty(category)) {
              this.consentCategories[category] = hasConsent;
          }
      }
      /**
       * Set user ID for tracking
       */
      setUserId(userId) {
          this.userId = userId;
          this.setUserIdCookie(userId);
      }
      /**
       * Set groupId in instance and cookie
       */
      setGroupId(groupId) {
          this.groupId = groupId;
          this.setGroupIdCookie(groupId);
      }
      /**
       * Set source for tracking
       */
      setSource(source) {
          this.source = source;
      }
      /**
       * Set userId in cookie
       */
      setUserIdCookie(userId) {
          if (!userId)
              return;
          const cookieName = 'thrivestack_user_id';
          // Set cookie with a 1 year expiration (in seconds)
          const expiryDate = new Date();
          expiryDate.setTime(expiryDate.getTime() + 365 * 24 * 60 * 60 * 1000); // 365 days
          // Set secure and SameSite attributes for better security
          const cookieValue = `${cookieName}=${encodeURIComponent(userId)};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
          try {
              document.cookie = cookieValue;
          }
          catch (e) {
              // Could not store user ID in cookie
          }
      }
      /**
       * Get userId from cookie
       */
      getUserIdFromCookie() {
          const cookieName = 'thrivestack_user_id';
          // Get value from cookies
          const cookies = document.cookie.split(';');
          for (let i = 0; i < cookies.length; i++) {
              const cookie = cookies[i].trim();
              if (cookie.indexOf(cookieName + '=') === 0) {
                  const value = cookie.substring(cookieName.length + 1);
                  return decodeURIComponent(value);
              }
          }
          return null;
      }
      /**
       * Set groupId in cookie
       */
      setGroupIdCookie(groupId) {
          if (!groupId)
              return;
          const cookieName = 'thrivestack_group_id';
          // Set cookie with a 1 year expiration (in seconds)
          const expiryDate = new Date();
          expiryDate.setTime(expiryDate.getTime() + 365 * 24 * 60 * 60 * 1000); // 365 days
          // Set secure and SameSite attributes for better security
          const cookieValue = `${cookieName}=${encodeURIComponent(groupId)};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
          try {
              document.cookie = cookieValue;
          }
          catch (e) {
              // Could not store group ID in cookie
          }
      }
      /**
       * Get groupId from cookie
       */
      getGroupIdFromCookie() {
          const cookieName = 'thrivestack_group_id';
          // Get value from cookies
          const cookies = document.cookie.split(';');
          for (let i = 0; i < cookies.length; i++) {
              const cookie = cookies[i].trim();
              if (cookie.indexOf(cookieName + '=') === 0) {
                  const value = cookie.substring(cookieName.length + 1);
                  return decodeURIComponent(value);
              }
          }
          return null;
      }
      /**
       * Get visitor generated sent status from cookie
       */
      getVisitorGeneratedSentFromCookie() {
          const cookieName = 'thrivestack_visitor_generated_sent';
          const cookies = document.cookie.split(';');
          for (let i = 0; i < cookies.length; i++) {
              const cookie = cookies[i].trim();
              if (cookie.indexOf(cookieName + '=') === 0) {
                  const value = cookie.substring(cookieName.length + 1);
                  return value === 'true';
              }
          }
          return false;
      }
      /**
       * Set visitor generated sent status in cookie
       */
      setVisitorGeneratedSentCookie(sent) {
          const cookieName = 'thrivestack_visitor_generated_sent';
          const expiryDate = new Date();
          expiryDate.setTime(expiryDate.getTime() + 365 * 24 * 60 * 60 * 1000); // 1 year
          const cookieValue = `${cookieName}=${sent};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
          try {
              document.cookie = cookieValue;
          }
          catch (e) {
              // Could not store visitor generated sent status in cookie
          }
      }
      /**
       * Get last session ID from cookie
       */
      getLastSessionIdFromCookie() {
          const cookieName = 'thrivestack_last_session_id';
          const cookies = document.cookie.split(';');
          for (let i = 0; i < cookies.length; i++) {
              const cookie = cookies[i].trim();
              if (cookie.indexOf(cookieName + '=') === 0) {
                  const value = cookie.substring(cookieName.length + 1);
                  return decodeURIComponent(value);
              }
          }
          return null;
      }
      /**
       * Set last session ID in cookie
       */
      setLastSessionIdCookie(sessionId) {
          const cookieName = 'thrivestack_last_session_id';
          const expiryDate = new Date();
          expiryDate.setTime(expiryDate.getTime() + 365 * 24 * 60 * 60 * 1000); // 1 year
          const cookieValue = `${cookieName}=${encodeURIComponent(sessionId)};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
          try {
              document.cookie = cookieValue;
          }
          catch (e) {
              // Could not store last session ID in cookie
          }
      }
      /**
       * Queue an event for batched sending
       */
      queueEvent(events) {
          // Handle both single events and arrays
          if (!Array.isArray(events)) {
              events = [events];
          }
          // Send immediately; device_id will be finalized at API send time in track()
          this.track(events).catch((error) => {
              console.error('Failed to send events:', error);
          });
      }
      /**
       * Track events by sending to ThriveStack API
       */
      async track(events) {
          if (!this.apiKey) {
              throw Error('Initialize the ThriveStack instance before sending telemetry data.');
          }
          // Check if tracking is enabled based on subscription status
          // Allow visitor_generated events even when tracking is disabled
          const allowedEvents = events.filter(event => this.isTrackingEnabled || event.event_name === 'visitor_generated');
          if (allowedEvents.length === 0) {
              console.debug('Tracking disabled due to subscription status, skipping events');
              return { success: false, reason: 'tracking_disabled' };
          }
          // Use only allowed events
          events = allowedEvents;
          // Finalize device_id at send time (use device ID from cookie if available, else session id)
          const sessionIdForSend = this.getSessionId();
          const deviceIdFromCookie = this.getDeviceIdFromCookie();
          const effectiveDeviceIdForSend = deviceIdFromCookie || sessionIdForSend;
          const eventsWithDevice = events.map((event) => ({
              ...event,
              context: {
                  ...(event.context || {}),
                  device_id: event.context && event.context.device_id
                      ? event.context.device_id
                      : effectiveDeviceIdForSend,
                  session_id: event.context && event.context.session_id
                      ? event.context.session_id
                      : sessionIdForSend,
              },
              // Normalize timestamp format for ClickHouse DateTime64(3) compatibility
              timestamp: new Date().toISOString(),
          }));
          // Clean events of PII before sending
          const cleanedEvents = eventsWithDevice.map((event) => this.cleanPIIFromEventData(event));
          // Add retry logic for network errors
          try {
              const response = await fetch(`${this.apiEndpoint}/track`, {
                  method: 'POST',
                  headers: {
                      'Content-Type': 'application/json',
                      'x-api-key': `${this.apiKey}`,
                  },
                  body: JSON.stringify(cleanedEvents),
              });
              if (!response.ok) {
                  throw Error(`HTTP error ${response.status}: ${await response.text()}`);
              }
              // Handle both 200 and 204 responses
              if (response.status === 204) {
                  // 204 No Content - success but no response body
                  return { success: true, status: 'no_content' };
              }
              else {
                  // 200 OK - parse JSON response
                  const data = await response.json();
                  return data;
              }
          }
          catch (error) {
              // Failed to send telemetry
              throw error;
          }
      }
      /**
       * Send user identification data
       */
      async identify(data) {
          if (!this.apiKey) {
              throw Error('Initialize the ThriveStack instance before sending telemetry data.');
          }
          // Check if tracking is enabled based on subscription status
          if (!this.isTrackingEnabled) {
              console.debug('Tracking disabled due to subscription status, skipping events');
              return { success: false, reason: 'tracking_disabled' };
          }
          try {
              let userId = '';
              if (Array.isArray(data) && data.length > 0) {
                  const lastElement = data[data.length - 1];
                  userId = lastElement.user_id || '';
              }
              else {
                  // Keep original logic for non-array data
                  const userData = data;
                  userId = userData.user_id || '';
              }
              // Set userId in instance and cookie
              if (userId) {
                  this.setUserId(userId);
              }
              // Add context data to identify requests
              const sessionId = this.getSessionId();
              const effectiveDeviceId = this.deviceId || this.getDeviceIdFromCookie();
              const currentGroupId = this.groupId || this.getGroupIdFromCookie() || '';
              // Process data to ensure context is included
              const dataWithContext = Array.isArray(data)
                  ? data.map(item => ({
                      ...item,
                      context: {
                          ...(item.context || {}),
                          device_id: (item.context && item.context.device_id) || effectiveDeviceId,
                          session_id: (item.context && item.context.session_id) || sessionId,
                          group_id: (item.context && item.context.group_id) || currentGroupId,
                          source: (item.context && item.context.source) || this.source
                      }
                  }))
                  : {
                      ...data,
                      context: {
                          ...(data.context || {}),
                          device_id: (data.context && data.context.device_id) || effectiveDeviceId,
                          session_id: (data.context && data.context.session_id) || sessionId,
                          group_id: (data.context && data.context.group_id) || currentGroupId,
                          source: (data.context && data.context.source) || this.source
                      }
                  };
              // Send data to API
              const response = await fetch(`${this.apiEndpoint}/identify`, {
                  method: 'POST',
                  headers: {
                      'Content-Type': 'application/json',
                      'x-api-key': `${this.apiKey}`,
                  },
                  body: JSON.stringify(dataWithContext),
              });
              if (!response.ok) {
                  throw Error(`HTTP error ${response.status}: ${await response.text()}`);
              }
              const result = await response.json();
              return result;
          }
          catch (error) {
              console.error('Failed to send identification data:', error instanceof Error ? error.message : 'Unknown error');
              throw error;
          }
      }
      /**
       * Send group data
       */
      async group(data) {
          if (!this.apiKey) {
              throw Error('Initialize the ThriveStack instance before sending telemetry data.');
          }
          // Check if tracking is enabled based on subscription status
          if (!this.isTrackingEnabled) {
              console.debug('Tracking disabled due to subscription status, skipping events');
              return { success: false, reason: 'tracking_disabled' };
          }
          try {
              // Extract groupId from data
              let groupId = '';
              if (Array.isArray(data) && data.length > 0) {
                  const lastElement = data[data.length - 1];
                  groupId = lastElement.group_id || '';
              }
              else {
                  // Keep original logic for non-array data
                  const groupData = data;
                  groupId = groupData.group_id || '';
              }
              // Store groupId in instance and cookie
              if (groupId) {
                  this.setGroupId(groupId);
              }
              // Add context data to group requests
              const sessionId = this.getSessionId();
              const effectiveDeviceId = this.deviceId || this.getDeviceIdFromCookie();
              const currentGroupId = this.groupId || this.getGroupIdFromCookie() || '';
              // Process data to ensure context is included
              const dataWithContext = Array.isArray(data)
                  ? data.map(item => ({
                      ...item,
                      context: {
                          ...(item.context || {}),
                          device_id: (item.context && item.context.device_id) || effectiveDeviceId,
                          session_id: (item.context && item.context.session_id) || sessionId,
                          group_id: (item.context && item.context.group_id) || currentGroupId,
                          source: (item.context && item.context.source) || this.source
                      }
                  }))
                  : {
                      ...data,
                      context: {
                          ...(data.context || {}),
                          device_id: (data.context && data.context.device_id) || effectiveDeviceId,
                          session_id: (data.context && data.context.session_id) || sessionId,
                          group_id: (data.context && data.context.group_id) || currentGroupId,
                          source: (data.context && data.context.source) || this.source
                      }
                  };
              // Send data to API
              const response = await fetch(`${this.apiEndpoint}/group`, {
                  method: 'POST',
                  headers: {
                      'Content-Type': 'application/json',
                      'x-api-key': `${this.apiKey}`,
                  },
                  body: JSON.stringify(dataWithContext),
              });
              if (!response.ok) {
                  throw Error(`HTTP error ${response.status}: ${await response.text()}`);
              }
              const result = await response.json();
              return result;
          }
          catch (error) {
              console.error('Failed to send group data:', error instanceof Error ? error.message : 'Unknown error');
              throw error;
          }
      }
      /**
       * Get UTM parameters from URL
       */
      getUtmParameters() {
          const urlParams = new URLSearchParams(window.location.search);
          return {
              utm_campaign: urlParams.get('utm_campaign') || null,
              utm_medium: urlParams.get('utm_medium') || null,
              utm_source: urlParams.get('utm_source') || null,
              utm_term: urlParams.get('utm_term') || null,
              utm_content: urlParams.get('utm_content') || null,
          };
      }
      /**
       * Get device ID - now returns the actual device ID or null if not ready
       */
      getDeviceId() {
          if (this.deviceIdReady && this.deviceId) {
              return this.deviceId;
          }
          return null;
      }
      /**
       * Get device ID from cookie
       */
      getDeviceIdFromCookie() {
          const cookieName = 'thrivestack_device_id';
          // Try to get existing device ID from cookies
          const cookies = document.cookie.split(';');
          // Look for our specific cookie
          for (let i = 0; i < cookies.length; i++) {
              const cookie = cookies[i].trim();
              if (cookie.indexOf(cookieName + '=') === 0) {
                  return cookie.substring(cookieName.length + 1);
              }
          }
          return null;
      }
      /**
       * Get session ID from cookie
       */
      getSessionId() {
          const sessionCookieName = 'thrivestack_session';
          try {
              // Try to get existing session from cookies
              const cookies = document.cookie.split(';');
              let sessionCookieValue = null;
              // Look for our specific cookie
              for (let i = 0; i < cookies.length; i++) {
                  const cookie = cookies[i].trim();
                  if (cookie.indexOf(sessionCookieName + '=') === 0) {
                      sessionCookieValue = cookie.substring(sessionCookieName.length + 1);
                      break;
                  }
              }
              if (sessionCookieValue) {
                  // Try to parse as new format (Base64 JSON)
                  try {
                      const sessionData = JSON.parse(atob(sessionCookieValue));
                      // Validate structure
                      if (sessionData.sessionId && sessionData.lastActivity) {
                          const lastActivity = new Date(sessionData.lastActivity);
                          const now = new Date();
                          const timeSinceLastActivity = now.getTime() - lastActivity.getTime();
                          // Check if session is still valid (within timeout)
                          if (timeSinceLastActivity < this.sessionTimeout) {
                              return sessionData.sessionId;
                          }
                          else {
                              // Session expired, create new one
                              console.debug('Session expired, creating new session');
                              return this.createNewSession();
                          }
                      }
                      else {
                          // Invalid structure, create new session
                          return this.createNewSession();
                      }
                  }
                  catch (parseError) {
                      // Not Base64 JSON, assume old format (plain session ID)
                      console.debug('Migrating old session format to new format');
                      return this.migrateOldSession(sessionCookieValue);
                  }
              }
              else {
                  // No existing session, create new one
                  return this.createNewSession();
              }
          }
          catch (error) {
              console.warn('Error getting session ID:', error);
              return this.createNewSession();
          }
      }
      /**
       * Create a new session
       */
      createNewSession() {
          const sessionId = 'session_' + Math.random().toString(36).substring(2, 15);
          const now = new Date().toISOString();
          const sessionData = {
              sessionId: sessionId,
              startTime: now,
              lastActivity: now,
          };
          this.setSessionCookie(sessionData);
          return sessionId;
      }
      /**
       * Migrate old session format
       */
      migrateOldSession(oldSessionId) {
          const now = new Date().toISOString();
          const sessionData = {
              sessionId: oldSessionId,
              startTime: now, // We don't know the original start time
              lastActivity: now,
          };
          this.setSessionCookie(sessionData);
          return oldSessionId;
      }
      /**
       * Set session cookie with new format
       */
      setSessionCookie(sessionData) {
          const sessionCookieName = 'thrivestack_session';
          try {
              const encodedData = btoa(JSON.stringify(sessionData));
              // Set cookie with 1 year expiration (same as other tracking cookies)
              const expiryDate = new Date();
              expiryDate.setTime(expiryDate.getTime() + 365 * 24 * 60 * 60 * 1000); // 1 year
              const cookieValue = `${sessionCookieName}=${encodedData};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
              document.cookie = cookieValue;
          }
          catch (error) {
              console.warn('Could not store session in cookie:', error);
          }
      }
      /**
       * Update session activity with debouncing
       */
      updateSessionActivity() {
          // Clear existing timer
          if (this.sessionUpdateTimer) {
              clearTimeout(this.sessionUpdateTimer);
          }
          // Set new debounced timer
          this.sessionUpdateTimer = setTimeout(() => {
              this.updateSessionActivityImmediate();
          }, this.debounceDelay);
      }
      /**
       * Immediate session activity update
       */
      updateSessionActivityImmediate() {
          const sessionCookieName = 'thrivestack_session';
          try {
              // Get current session data
              const cookies = document.cookie.split(';');
              let sessionCookieValue = null;
              for (let i = 0; i < cookies.length; i++) {
                  const cookie = cookies[i].trim();
                  if (cookie.indexOf(sessionCookieName + '=') === 0) {
                      sessionCookieValue = cookie.substring(sessionCookieName.length + 1);
                      break;
                  }
              }
              if (sessionCookieValue) {
                  try {
                      const sessionData = JSON.parse(atob(sessionCookieValue));
                      // Update last activity
                      sessionData.lastActivity = new Date().toISOString();
                      // Save updated session
                      this.setSessionCookie(sessionData);
                  }
                  catch (parseError) {
                      // If parsing fails, create new session
                      this.createNewSession();
                  }
              }
              else {
                  // No session exists, create new one
                  this.createNewSession();
              }
          }
          catch (error) {
              console.warn('Could not update session activity:', error);
          }
      }
      /**
       * Check if element has ThriveStack event class
       */
      hasThriveStackEventClass(element) {
          if (!element || !element.className)
              return undefined;
          // Handle both string and DOMTokenList className
          const classList = typeof element.className === 'string'
              ? element.className.split(/\s+/)
              : Array.from(element.classList);
          return classList.find((className) => className.includes('thrivestack-event'));
      }
      /**
       * Capture page visit event
       */
      async capturePageVisit() {
          // Check if we need to send visitor_generated for new session
          if (this.shouldSendVisitorGeneratedForNewSession()) {
              const deviceIdFromCookie = this.getDeviceIdFromCookie();
              if (deviceIdFromCookie) {
                  // Send visitor_generated event for new session with existing device
                  this.sendFingerprintEventFromCache(deviceIdFromCookie);
              }
          }
          // Allow first page visit and visitor_generated events regardless of subscription status
          // Only block subsequent page visits if subscription is disabled
          if (this.firstPageVisitCaptured && !this.isTrackingEnabled) {
              console.debug('Subsequent page visit tracking disabled due to subscription status');
              return;
          }
          // Get UTM parameters if marketing consent is given
          const utmParams = this.getUtmParameters();
          // Validate session first, then update activity
          const sessionId = this.getSessionId();
          this.updateSessionActivity();
          const effectiveDeviceId = this.getDeviceIdFromCookie() || sessionId;
          // Get user and group IDs (from instance or cookies)
          const currentUserId = this.userId || this.getUserIdFromCookie() || '';
          const currentGroupId = this.groupId || this.getGroupIdFromCookie() || '';
          // Build event with IP and location info
          const events = [
              {
                  event_name: 'page_visit',
                  properties: {
                      page_title: document.title,
                      page_url: window.location.href,
                      page_path: window.location.pathname,
                      page_referrer: document.referrer || "",
                      language: navigator.language || "",
                      ip_address: this.ipAddress || "",
                      city: this.locationInfo?.city || "",
                      region: this.locationInfo?.region || "",
                      country: this.locationInfo?.country || "",
                      postal: this.locationInfo?.postal || "",
                      loc: this.locationInfo?.loc || "", // Format: "lat,long"
                      timezone: this.locationInfo?.timezone || "",
                      ...utmParams,
                  },
                  user_id: currentUserId,
                  context: {
                      group_id: currentGroupId,
                      device_id: effectiveDeviceId,
                      session_id: sessionId,
                      source: this.source, // Add source to context
                  },
                  timestamp: new Date().toISOString(),
              },
          ];
          // Queue event instead of sending immediately
          this.queueEvent(events);
          // Mark first page visit as captured and update session tracking
          this.firstPageVisitCaptured = true;
          const currentSessionId = this.getSessionId();
          this.lastSessionId = currentSessionId;
          // Save to cookies
          this.setLastSessionIdCookie(currentSessionId);
      }
      /**
       * Capture element click event
       */
      captureClickEvent(event) {
          // Check if tracking is enabled based on subscription status
          if (!this.isTrackingEnabled) {
              console.debug('Click event tracking disabled due to subscription status');
              return;
          }
          const target = event.target;
          const thrviestackClass = this.hasThriveStackEventClass(target);
          // NEW: Check if element has thrivestack-event class
          if (!thrviestackClass) {
              console.debug("Element does not have 'thrivestack-event' class, skipping click tracking");
              return;
          }
          // Throttle if too many clicks
          const now = Date.now();
          if (this.lastClickTime && now - this.lastClickTime < 300) {
              return;
          }
          this.lastClickTime = now;
          const position = target.getBoundingClientRect();
          // Get UTM parameters if marketing consent is given
          const utmParams = this.getUtmParameters();
          // Validate session first, then update activity
          const sessionId = this.getSessionId();
          this.updateSessionActivity();
          const effectiveDeviceId = this.getDeviceIdFromCookie() || sessionId;
          // Get user and group IDs (from instance or cookies)
          const currentUserId = this.userId || this.getUserIdFromCookie() || '';
          const currentGroupId = this.groupId || this.getGroupIdFromCookie() || '';
          const events = [
              {
                  event_name: 'element_click',
                  properties: {
                      page_title: document.title,
                      page_url: window.location.href,
                      page_path: window.location.pathname,
                      element_text: target.textContent?.trim() || null,
                      element_tag: target.tagName || null,
                      element_id: target.id || null,
                      element_href: target.getAttribute('href') || null,
                      element_aria_label: target.getAttribute('aria-label') || null,
                      element_class: target.className || null,
                      element_position_left: position.left || null,
                      element_position_top: position.top || null,
                      viewport_height: window.innerHeight,
                      viewport_width: window.innerWidth,
                      page_referrer: document.referrer || null,
                      language: navigator.language || null,
                      ip_address: this.ipAddress,
                      city: this.locationInfo?.city || null,
                      region: this.locationInfo?.region || null,
                      country: this.locationInfo?.country || null,
                      postal: this.locationInfo?.postal || null,
                      loc: this.locationInfo?.loc || null, // Format: "lat,long"
                      timezone: this.locationInfo?.timezone || null,
                      activity: thrviestackClass.split(':')[1] || thrviestackClass || null, // Capture the class name
                      ...utmParams,
                  },
                  user_id: currentUserId,
                  context: {
                      group_id: currentGroupId,
                      device_id: effectiveDeviceId,
                      session_id: sessionId,
                      source: this.source, // Add source to context
                  },
                  timestamp: new Date().toISOString(),
              },
          ];
          // Queue event instead of sending immediately
          this.queueEvent(events);
      }
      /**
       * Capture form events (submission, abandonment)
       */
      captureFormEvent(event, type) {
          // Check if tracking is enabled based on subscription status
          if (!this.isTrackingEnabled) {
              console.debug('Form event tracking disabled due to subscription status');
              return;
          }
          const form = event.target;
          // Validate session first, then update activity
          const sessionId = this.getSessionId();
          this.updateSessionActivity();
          const effectiveDeviceId = this.getDeviceIdFromCookie() || sessionId;
          const currentUserId = this.userId || this.getUserIdFromCookie() || '';
          const currentGroupId = this.groupId || this.getGroupIdFromCookie() || '';
          // Calculate form completion percentage
          const formData = form._trackingData || {
              filledFields: new Set(),
          };
          const totalFields = Array.from(form.elements).filter((e) => !['submit', 'button', 'reset'].includes(e.type)).length;
          const completionPercent = Math.round((formData.filledFields.size / Math.max(totalFields, 1)) * 100);
          const events = [
              {
                  event_name: `form_${type}`,
                  properties: {
                      page_title: document.title,
                      page_url: window.location.href,
                      form_id: form.id || null,
                      form_name: form.name || null,
                      form_action: form.action || null,
                      form_fields: totalFields,
                      form_completion: completionPercent,
                      interaction_time: formData.startTime
                          ? Date.now() - formData.startTime
                          : null,
                  },
                  user_id: currentUserId,
                  context: {
                      group_id: currentGroupId,
                      device_id: effectiveDeviceId,
                      session_id: sessionId,
                      source: this.source, // Add source to context
                  },
                  timestamp: new Date().toISOString(),
              },
          ];
          // Queue event instead of sending immediately
          this.queueEvent(events);
      }
      /**
       * Set up automatic page visit tracking
       */
      autoCapturePageVisit() {
          // Check if tracking is enabled based on subscription status
          if (!this.isTrackingEnabled) {
              console.debug('Auto page visit tracking disabled due to subscription status');
              return;
          }
          // Initial page visit will be captured when script loads, not on window.load
          // Track navigation events
          window.addEventListener('popstate', () => this.capturePageVisit());
          // Track history API calls for SPA support
          const originalPushState = history.pushState;
          history.pushState = (...args) => {
              originalPushState.apply(history, args);
              this.capturePageVisit();
          };
      }
      /**
       * Set up automatic click event tracking
       */
      autoCaptureClickEvents() {
          // Don't set up auto-tracking if subscription is disabled
          if (!this.isTrackingEnabled) {
              console.debug('Auto click tracking disabled due to subscription status');
              return;
          }
          document.addEventListener('click', (event) => this.captureClickEvent(event));
      }
      /**
       * Set up automatic form event tracking
       */
      autoCaptureFormEvents() {
          // Don't set up auto-tracking if subscription is disabled
          if (!this.isTrackingEnabled) {
              console.debug('Auto form tracking disabled due to subscription status');
              return;
          }
          // Track form submissions
          document.addEventListener('submit', (event) => {
              this.captureFormEvent(event, 'submit');
          });
          // Track form field interactions
          document.addEventListener('input', (event) => {
              const target = event.target;
              if (target.form) {
                  const form = target.form;
                  if (!form._trackingData) {
                      form._trackingData = {
                          startTime: Date.now(),
                          filledFields: new Set(),
                      };
                  }
                  // Track field completion
                  const field = event.target;
                  if (field.value.trim() !== '') {
                      form._trackingData.filledFields.add(field.name || field.id);
                  }
                  else {
                      form._trackingData.filledFields.delete(field.name || field.id);
                  }
              }
          });
          // Track form abandonment
          document.addEventListener('visibilitychange', () => {
              if (document.visibilityState === 'hidden') {
                  // Find forms with tracking data
                  document.querySelectorAll('form').forEach((form) => {
                      if (form._trackingData &&
                          form._trackingData.filledFields.size > 0) {
                          // Create a synthetic event with the form as target
                          const event = {
                              target: form,
                          };
                          this.captureFormEvent(event, 'abandoned');
                      }
                  });
              }
          });
      }
      /**
       * Automatically detect and clean PII from event data
       */
      cleanPIIFromEventData(eventData) {
          // Deep clone to avoid modifying original
          const cleanedData = JSON.parse(JSON.stringify(eventData));
          return cleanedData;
      }
      /**
       * Set user information and optionally make identify API call
       */
      async setUser(userId, emailId, properties = {}) {
          if (!userId) {
              console.warn('setUser: userId is required');
              return null;
          }
          // Check if we need to make API call
          const currentUserId = this.getUserIdFromCookie();
          const shouldMakeApiCall = !currentUserId || currentUserId !== userId;
          // Always update local state and cookie
          this.setUserId(userId);
          if (shouldMakeApiCall) {
              try {
                  // Prepare identify payload
                  const identifyData = [
                      {
                          user_id: userId,
                          context: {
                              device_id: '',
                              session_id: '',
                              group_id: '',
                              source: ''
                          },
                          traits: {
                              user_email: emailId,
                              user_name: emailId,
                              ...properties,
                          },
                          timestamp: new Date().toISOString(),
                      },
                  ];
                  console.debug('Making identify API call for user:', userId);
                  const result = await this.identify(identifyData);
                  console.debug('Identify API call successful');
                  return result;
              }
              catch (error) {
                  console.error('Failed to make identify API call:', error);
                  throw error;
              }
          }
          else {
              console.debug('Skipping identify API call - user already set in cookie:', userId);
              return null;
          }
      }
      /**
       * Set group information and optionally make group API call
       */
      async setGroup(groupId, groupDomain, groupName, properties = {}) {
          if (!groupId) {
              console.warn('setGroup: groupId is required');
              return null;
          }
          // Check if we need to make API call
          const currentGroupId = this.getGroupIdFromCookie();
          const shouldMakeApiCall = !currentGroupId || currentGroupId !== groupId;
          // Always update local state and cookie
          this.setGroupId(groupId);
          if (shouldMakeApiCall) {
              try {
                  // Prepare group payload
                  const groupData = [
                      {
                          group_id: groupId,
                          user_id: this.userId || this.getUserIdFromCookie() || undefined,
                          context: {
                              device_id: '',
                              session_id: '',
                              group_id: '',
                              source: ''
                          },
                          traits: {
                              group_type: 'Account',
                              account_domain: groupDomain,
                              account_name: groupName,
                              ...properties,
                          },
                          timestamp: new Date().toISOString(),
                      },
                  ];
                  console.debug('Making group API call for group:', groupId);
                  const result = await this.group(groupData);
                  console.debug('Group API call successful');
                  return result;
              }
              catch (error) {
                  console.error('Failed to make group API call:', error);
                  throw error;
              }
          }
          else {
              console.debug('Skipping group API call - group already set in cookie:', groupId);
              return null;
          }
      }
      /**
       * Public method to report email abuse
       * @param email - The email address to report for abuse
       * @returns Promise<EmailAbuseResponse> - Response from the abuse API
       */
      async reportEmailAbuse(email) {
          try {
              // Validate email parameter
              if (!email || typeof email !== 'string' || email.trim() === '') {
                  throw new Error('Email address is required and must be a non-empty string');
              }
              const trimmedEmail = email.trim();
              // Basic email validation
              const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
              if (!emailRegex.test(trimmedEmail)) {
                  throw new Error('Invalid email address format');
              }
              // Email abuse report (developer logs disabled)
              // Prepare the API endpoint
              // Handle both '/api/track' and '/api' endpoints
              let abuseApiEndpoint;
              if (this.apiEndpoint.endsWith('/api/track')) {
                  abuseApiEndpoint = this.apiEndpoint.replace('/api/track', '/api/email-abuse');
              }
              else if (this.apiEndpoint.endsWith('/api')) {
                  abuseApiEndpoint = this.apiEndpoint + '/email-abuse';
              }
              else {
                  // Fallback: assume it's a base URL and append the path
                  abuseApiEndpoint =
                      this.apiEndpoint.replace(/\/$/, '') + '/api/email-abuse';
              }
              // Make the API call
              const response = await fetch(abuseApiEndpoint, {
                  method: 'POST',
                  headers: {
                      'Content-Type': 'application/json',
                      'x-api-key': this.apiKey,
                  },
                  body: JSON.stringify({
                      email: trimmedEmail,
                  }),
              });
              if (!response.ok) {
                  const errorText = await response.text();
                  throw new Error(`Email abuse API failed: ${response.status} ${response.statusText} - ${errorText}`);
              }
              const result = await response.json();
              // Email abuse reported successfully (developer logs disabled)
              // Interpret the response
              const isAbuse = result.isAbuse === 1;
              const reasons = result.reasonForAbuse;
              let message = isAbuse
                  ? `Email marked as abuse. Reasons: ${Object.entries(reasons)
                    .filter(([_, value]) => value === true)
                    .map(([key, _]) => key)
                    .join(', ') || 'General abuse detected'}`
                  : 'Email is not flagged as abuse';
              return {
                  success: true,
                  message: message,
                  data: result,
              };
          }
          catch (error) {
              // Failed to report email abuse (developer logs disabled)
              return {
                  success: false,
                  message: error instanceof Error ? error.message : 'Unknown error occurred',
              };
          }
      }
      /**
       * Enable debug mode for troubleshooting
       */
      enableDebugMode() {
          this.debugMode = true;
          // Override track method to log events
          const originalTrack = this.track.bind(this);
          this.track = async function (events) {
              console.group('ThriveStack Debug: Sending Events');
              console.log('Events:', JSON.parse(JSON.stringify(events)));
              console.groupEnd();
              return originalTrack(events);
          };
          console.log('ThriveStack debug mode enabled');
      }
  }
  // Removed startLocationEnhancement - location fetching now handled by ThriveStack instance
  // Removed capturePageVisitWithEnhancement - no longer needed with new blocking architecture
  /**
   * Auto-initialize ThriveStack when script loads via CDN
   */
  (function () {
      // Only run CDN initialization if this is loaded as a script tag, not as a module
      if (typeof document === 'undefined' || typeof window === 'undefined') {
          return; // Not in browser environment
      }
      // Check if this is loaded as a script tag (CDN usage) vs module import
      const script = document.currentScript ||
          Array.from(document.getElementsByTagName('script')).pop();
      // If no script tag found, this is likely a module import - skip CDN initialization
      if (!script) {
          return;
      }
      // Extract script configuration
      const apiKey = script?.getAttribute('data-api-key') || script?.getAttribute('api-key');
      const source = script?.getAttribute('data-source') || script?.getAttribute('source');
      if (!apiKey || !source) {
          if (!apiKey) {
              console.error('Missing required API key for ThriveStack initialization. Use data-api-key or api-key attribute.');
          }
          if (!source) {
              console.error('Missing required source for ThriveStack initialization. Use data-source or source attribute.');
          }
          return;
      }
      // Create ThriveStack instance immediately - fingerprint generation starts in constructor
      console.debug('Creating ThriveStack instance with fingerprint priority...');
      const thriveStack = new ThriveStack({
          apiKey: apiKey,
          trackClicks: script?.getAttribute('data-track-clicks') !== 'false' &&
              script?.getAttribute('track-clicks') !== 'false',
          trackForms: script?.getAttribute('data-track-forms') === 'true' ||
              script?.getAttribute('track-forms') === 'true',
          enableDeveloperLogs: script?.getAttribute('data-enable-developer-logs') === 'true' ||
              script?.getAttribute('enable-developer-logs') === 'true',
          source: source,
      });
      // Initialize immediately
      thriveStack.init().catch((error) => {
          console.error('Failed to initialize ThriveStack:', error);
      });
      // Make instance available globally
      window.thriveStack = thriveStack;
      console.debug('ThriveStack initialization complete (fingerprint generation started)');
  })();

  // Singleton instance
  let thrivestackInstance = null;
  /**
   * Initialize ThriveStack analytics
   * @param apiKey - Your ThriveStack API key
   * @param source - Optional source identifier
   * @param options - Optional configuration options
   */
  async function init(apiKey, source, options) {
      if (thrivestackInstance) {
          console.warn('ThriveStack already initialized');
          return;
      }
      const config = {
          apiKey,
          source: source || '',
          ...options,
      };
      thrivestackInstance = new ThriveStack(config);
      await thrivestackInstance.init();
  }
  /**
   * Set user information
   * @param email - User's email address
   * @param userId - User's unique identifier
   * @param properties - Optional user properties
   */
  async function setUser(email, userId, properties = {}) {
      if (!thrivestackInstance) {
          throw new Error('ThriveStack not initialized. Call init() first.');
      }
      return await thrivestackInstance.setUser(userId, email, properties);
  }
  /**
   * Set group information
   * @param groupId - Group's unique identifier
   * @param groupDomain - Group's domain
   * @param groupName - Group's name
   * @param properties - Optional group properties
   */
  async function setGroup(groupId, groupDomain, groupName, properties = {}) {
      if (!thrivestackInstance) {
          throw new Error('ThriveStack not initialized. Call init() first.');
      }
      return await thrivestackInstance.setGroup(groupId, groupDomain, groupName, properties);
  }
  /**
   * Track custom events
   * @param eventName - Name of the event
   * @param properties - Optional event properties
   */
  async function track(eventName, properties = {}) {
      if (!thrivestackInstance) {
          throw new Error('ThriveStack not initialized. Call init() first.');
      }
      // Use the instance's internal tracking mechanism
      // This will automatically handle context, batching, and device ID
      const event = {
          event_name: eventName,
          properties,
          context: {
              device_id: thrivestackInstance.getDeviceId(),
              group_id: '',
              session_id: '',
              source: '',
          },
          timestamp: new Date().toISOString(),
      };
      // Use the instance's track method which handles all the processing
      return await thrivestackInstance.track([event]);
  }
  /**
   * Track multiple events using array type (ThriveStackEvent[])
   * @param events - Array of ThriveStackEvent objects
   */
  async function trackEvents(events) {
      if (!thrivestackInstance) {
          throw new Error('ThriveStack not initialized. Call init() first.');
      }
      return await thrivestackInstance.track(events);
  }
  /**
   * Get the ThriveStack instance (for advanced usage)
   */
  function getInstance() {
      return thrivestackInstance;
  }
  /**
   * Check if ThriveStack is initialized
   */
  function isInitialized() {
      return thrivestackInstance !== null;
  }

  exports.getInstance = getInstance;
  exports.init = init;
  exports.isInitialized = isInitialized;
  exports.setGroup = setGroup;
  exports.setUser = setUser;
  exports.track = track;
  exports.trackEvents = trackEvents;

}));
//# sourceMappingURL=thrivestack.js.map