/** * Production UTM Session Tracker for Static Sites * Preserves UTM parameters across traditional page navigation using localStorage * Includes Confluent-style internal tracking and auto-integration with analytics tools * * Features: * - Session-based UTM parameter preservation (30min timeout) * - DOM link modification for visible UTM parameters * - Confluent property internal tracking (www.confluent.io, *.confluent.cloud) * - Auto-detection and integration with Segment Analytics * - Auto-integration with Google Analytics (GA4 & Universal) * - Auto-integration with Facebook Pixel * - Dynamic content observation (MutationObserver) * - Production-ready error handling and debugging * * Auto-Integration: * - Segment: Automatically uses analytics.user().anonymousId() * - Google Analytics: Sends UTM data to gtag/ga events * - Facebook Pixel: Sends UTM data to fbq custom events * * @version 1.2.0 * @author Phaneendra Charyulu Kanduri * @license MIT */ (function (global) { 'use strict'; /** * UTM Session Tracker Class * Optimized for static multi-page websites with traditional navigation */ class UTMSessionTracker { /** * Initialize UTM Session Tracker * @param {Object} options - Configuration options * @param {boolean} options.debug - Enable debug logging * @param {number} options.sessionTimeout - Session timeout in milliseconds * @param {string} options.storageKey - localStorage key name * @param {Array} options.customUtmKeys - Additional UTM keys to track * @param {boolean} options.enableLinkTracking - Enable automatic UTM appending to links * @param {Array} options.excludeDomains - Domains to exclude from UTM appending * @param {Array} options.confluentDomains - Confluent domains for internal tracking * @param {boolean} options.enableConfluentTracking - Enable Confluent-style internal tracking * @param {string} options.anonymousId - Segment anonymous ID for Confluent Cloud tracking * @param {Function} options.onSessionUpdate - Callback for session updates * @param {Function} options.onError - Error callback */ constructor(options = {}) { // Configuration this.config = { sessionTimeout: options.sessionTimeout || 30 * 60000, // 30 minutes storageKey: options.storageKey || 'cnfl_session', sessionRefKey: 'session_ref', debug: options.debug || false, customUtmKeys: options.customUtmKeys || [], enableLinkTracking: options.enableLinkTracking !== false, // Default true excludeDomains: options.excludeDomains || [], confluentDomains: options.confluentDomains || ['confluent.io', 'confluent.cloud'], enableConfluentTracking: options.enableConfluentTracking !== false, // Default true anonymousId: options.anonymousId || null, onSessionUpdate: options.onSessionUpdate || null, onError: options.onError || null, }; // Core UTM parameters to track this.UTM_KEYS = [ 'utm_medium', 'utm_source', 'utm_campaign', 'utm_term', 'utm_content', 'utm_partner', 'placement', 'device', 'creative', 'gclid', 'fbclid', ...this.config.customUtmKeys, ]; // Internal state this.state = { locations: [], isInitialized: false, hasStorageSupport: this._checkStorageSupport(), }; // Auto-initialize if in browser environment if (this._isBrowserEnvironment()) { this._initialize(); } } /** * Check if running in browser environment * @private */ _isBrowserEnvironment() { return ( typeof window !== 'undefined' && typeof document !== 'undefined' && typeof localStorage !== 'undefined' ); } /** * Check localStorage support * @private */ _checkStorageSupport() { try { if (!this._isBrowserEnvironment()) return false; const testKey = '__utm_tracker_test__'; localStorage.setItem(testKey, 'test'); localStorage.removeItem(testKey); return true; } catch (error) { this._handleError('localStorage not supported', error); return false; } } /** * Handle errors with optional callback * @private */ _handleError(message, error = null) { const errorObj = { message, error, timestamp: Date.now(), userAgent: this._isBrowserEnvironment() ? navigator.userAgent : 'server', }; if (this.config.debug) { console.warn('[UTM Tracker]', message, error); // eslint-disable-line no-console } if (this.config.onError && typeof this.config.onError === 'function') { try { this.config.onError(errorObj); } catch (callbackError) { console.error('[UTM Tracker] Error in error callback:', callbackError); // eslint-disable-line no-console } } } /** * Debug logging * @private */ _log(...args) { if (this.config.debug) { console.log('[UTM Tracker]', ...args); // eslint-disable-line no-console } } /** * Generate cryptographically strong UUID v4 * @private */ _generateUUID() { if (this._isBrowserEnvironment() && window.crypto && window.crypto.getRandomValues) { // Use crypto.getRandomValues for better randomness const array = new Uint8Array(16); crypto.getRandomValues(array); // Set version (4) and variant bits array[6] = (array[6] & 0x0f) | 0x40; array[8] = (array[8] & 0x3f) | 0x80; const hex = Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(''); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; } // Fallback for older browsers return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } /** * Get expiration timestamp * @private */ _getExpiration() { return Date.now() + this.config.sessionTimeout; } /** * Check if timestamp is expired * @private */ _isExpired(timestamp) { return !timestamp || timestamp < Date.now(); } /** * Check if object is empty * @private */ _isEmpty(obj) { return !obj || Object.keys(obj).length === 0; } /** * Deep object equality check * @private */ _isEqual(obj1, obj2) { if (obj1 === obj2) return true; if (!obj1 || !obj2) return false; const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) return false; return keys1.every((key) => obj1[key] === obj2[key]); } /** * Pick specified keys from object * @private */ _pick(obj, keys) { if (!obj) return {}; const result = {}; keys.forEach((key) => { if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== undefined) { result[key] = obj[key]; } }); return result; } /** * Safely parse JSON with error handling * @private */ _safeJSONParse(str) { try { return JSON.parse(str); } catch (error) { this._handleError('Failed to parse JSON', error); return null; } } /** * Safely stringify JSON with error handling * @private */ _safeJSONStringify(obj) { try { return JSON.stringify(obj); } catch (error) { this._handleError('Failed to stringify JSON', error); return null; } } /** * Get query parameters from URL * @param {Array} filterKeys - Keys to filter for * @private */ _getQueryParams(filterKeys = null) { if (!this._isBrowserEnvironment()) return {}; try { const params = new URLSearchParams(window.location.search); const result = {}; for (const [key, value] of params.entries()) { if (!filterKeys || filterKeys.includes(key)) { // Sanitize parameter values result[key] = this._sanitizeValue(value); } } return result; } catch (error) { this._handleError('Failed to parse URL parameters', error); return {}; } } /** * Sanitize parameter values * @private */ _sanitizeValue(value) { if (typeof value !== 'string') return value; // Trim whitespace and limit length for security return value.trim().slice(0, 500); } /** * Get UTM parameters from current URL */ getUtmParams() { return this._getQueryParams(this.UTM_KEYS); } /** * Check if direct traffic should create a new session * Based on production logic: direct traffic creates new session only if previous session had UTMs * @private */ _shouldCreateNewSessionForDirectTraffic(currentSession, currentUtmParams) { // If current traffic is direct (no referrer, no UTMs) const isDirectTraffic = !document.referrer && this._isEmpty(currentUtmParams); if (!isDirectTraffic || !currentSession) return false; // If previous session had UTMs, create new session even for direct traffic const previousHadUtms = !this._isEmpty(this._pick(currentSession.tracking, this.UTM_KEYS)); this._log('Direct traffic check:', { isDirectTraffic, previousHadUtms, shouldCreateNew: previousHadUtms, }); return previousHadUtms; } /** * Get referrer information with fallback logic * @private */ _getReferrer(session = {}) { if (!this._isBrowserEnvironment()) return 'server'; try { const params = new URLSearchParams(window.location.search); // If we have an existing session with session_ref, preserve it (don't overwrite) if (session?.tracking?.session_ref) { return session.tracking.session_ref; } // For NEW sessions only: Priority order: URL param > sessionStorage > document.referrer > current page if (params.has(this.config.sessionRefKey)) { return this._sanitizeValue(params.get(this.config.sessionRefKey)); } // Check sessionStorage with the correct key (maps to sessionReferrer) const sessionStorageKey = this.config.sessionRefKey === 'session_ref' ? 'sessionReferrer' : this.config.sessionRefKey; const redirectTestReferrer = sessionStorage.getItem(`tracking_${sessionStorageKey}`); if (redirectTestReferrer) { return this._sanitizeValue(redirectTestReferrer); } // For direct traffic, return "direct" as session_ref if (!document.referrer) { return 'direct'; } return document.referrer || 'direct'; } catch (error) { this._handleError('Failed to get referrer', error); return 'direct'; } } /** * Create session object * @private */ _createSession({ trackingParams, referrer, locations = [] }) { return { expiration: this._getExpiration(), sessionId: this._generateUUID(), // Always generate new UUID - never reuse tracking: { ...trackingParams, ...(referrer && { [this.config.sessionRefKey]: referrer }), }, locations: [...locations], version: '1.0.0', created: Date.now(), }; } /** * Get session from localStorage with validation */ getStoredSession() { if (!this.state.hasStorageSupport) { this._log('Storage not supported, returning null session'); return null; } try { const stored = localStorage.getItem(this.config.storageKey); if (!stored) return null; const session = this._safeJSONParse(stored); if (!session) return null; // Validate session structure if (!session.sessionId || !session.expiration) { this._handleError('Invalid session structure'); this._clearSession(); return null; } // Check expiration if (this._isExpired(session.expiration)) { this._log('Session expired, clearing'); this._clearSession(); return null; } return session; } catch (error) { this._handleError('Failed to retrieve session', error); this._clearSession(); return null; } } /** * Save session to localStorage * @private */ _saveSession(session) { if (!this.state.hasStorageSupport) { this._log('Storage not supported, session not saved'); return false; } try { const serialized = this._safeJSONStringify(session); if (!serialized) return false; localStorage.setItem(this.config.storageKey, serialized); this._log('Session saved successfully'); return true; } catch (error) { this._handleError('Failed to save session', error); return false; } } /** * Update session with current page information * Main method called on each page load */ updateSession() { if (!this._isBrowserEnvironment()) { this._log('Not in browser environment, skipping session update'); return null; } try { // Track current location (page path + query string) const currentLocation = `${window.location.pathname}${window.location.search}`; this.state.locations.push(currentLocation); const currentSession = this.getStoredSession(); const currentUtmParams = this.getUtmParams(); // Get session_ref from URL params const sessionRefParams = this._getQueryParams([this.config.sessionRefKey]); const urlSessionRef = sessionRefParams[this.config.sessionRefKey]; // Get stored UTM tracking data from previous session const storedUtmTracking = this._pick(currentSession?.tracking, this.UTM_KEYS); let sameReferrer = true; // Check if referrer changed (indicates new traffic source) if (document.referrer && currentSession) { const currentSessionRef = currentSession.tracking?.session_ref; // For direct traffic sessions, check if referrer is from same domain (internal navigation) if (currentSessionRef === 'direct') { try { const referrerUrl = new URL(document.referrer); const currentUrl = new URL(window.location.href); // Same domain means continuing the direct traffic session sameReferrer = referrerUrl.hostname === currentUrl.hostname; } catch (error) { sameReferrer = false; } } else { // For sessions with external referrers, check exact match sameReferrer = currentSessionRef === document.referrer; } } if (urlSessionRef && currentSession) { sameReferrer = currentSession.tracking?.session_ref === urlSessionRef; } // Check if UTM parameters changed on first page of session const utmChanged = !this._isEqual(storedUtmTracking, currentUtmParams); // Check if direct traffic should create new session based on previous UTMs const shouldCreateNewSessionForDirect = this._shouldCreateNewSessionForDirectTraffic( currentSession, currentUtmParams ); // Determine if we need a clean session (new attribution) const needsCleanSession = !currentSession || this._isExpired(currentSession.expiration) || !sameReferrer || (this.state.locations.length <= 1 && utmChanged) || shouldCreateNewSessionForDirect; let newSession; if (needsCleanSession) { // New session: use current UTM params for attribution this._log('Creating new session with current UTM params'); const referrer = this._getReferrer({}); // Use current UTM params as-is for new sessions (no utm_source=direct override) const finalTrackingParams = { ...currentUtmParams }; newSession = this._createSession({ locations: this.state.locations, referrer: referrer, trackingParams: finalTrackingParams, }); } else { // Existing session: preserve original UTM params AND original session_ref, extend expiration with same sessionId this._log('Extending existing session with original UTM params and session_ref'); // Ensure session_ref is preserved by calling _getReferrer with existing session const preservedReferrer = this._getReferrer(currentSession); newSession = { ...currentSession, expiration: this._getExpiration(), // Extend expiration locations: [...this.state.locations], // Update locations tracking: { ...currentSession.tracking, // Explicitly preserve the original session_ref [this.config.sessionRefKey]: preservedReferrer, }, }; } // Save session to localStorage this._saveSession(newSession); // Trigger callbacks and events this._dispatchSessionUpdate(newSession); return newSession; } catch (error) { this._handleError('Failed to update session', error); return null; } } /** * Dispatch session update event and callback * @private */ _dispatchSessionUpdate(session) { try { // Store individual UTM parameters in sessionStorage for analytics scripts this._storeTrackingDataInSessionStorage(session.tracking); // Custom event for other scripts to listen to if (this._isBrowserEnvironment()) { const event = new CustomEvent('utmSessionUpdate', { detail: { session: { ...session } }, }); window.dispatchEvent(event); } // User-provided callback if (this.config.onSessionUpdate && typeof this.config.onSessionUpdate === 'function') { this.config.onSessionUpdate({ ...session }); } } catch (error) { this._handleError('Failed to dispatch session update', error); } } /** * Store tracking data in sessionStorage for analytics scripts * @private */ _storeTrackingDataInSessionStorage(trackingData) { if (!this._isBrowserEnvironment() || !trackingData) return; try { // Store individual UTM parameters with 'tracking_' prefix Object.keys(trackingData).forEach((key) => { const value = trackingData[key]; if (value) { // Map session_ref to sessionReferrer for Segment compatibility const sessionStorageKey = key === 'session_ref' ? 'sessionReferrer' : key; sessionStorage.setItem(`tracking_${sessionStorageKey}`, value); } }); this._log('Stored tracking data in sessionStorage:', trackingData); } catch (error) { this._handleError('Failed to store tracking data in sessionStorage', error); } } /** * Get current session */ getCurrentSession() { return this.getStoredSession(); } /** * Get current UTM tracking data */ getCurrentTracking() { const session = this.getCurrentSession(); return session?.tracking || {}; } /** * Check if URL is a Confluent property (matches current app behavior exactly) * @private */ _isConfluentProperty(url) { if (!url) return false; try { // Exact same regex as current Confluent app return /((www\.confluent\.io)|(confluent\.cloud))/.test(url); } catch (error) { this._handleError('Failed to check Confluent property', error); return false; } } /** * Check if URL is external to current domain * @private */ _isExternalLink(url) { try { const link = new URL(url, window.location.href); // Same domain = not external if (link.hostname === window.location.hostname) { return false; } // Check if domain is in exclude list if (this.config.excludeDomains.length > 0) { const isExcluded = this.config.excludeDomains.some((domain) => { return link.hostname === domain || link.hostname.endsWith('.' + domain); }); if (isExcluded) { this._log('Domain excluded from UTM tracking:', link.hostname); return false; } } return true; } catch (error) { this._handleError('Failed to parse URL', error); return false; } } /** * Create Confluent Cloud redirect URL for cross-domain tracking * @private */ _getCloudRedirectUrl(finalDestinationUrl, segmentAnonId) { if (!finalDestinationUrl) { return ''; } if (!segmentAnonId) { return finalDestinationUrl; } const params = [`redirect=${encodeURIComponent(finalDestinationUrl)}`]; // Append the anonymous id if available if (segmentAnonId) { params.push(`ajs_aid=${segmentAnonId}`); } return `https://confluent.cloud/confluent_redirect?${params.join('&')}`; } /** * Add UTM parameters to URL (for external links) * @private */ _addUtmToUrl(url, utmParams) { try { if (this._isEmpty(utmParams)) { return url; } const urlObj = new URL(url); // Add UTM parameters that don't already exist Object.entries(utmParams).forEach(([key, value]) => { if (value && !urlObj.searchParams.has(key)) { urlObj.searchParams.set(key, value); } }); return urlObj.toString(); } catch (error) { this._handleError('Failed to add UTM params to URL', error); return url; } } /** * Add session_ref parameter to URL (for external links) * @private */ _addSessionRefToUrl(url) { try { // Get the original referrer from localStorage session (where the user came from initially) const currentTracking = this.getCurrentTracking(); const originalReferrer = currentTracking[this.config.sessionRefKey]; // Check if URL already has session_ref parameter if (url.includes('session_ref=')) { return url; } // Don't add session_ref if no referrer information available if (!originalReferrer) { return url; } // Use proper query parameter addition to handle fragments correctly return this._addQueryParams(url, { session_ref: originalReferrer }); } catch (error) { this._handleError('Failed to add session_ref to URL', error); return url; } } /** * Add url_ref parameter to URL (for external links) * @private */ _addUrlRefToUrl(url) { try { // Check if URL already has url_ref parameter if (url.includes('url_ref=')) { return url; } // Get current page URL without query parameters (protocol://domain/path only) if (!this._isBrowserEnvironment()) { return url; } const currentUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}`; // Don't add url_ref if no current URL available if (!currentUrl) { return url; } // Use proper query parameter addition to handle fragments correctly return this._addQueryParams(url, { url_ref: currentUrl }); } catch (error) { this._handleError('Failed to add url_ref to URL', error); return url; } } /** * Append tracking params like Confluent application (for Confluent properties) * @private */ _appendTrackingParams(href, utmParams, anonymousId) { // Only apply to Confluent properties if (!this._isConfluentProperty(href)) { return href; } // Skip platform download domains (exact same check as current app) if (href.includes('packages.confluent')) { return href; } const params = { ...utmParams }; // Add Segment anonymousId for Confluent Cloud (exact same logic as current app) if (anonymousId && href.includes('confluent.cloud')) { params.iov_id = anonymousId; } return this._addQueryParams(href, params); } /** * Add query parameters to URL (similar to Confluent's addQueryParams) * @private */ _addQueryParams(url, queryParams) { try { const urlObj = new URL(url); // Add new params, filtering out undefined values Object.entries(queryParams).forEach(([key, value]) => { if (value !== undefined) { urlObj.searchParams.set(key, value); } }); return urlObj.toString(); } catch (error) { this._handleError('Failed to add query parameters', error); return url; } } /** * Modify links in DOM to add UTM parameters (like Confluent app) * @private */ _modifyLinksInDOM() { if (!this._isBrowserEnvironment()) return; try { const currentTracking = this.getCurrentTracking(); const utmParams = this._pick(currentTracking, this.UTM_KEYS); const hasUtmParams = !this._isEmpty(utmParams); if (!hasUtmParams) { this._log( 'No UTM params to add, but checking for external links to add session_ref and url_ref' ); } // Find all anchor tags with href attributes const anchors = Array.from(document.querySelectorAll('a[href]')); let modifiedCount = 0; anchors.forEach((anchor) => { const originalHref = anchor.getAttribute('href'); if (!originalHref) return; // Store original href for potential restoration if (!anchor.dataset.originalHref) { anchor.dataset.originalHref = originalHref; } let newHref = originalHref; // Process Confluent properties (like original app) if (this.config.enableConfluentTracking && this._isConfluentProperty(originalHref)) { // Add UTM parameters if available if (hasUtmParams) { newHref = this._appendTrackingParams( originalHref, utmParams, this.config.anonymousId ); } // Always add session_ref and url_ref to Confluent properties newHref = this._addSessionRefToUrl(newHref); newHref = this._addUrlRefToUrl(newHref); } // Process external links (non-Confluent properties) else if (this._isExternalLink(originalHref)) { // Add UTM parameters (if any), session_ref, and url_ref to external links if (hasUtmParams) { newHref = this._addUtmToUrl(originalHref, utmParams); } newHref = this._addSessionRefToUrl(newHref); newHref = this._addUrlRefToUrl(newHref); } if (newHref !== originalHref) { anchor.setAttribute('href', newHref); modifiedCount++; this._log('Modified link in DOM:', originalHref, '→', newHref); } }); this._log(`Modified ${modifiedCount} links in DOM`); } catch (error) { this._handleError('Failed to modify links in DOM', error); } } /** * Setup observer for dynamic content changes * @private */ _setupDOMObserver() { if (!this._isBrowserEnvironment() || !window.MutationObserver) return; try { // Observe DOM changes for dynamically added links const observer = new MutationObserver((mutations) => { let hasNewLinks = false; mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // Check if the added node is a link or contains links if (node.tagName === 'A' || node.querySelectorAll('a[href]').length > 0) { hasNewLinks = true; } } }); }); if (hasNewLinks) { this._log('Dynamic content detected, re-scanning links'); this._modifyLinksInDOM(); } }); observer.observe(document.body, { childList: true, subtree: true, }); this._log('DOM observer enabled for dynamic content'); } catch (error) { this._handleError('Failed to setup DOM observer', error); } } /** * Auto-detect and integrate with analytics tools * @private */ _integrateAnalytics() { try { // Auto-detect Segment Analytics // eslint-disable-next-line no-undef if (typeof analytics !== 'undefined' && analytics.user) { // eslint-disable-next-line no-undef const segmentAnonymousId = analytics.user().anonymousId(); if (segmentAnonymousId && !this.config.anonymousId) { this.config.anonymousId = segmentAnonymousId; this._log('Auto-detected Segment anonymousId:', segmentAnonymousId); } } // Auto-detect Google Analytics (GA4) if (typeof gtag !== 'undefined') { this._log('Google Analytics (GA4) detected'); // Hook into session updates to send to GA const originalCallback = this.config.onSessionUpdate; this.config.onSessionUpdate = (session) => { try { // Send UTM data to Google Analytics // eslint-disable-next-line no-undef gtag('event', 'utm_session_update', { custom_parameters: session.tracking, session_id: session.sessionId, }); this._log('Sent session data to Google Analytics'); } catch (error) { this._handleError('Failed to send data to Google Analytics', error); } // Call original callback if provided if (originalCallback && typeof originalCallback === 'function') { originalCallback(session); } }; } // Auto-detect Universal Analytics (legacy) else if (typeof ga !== 'undefined') { this._log('Google Universal Analytics detected'); const originalCallback = this.config.onSessionUpdate; this.config.onSessionUpdate = (session) => { try { // Send UTM data to Universal Analytics Object.entries(session.tracking).forEach(([key, value]) => { if (value) { ga('send', 'event', 'UTM Tracking', key, value); // eslint-disable-line no-undef } }); this._log('Sent session data to Universal Analytics'); } catch (error) { this._handleError('Failed to send data to Universal Analytics', error); } if (originalCallback && typeof originalCallback === 'function') { originalCallback(session); } }; } // Auto-detect Facebook Pixel if (typeof fbq !== 'undefined') { this._log('Facebook Pixel detected'); const originalCallback = this.config.onSessionUpdate; this.config.onSessionUpdate = (session) => { try { // Send UTM data to Facebook Pixel fbq('trackCustom', 'UTMSessionUpdate', session.tracking); // eslint-disable-line no-undef this._log('Sent session data to Facebook Pixel'); } catch (error) { this._handleError('Failed to send data to Facebook Pixel', error); } if (originalCallback && typeof originalCallback === 'function') { originalCallback(session); } }; } } catch (error) { this._handleError('Failed to integrate analytics', error); } } /** * Initialize the tracker (called automatically) * @private */ _initialize() { if (this.state.isInitialized) { this._log('Already initialized'); return; } try { this._log('Initializing UTM Session Tracker for static site'); // Auto-integrate with analytics tools this._integrateAnalytics(); // Update session on page load this.updateSession(); // Setup DOM modification when ready (if enabled) if (this.config.enableLinkTracking) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { this._modifyLinksInDOM(); this._setupDOMObserver(); }); } else { // DOM already loaded this._modifyLinksInDOM(); this._setupDOMObserver(); } } this.state.isInitialized = true; this._log('Initialization complete'); } catch (error) { this._handleError('Failed to initialize tracker', error); } } /** * Clear current session */ clearSession() { try { if (this.state.hasStorageSupport) { localStorage.removeItem(this.config.storageKey); } this.state.locations = []; this._log('Session cleared'); return true; } catch (error) { this._handleError('Failed to clear session', error); return false; } } /** * Internal clear session method * @private */ _clearSession() { this.clearSession(); } /** * Manually append tracking parameters to any URL * @param {string} url - URL to append tracking params to * @param {boolean} forceExternal - Force external link treatment * @returns {string} URL with tracking parameters */ appendTrackingToUrl(url, forceExternal = false) { if (!url) return url; const currentTracking = this.getCurrentTracking(); const utmParams = this._pick(currentTracking, this.UTM_KEYS); const hasUtmParams = !this._isEmpty(utmParams); // Determine link type and apply appropriate tracking if (!forceExternal && this.config.enableConfluentTracking && this._isConfluentProperty(url)) { // Confluent properties get UTMs (if available) + session_ref + url_ref let newUrl = url; if (hasUtmParams) { newUrl = this._appendTrackingParams(url, utmParams, this.config.anonymousId); } newUrl = this._addSessionRefToUrl(newUrl); newUrl = this._addUrlRefToUrl(newUrl); return newUrl; } else if (forceExternal || this._isExternalLink(url)) { // External links get UTMs (if available) + session_ref + url_ref let newUrl = url; if (hasUtmParams) { newUrl = this._addUtmToUrl(url, utmParams); } newUrl = this._addSessionRefToUrl(newUrl); newUrl = this._addUrlRefToUrl(newUrl); return newUrl; } else { // Internal links: only add UTMs if available if (hasUtmParams) { return this._addUtmToUrl(url, utmParams); } return url; } } /** * Check if a URL is a Confluent property (public method) * @param {string} url - URL to check * @returns {boolean} True if Confluent property */ isConfluentProperty(url) { return this._isConfluentProperty(url); } /** * Check if a URL is external (public method) * @param {string} url - URL to check * @returns {boolean} True if external */ isExternalLink(url) { return this._isExternalLink(url); } /** * Get tracker status and diagnostics */ getStatus() { const currentSession = this.getCurrentSession(); return { isInitialized: this.state.isInitialized, hasStorageSupport: this.state.hasStorageSupport, isBrowser: this._isBrowserEnvironment(), config: { ...this.config }, locationsCount: this.state.locations.length, currentSession: currentSession ? 'active' : 'none', sessionTimeout: this.config.sessionTimeout, sessionAge: currentSession ? Date.now() - currentSession.created : 0, sessionExpired: currentSession ? this._isExpired(currentSession.expiration) : false, }; } } // Export for different environments if (typeof module !== 'undefined' && module.exports) { module.exports = UTMSessionTracker; // eslint-disable-next-line no-undef } else if (typeof define === 'function' && define.amd) { // eslint-disable-next-line no-undef define([], function () { return UTMSessionTracker; }); } else if (typeof global !== 'undefined') { global.UTMSessionTracker = UTMSessionTracker; // Auto-initialize in browser environment if (typeof window !== 'undefined' && typeof document !== 'undefined') { // Create default instance automatically global.utmTracker = new UTMSessionTracker({ debug: false, // Set to true for debugging in development }); // Also expose the class for manual instantiation with custom config global.UTMSessionTracker = UTMSessionTracker; } } })(typeof window !== 'undefined' ? window : this); /* * USAGE EXAMPLES: * * // AUTO-INITIALIZATION (Just include the script - no additional code needed!) * * * * // Access the auto-created instance * console.log(window.utmTracker.getCurrentTracking()); * * // Custom Configuration (if you need specific settings) * * * * // Multiple Instances (if needed) * const customTracker = new UTMSessionTracker({ * storageKey: 'custom_session', // Different storage key * debug: true * }); * * // Using the Auto-Created Instance * // Get Current UTM Data * const utmData = window.utmTracker.getCurrentTracking(); * // { utm_source: 'google', utm_campaign: 'ads', iov_id: 'abc123' } * * // Manual URL Building * const trackingUrl = window.utmTracker.appendTrackingToUrl('https://www.confluent.io/pricing'); * * // Check Link Types * window.utmTracker.isConfluentProperty('https://www.confluent.io/'); // true * window.utmTracker.isExternalLink('https://google.com/'); // true * * // Listen for Session Updates * window.addEventListener('utmSessionUpdate', (event) => { * console.log('Session:', event.detail.session); * }); * * // Check Status * console.log(window.utmTracker.getStatus()); */