UNPKG

@userflux/browser-js

Version:

UserFlux's JavaScript SDK - send your frontend analytics data to the UserFlux platform

1,080 lines (925 loc) 31.8 kB
const fetch = require("cross-fetch") class UserFlux { static ufApiKey = null static ufUserId = null static ufExternalId = null static ufTrackQueue = [] static ufAnonymousId = "" static ufSessionId = null static ufAllowCookies = false static ufCookieSameSiteSetting = "Strict" static ufLocationEnrichmentEnabled = true static ufDeviceDataEnrichmentEnabled = true static ufDefaultTrackingProperties = {} static ufCustomQueryParamsToCollect = [] static ufDisableUserIdStorage = false static ufCookieExpiryDays = 365 static ufConsecutiveFailures = 0 static ufMaxConsecutiveFailures = 3 static ufRetryDelay = 1000 // 1 second delay between retries static initialize(apiKey, options) { try { const shouldDisableCommonBotsBlocking = "blockCommonBots" in options && options["blockCommonBots"] == false if (!shouldDisableCommonBotsBlocking && UserFlux.isBotUserAgent(window.navigator.userAgent)) { console.info("Common bot detected. UserFlux SDK will not initialize.") return } UserFlux.ufApiKey = apiKey if ("allowCookies" in options && options["allowCookies"] == true) { UserFlux.ufAllowCookies = true } if ("cookieSameSiteSetting" in options && options["cookieSameSiteSetting"] == "Lax") { UserFlux.ufCookieSameSiteSetting = "Lax" } if ("cookieExpiryDays" in options && typeof options["cookieExpiryDays"] === "number") { UserFlux.ufCookieExpiryDays = options["cookieExpiryDays"] } if ("disableUserIdStorage" in options && options["disableUserIdStorage"] == true) { UserFlux.ufDisableUserIdStorage = true } UserFlux.ufAnonymousId = UserFlux.getOrCreateAnonymousId() UserFlux.ufUserId = UserFlux.getUserId() UserFlux.ufTrackQueue = UserFlux.loadEventsFromStorage() if ("trackSession" in options && options["trackSession"] == false) { // don't setup session id } else { UserFlux.setupSessionId() } if ("autoEnrich" in options && options["autoEnrich"] == false) { UserFlux.ufLocationEnrichmentEnabled = false UserFlux.ufDeviceDataEnrichmentEnabled = false } if ("defaultTrackingProperties" in options && typeof options["defaultTrackingProperties"] === "object") { UserFlux.ufDefaultTrackingProperties = options["defaultTrackingProperties"] } if ( "customQueryParamsToCollect" in options && Array.isArray(options["customQueryParamsToCollect"]) === true ) { UserFlux.ufCustomQueryParamsToCollect = options["customQueryParamsToCollect"] } UserFlux.startFlushInterval() if ("autoCapture" in options) { UserFlux.setupAutoTracking(options["autoCapture"]) } if (UserFlux.ufDisableUserIdStorage == true && UserFlux.ufUserId != null) { UserFlux.getStorage()?.removeItem("uf-userId") } } catch (error) { console.info("Failed to initialize UserFlux SDK: ", error) } } static updateDefaultTrackingProperties(properties) { if (typeof properties !== "object") { console.info("UF defaultTrackingProperties must be an object.") return } UserFlux.ufDefaultTrackingProperties = properties } static getStorage() { if (typeof window === "undefined") { return null } return { setItem: (key, value) => { try { let shouldSkipForLocalStorage = UserFlux.ufDisableUserIdStorage == true && key === "uf-userId" if (!shouldSkipForLocalStorage && UserFlux.isLocalStorageAccessible()) localStorage.setItem(key, value) let shouldSkipForCookieStorage = key == "uf-track" if (UserFlux.ufAllowCookies == true && !shouldSkipForCookieStorage) UserFlux.setCookie(key, value, UserFlux.ufCookieExpiryDays) } catch (error) { console.info("Error setting item to storage: ", error) } }, getItem: (key) => { try { return ( (UserFlux.isLocalStorageAccessible() ? localStorage.getItem(key) : null) || (UserFlux.ufAllowCookies == true ? UserFlux.getCookie(key) : null) ) } catch (error) { console.info("Error getting item from storage: ", error) return null } }, removeItem: (key) => { try { if (UserFlux.isLocalStorageAccessible()) localStorage.removeItem(key) if (UserFlux.ufAllowCookies == true) UserFlux.eraseCookie(key) } catch (error) { console.info("Error removing item from storage: ", error) } }, } } static getSessionStorage() { if (typeof window === "undefined" || !UserFlux.isSessionStorageAccessible()) { return null } return { setItem: (key, value) => { try { sessionStorage.setItem(key, value) } catch (error) { console.info("Error setting item to session storage: ", error) } }, getItem: (key) => { try { return sessionStorage.getItem(key) } catch (error) { console.info("Error getting item from session storage: ", error) return null } }, removeItem: (key) => { try { sessionStorage.removeItem(key) } catch (error) { console.info("Error removing item from session storage: ", error) } }, } } static setupSessionId() { try { if (!UserFlux.isSessionStorageAccessible()) { console.info("Session storage is not accessible. Session ID handling will be disabled.") UserFlux.clearSessionId() return } const currentSessionId = UserFlux.getSessionId() if (UserFlux.isStringNullOrBlank(currentSessionId)) { const newSessionId = UserFlux.generateUUID() UserFlux.setSessionId(newSessionId) } } catch (error) { console.info("Error setting up session ID: ", error) } } static setSessionId(newSessionId) { if (!UserFlux.isSessionStorageAccessible()) { return } try { UserFlux.ufSessionId = newSessionId UserFlux.getSessionStorage()?.setItem("uf-sessionId", newSessionId) if (UserFlux.ufAllowCookies == true) UserFlux.setCookie("uf-sessionId", newSessionId, 0.003) // 5 minutes in days } catch (error) { console.info("Error setting session ID: ", error) } } static getSessionId() { try { if (!UserFlux.isSessionStorageAccessible()) { return null } // fetch from memory if (!UserFlux.isStringNullOrBlank(UserFlux.ufSessionId)) { UserFlux.setSessionId(UserFlux.ufSessionId) // replenish storage return UserFlux.ufSessionId } // fetch from sesionStorage const idFromSessionStorage = UserFlux.getSessionStorage()?.getItem("uf-sessionId") if (!UserFlux.isStringNullOrBlank(idFromSessionStorage)) { UserFlux.setSessionId(idFromSessionStorage) // replenish storage return idFromSessionStorage } // fetch from cookie const idFromCookie = UserFlux.ufAllowCookies == true ? UserFlux.getCookie("uf-sessionId") : null if (!UserFlux.isStringNullOrBlank(idFromCookie)) { UserFlux.setSessionId(idFromCookie) // replenish storage return idFromCookie } // otherwise return null return null } catch (error) { console.info("Error getting session ID: ", error) return null } } static clearSessionId() { UserFlux.ufSessionId = null } static setupAutoTracking(autoCaptureOptions) { if (typeof autoCaptureOptions !== "object") { // The typeof operator returns " object " for arrays because in JavaScript arrays are objects. console.info("UF autoCapture must be an array.") return } if (autoCaptureOptions.includes("page_views") || autoCaptureOptions.includes("all")) { UserFlux.setupPageViewListener() } if (autoCaptureOptions.includes("page_leaves") || autoCaptureOptions.includes("all")) { UserFlux.setupPageLeaveListener() } if (autoCaptureOptions.includes("clicks") || autoCaptureOptions.includes("all")) { UserFlux.setupClickListener() } } static setupPageViewListener() { // Check if running in a browser environment if (typeof window === "undefined") { return } window.addEventListener("pageshow", async (event) => { await UserFlux.trackPageView() }) } static setupPageLeaveListener() { // Check if running in a browser environment if (typeof window === "undefined") { return } // TBD: what's best to use pagehide or beforeunload window.addEventListener("pagehide", async (event) => { await UserFlux.trackPageLeave() }) } static async trackPageView() { await UserFlux.track({ event: "page_view", properties: { ...UserFlux.getPageProperties(), ...(UserFlux.getReferrerProperties() || {}), ...(UserFlux.getUTMProperties() || {}), ...(UserFlux.getPaidAdProperties() || {}), }, addToQueue: false, }) } static setupClickListener() { // Check if running in a browser environment if (typeof window === "undefined") { return } document.addEventListener("click", async (event) => { const element = event.target.closest('a, button, input[type="submit"], input[type="button"]') // If the clicked element or its parent is not what we want to track, return early. if (!element) return await UserFlux.trackClick(element) }) } static async trackClick(element) { const properties = { elementTagName: element.tagName, elementInnerText: element.innerText && element.innerText.length < 200 ? element.innerText.trim() : undefined, elementId: element.id && element.id !== "" ? element.id : undefined, ...UserFlux.getPageProperties(), } // Filter out properties that are undefined const filteredProperties = Object.keys(properties).reduce((obj, key) => { if (properties[key] !== undefined) { obj[key] = properties[key] } return obj }, {}) await UserFlux.track({ event: "click", properties: { ...filteredProperties, }, addToQueue: true, }) } static async trackPageLeave() { await UserFlux.track({ event: "page_leave", properties: { ...UserFlux.getPageProperties(), ...(UserFlux.getReferrerProperties() || {}), ...(UserFlux.getUTMProperties() || {}), ...(UserFlux.getPaidAdProperties() || {}), }, addToQueue: true, }) } static isApiKeyProvided() { return UserFlux.ufApiKey !== null } static getOrCreateAnonymousId() { let anonymousId if (UserFlux.isStringNullOrBlank(UserFlux.ufAnonymousId)) { // default value is '' which means it hasn't been set yet // fetch from storage, if it isn't there then create a new ID anonymousId = UserFlux.getStorage()?.getItem("uf-anonymousId") ?? UserFlux.createNewAnonymousId() } else { // otherwise value is set anonymousId = UserFlux.ufAnonymousId } // Update anonymousId in memory + local + cookie storage to prevent it from expiring UserFlux.ufAnonymousId = anonymousId UserFlux.getStorage()?.setItem("uf-anonymousId", anonymousId) return anonymousId } static createNewAnonymousId() { return UserFlux.generateUUID() } static generateUUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { let r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8 return v.toString(16) }) } static getUserId() { let userId = UserFlux.ufUserId || UserFlux.getStorage()?.getItem("uf-userId") // clean up any wrongly stored user ids let shouldForceUpdate = false // handle edge case values if (UserFlux.isStringNullOrBlank(userId)) { userId = null shouldForceUpdate = true } if (userId || shouldForceUpdate) { // Update userId in local storage to prevent it from expiring UserFlux.getStorage()?.setItem("uf-userId", userId) } return userId } static getAnonymousId() { return UserFlux.getOrCreateAnonymousId() } static setUserId(userId) { UserFlux.ufUserId = userId UserFlux.getStorage()?.setItem("uf-userId", userId) } static setExternalId(externalId) { UserFlux.ufExternalId = externalId UserFlux.getStorage()?.setItem("uf-externalId", externalId) } static loadEventsFromStorage() { try { const events = UserFlux.getStorage()?.getItem("uf-track") return events ? JSON.parse(events) : [] } catch (error) { console.info("Failed to get tracking events from storage: ", error) UserFlux.getStorage()?.removeItem("uf-track") return [] } } static async reset() { // Firstly, flush any pending events await UserFlux.checkQueue(UserFlux.ufTrackQueue, "event/ingest/batch", true) // Clear all stored data UserFlux.ufUserId = null UserFlux.getStorage()?.removeItem("uf-userId") UserFlux.ufAnonymousId = null UserFlux.getStorage()?.removeItem("uf-anonymousId") UserFlux.ufAnonymousId = UserFlux.createNewAnonymousId() UserFlux.getStorage()?.setItem("uf-anonymousId", UserFlux.ufAnonymousId) UserFlux.ufExternalId = null UserFlux.getStorage()?.removeItem("uf-externalId") } static startFlushInterval() { setInterval(async () => { await UserFlux.checkQueue(UserFlux.ufTrackQueue, "event/ingest/batch", true) }, 1500) } static async identify(parameters) { // sanity check API key if (!UserFlux.isApiKeyProvided()) { console.info("API key not provided. Cannot identify user.") return } // sanity check parameters if (!parameters || typeof parameters !== "object") { console.info("Invalid parameters passed to track method") return } // sanity check userId let userId = parameters.userId || UserFlux.ufUserId if (userId && (typeof userId !== "string" || UserFlux.isStringNullOrBlank(userId))) userId = null if (userId !== UserFlux.ufUserId) UserFlux.setUserId(userId) // sanity check externalId let externalId = parameters.externalId || UserFlux.ufExternalId || UserFlux.getExternalIdQueryParam() if (externalId && (typeof externalId !== "string" || UserFlux.isStringNullOrBlank(externalId))) externalId = null if (externalId !== UserFlux.ufExternalId) UserFlux.setExternalId(externalId) // sanity check properties const properties = parameters.properties || {} if (typeof properties !== "object") { console.info("Invalid properties passed to identify method") return } // sanity check enrichDeviceData const enrichDeviceData = parameters.enrichDeviceData || UserFlux.ufDeviceDataEnrichmentEnabled if (typeof enrichDeviceData !== "boolean") { console.info("Invalid enrichDeviceData passed to identify method") return } // sanity check enrichLocationData const enrichLocationData = parameters.enrichLocationData || UserFlux.ufLocationEnrichmentEnabled if (typeof enrichLocationData !== "boolean") { console.info("Invalid enrichLocationData passed to identify method") return } const payload = { userId: userId, externalId: externalId, anonymousId: UserFlux.getOrCreateAnonymousId(), properties: properties, deviceData: enrichDeviceData ? UserFlux.getDeviceProperties() : null, } return await UserFlux.sendRequest("profile", payload, enrichLocationData) } static async track(parameters) { // sanity check API key if (!UserFlux.isApiKeyProvided()) { console.info("API key not provided. Cannot track event.") return } // sanity check parameters if (!parameters || typeof parameters !== "object") { console.info("Invalid parameters passed to track method") return } // sanity check event const event = parameters.event if (!event || typeof event !== "string" || UserFlux.isStringNullOrBlank(event)) { console.info("Invalid event passed to track method") return } // sanity check userId let userId = parameters.userId || UserFlux.ufUserId if (userId && (typeof userId !== "string" || UserFlux.isStringNullOrBlank(userId))) userId = null if (userId !== UserFlux.ufUserId) UserFlux.setUserId(userId) // sanity check externalId let externalId = parameters.externalId || UserFlux.ufExternalId || UserFlux.getExternalIdQueryParam() if (externalId && (typeof externalId !== "string" || UserFlux.isStringNullOrBlank(externalId))) externalId = null if (externalId !== UserFlux.ufExternalId) UserFlux.setExternalId(externalId) // sanity check properties const properties = parameters.properties || {} if (typeof properties !== "object") { console.info("Invalid properties passed to track method") return } // sanity check enrichDeviceData const enrichDeviceData = parameters.enrichDeviceData || UserFlux.ufDeviceDataEnrichmentEnabled if (typeof enrichDeviceData !== "boolean") { console.info("Invalid enrichDeviceData passed to track method") return } // sanity check enrichLocationData const enrichLocationData = parameters.enrichLocationData || UserFlux.ufLocationEnrichmentEnabled if (typeof enrichLocationData !== "boolean") { console.info("Invalid enrichLocationData passed to track method") return } const enrichPageProperties = parameters.enrichPageProperties || true if (typeof enrichPageProperties !== "boolean") { console.info("Invalid enrichPageProperties passed to track method") return } const enrichReferrerProperties = parameters.enrichReferrerProperties || true if (typeof enrichReferrerProperties !== "boolean") { console.info("Invalid enrichReferrerProperties passed to track method") return } const enrichUTMProperties = parameters.enrichUTMProperties || true if (typeof enrichUTMProperties !== "boolean") { console.info("Invalid enrichUTMProperties passed to track method") return } const enrichPaidAdProperties = parameters.enrichPaidAdProperties || true if (typeof enrichPaidAdProperties !== "boolean") { console.info("Invalid enrichPaidAdProperties passed to track method") return } // sanity check addToQueue const addToQueue = parameters.addToQueue || false if (typeof addToQueue !== "boolean") { console.info("Invalid addToQueue passed to track method") return } // combine event properties with any default tracking properties const finalProperties = { ...properties, ...UserFlux.ufDefaultTrackingProperties, ...(enrichPageProperties ? UserFlux.getPageProperties() : {}), ...(enrichReferrerProperties ? UserFlux.getReferrerProperties() : {}), ...(enrichUTMProperties ? UserFlux.getUTMProperties() : {}), ...(enrichPaidAdProperties ? UserFlux.getPaidAdProperties() : {}), ...(UserFlux.getCustomQueryParamProperties() || {}), } const payload = { timestamp: Date.now(), userId: userId, anonymousId: UserFlux.getOrCreateAnonymousId(), externalId: externalId, sessionId: UserFlux.getSessionId(), name: event, properties: finalProperties, deviceData: enrichDeviceData ? UserFlux.getDeviceProperties() : null, } const shouldForceFlush = UserFlux.getStorage() == null || addToQueue == false UserFlux.ufTrackQueue.push(payload) UserFlux.saveEventsToStorage("uf-track", UserFlux.ufTrackQueue) await UserFlux.checkQueue(UserFlux.ufTrackQueue, "event/ingest/batch", shouldForceFlush) return null } static async trackBatch(events) { for (const event of events) { await UserFlux.track({ ...event, addToQueue: true }) } await UserFlux.flush() return } static async flush() { await UserFlux.checkQueue(UserFlux.ufTrackQueue, "event/ingest/batch", true) } static saveEventsToStorage(key, queue) { UserFlux.getStorage()?.setItem(key, JSON.stringify(queue)) } static async checkQueue(queue, eventType, forceFlush) { if (queue.length >= 10 || (forceFlush && queue.length > 0)) { await UserFlux.flushEvents(queue, eventType) } } static async flushEvents(queue, eventType) { if (!UserFlux.isApiKeyProvided()) { console.info("API key not provided. Cannot flush events.") return } // Check if we've hit the maximum consecutive failures if (UserFlux.ufConsecutiveFailures >= UserFlux.ufMaxConsecutiveFailures) { console.info(`UF: Max consecutive failures (${UserFlux.ufMaxConsecutiveFailures}) reached. Stopping flush attempts.`) // Reset the failure counter after some time to allow retrying later setTimeout(() => { UserFlux.ufConsecutiveFailures = 0 }, 60000) // Reset after 1 minute return } const eventsToTrack = queue.splice(0, 10) const success = await UserFlux.sendRequest(eventType, { events: eventsToTrack }) if (success) { UserFlux.saveEventsToStorage(`uf-track`, queue) UserFlux.ufConsecutiveFailures = 0 } else { // If the request fails, add the events back to the queue queue.push(...eventsToTrack) UserFlux.saveEventsToStorage(`uf-track`, queue) UserFlux.ufConsecutiveFailures++ } // If the queue is not empty, check it again with a delay if (queue.length > 0) { setTimeout(async () => { await UserFlux.checkQueue(queue, eventType, true) }, UserFlux.ufRetryDelay) } } static async sendRequest(endpoint, data, locationEnrich = UserFlux.ufLocationEnrichmentEnabled) { if (!UserFlux.isApiKeyProvided()) { console.info("API key not provided. Cannot send request.") return false } // Check if we're in an iframe that might be closing/closed if (typeof window !== 'undefined' && window.frameElement && !document.body) { console.info("UF: Iframe appears to be closing/closed. Skipping request.") return false } try { await fetch(`https://integration-api.userflux.co/${endpoint}?locationEnrichment=${locationEnrich}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${UserFlux.ufApiKey}`, }, body: JSON.stringify(data), keepalive: true, }) return true } catch (error) { console.info("UF HTTP Error: ", error) return false } } static getPageProperties() { try { // Check if running in a browser environment if (typeof window === "undefined") { return {} } return UserFlux.removeNullProperties({ host: window.location.host, href: window.location.href, path: window.location.pathname, pageTitle: document.title, }) } catch (e) { console.info("Error on getPageProperties:", error) return {} } } static getDeviceProperties() { try { // Check if running in a browser environment if (typeof window === "undefined") { return null } const userAgent = window.navigator.userAgent let browser, browserVersion, deviceType, os // Determine Browser and Browser Version if (userAgent.indexOf("Chrome") > -1) { browser = "Chrome" const match = userAgent.match(/Chrome\/(\d+)/) browserVersion = match ? match[1] : "Unknown" } else if (userAgent.indexOf("CriOS") > -1) { browser = "Chrome" const match = userAgent.match(/CriOS\/([\d.]+)/) browserVersion = match ? match[1] : "Unknown" } else if (userAgent.indexOf("Safari") > -1) { browser = "Safari" const match = userAgent.match(/Version\/([\d.]+)/) browserVersion = match ? match[1] : "Unknown" } else if (userAgent.indexOf("Firefox") > -1) { browser = "Firefox" const match = userAgent.match(/Firefox\/([\d.]+)/) browserVersion = match ? match[1] : "Unknown" } else if (userAgent.indexOf("MSIE") > -1 || userAgent.indexOf("Trident") > -1) { browser = "Internet Explorer" const match = userAgent.match(/(?:MSIE |rv:)(\d+)/) browserVersion = match ? match[1] : "Unknown" } else { browser = "Unknown" browserVersion = "Unknown" } // Determine Device Type if (/Mobi|Android/i.test(userAgent)) { deviceType = "Mobile" } else { deviceType = "Desktop" } // Determine OS if (/iPhone|iPad|iPod/i.test(userAgent)) { os = "iOS" } else if (userAgent.indexOf("Mac OS X") > -1) { os = "Mac OS X" } else if (userAgent.indexOf("Windows NT") > -1) { os = "Windows" } else if (userAgent.indexOf("Android") > -1) { os = "Android" } else if (userAgent.indexOf("Linux") > -1) { os = "Linux" } else { os = "Unknown" } // Determine Browser Language Preference const browserLanguage = navigator.language || navigator.userLanguage || navigator.browserLanguage || "Unknown" return UserFlux.removeNullProperties({ userAgent: userAgent, browser: browser, browserVersion: browserVersion, deviceType: deviceType, os: os, screenWidth: window.screen.width, screenHeight: window.screen.height, browserWidth: window.innerWidth, browserHeight: window.innerHeight, browserLanguage: browserLanguage, }) } catch (error) { console.info("Error:", error) return null } } static getCustomQueryParamProperties() { try { // Check if there are any custom query parameters to collect if (UserFlux.ufCustomQueryParamsToCollect.length == 0) return null // Check if running in a browser environment if (typeof window === "undefined") { return null } // Pickup any custom query parameters from the href, default to null if it doesn't exist let locationHref = window.location.href const urlSearchParams = new URLSearchParams(new URL(locationHref).search) let customQueryParams = {} UserFlux.ufCustomQueryParamsToCollect.forEach((param) => { customQueryParams[param] = urlSearchParams.get(param) || null }) // Remove any null properties from the object before returning return UserFlux.removeNullProperties(customQueryParams) } catch (error) { console.info("Error for getCustomQueryParamProperties(): ", error) return null } } static getUTMProperties() { try { // Check if running in a browser environment if (typeof window === "undefined") { return null } let locationHref = window.location.href // Extract query parameters const urlSearchParams = new URLSearchParams(new URL(locationHref).search) let queryParams = { utmSource: urlSearchParams.get("utm_source") || null, utmMedium: urlSearchParams.get("utm_medium") || null, utmCampaign: urlSearchParams.get("utm_campaign") || null, utmTerm: urlSearchParams.get("utm_term") || null, utmContent: urlSearchParams.get("utm_content") || null, utmId: urlSearchParams.get("utm_id") || null, utmSourcePlatform: urlSearchParams.get("utm_source_platform") || null, } return UserFlux.removeNullProperties(queryParams) } catch (error) { console.info("Error for getUTMProperties(): ", error) return null } } static getExternalIdQueryParam() { try { // Check if running in a browser environment if (typeof window === "undefined") { return null } let locationHref = window.location.href const urlSearchParams = new URLSearchParams(new URL(locationHref).search) return urlSearchParams.get("ufeid") || null } catch (error) { console.info("Error for getExternalIdQueryParam(): ", error) return null } } static getPaidAdProperties() { try { // Check if running in a browser environment if (typeof window === "undefined") { return null } let locationHref = window.location.href // Extract query parameters const urlSearchParams = new URLSearchParams(new URL(locationHref).search) let queryParams = { gclid: urlSearchParams.get("gclid") || null, fbclid: urlSearchParams.get("fbclid") || null, msclkid: urlSearchParams.get("msclkid") || null, } return UserFlux.removeNullProperties(queryParams) } catch (error) { console.info("Error for getPaidAdProperties(): ", error) return null } } static getReferrerProperties() { try { // Check if running in a browser environment if (typeof window === "undefined") { return null } return UserFlux.removeNullProperties({ referrerHref: document.referrer !== "" ? document.referrer : null, referrerHost: document.referrer ? new URL(document.referrer).hostname : null, }) } catch (error) { console.info("Error getReferrerProperties(): ", error) return null } } // Utility function to set a cookie static setCookie(name, value, days) { try { let expires = "" if (days) { const date = new Date() date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000) expires = "; expires=" + date.toUTCString() } // Set SameSite setting const sameSite = `; SameSite=${UserFlux.ufCookieSameSiteSetting}` // Dynamically determine the base domain const hostMatchRegex = /^(?:https?:\/\/)?(?:[^\/]+\.)?([^.\/]+\.(?:co\.uk|com\.au|com|co|money|io|is)).*$/i const matches = document.location.hostname.match(hostMatchRegex) const domain = matches ? matches[1] : "" const cookieDomain = domain ? "; domain=." + domain : "" document.cookie = name + "=" + (value || "") + expires + sameSite + "; Secure" + cookieDomain + "; path=/" } catch (error) { console.info("Error:", error) } } // Utility function to get a cookie static getCookie(name) { try { const nameEQ = name + "=" const ca = document.cookie.split(";") for (let i = 0; i < ca.length; i++) { let c = ca[i] while (c.charAt(0) == " ") c = c.substring(1, c.length) if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length) } return null } catch (error) { console.info("Error:", error) return null } } // Utility function to erase a cookie static eraseCookie(name) { try { // Dynamically determine the base domain const hostMatchRegex = /[a-z0-9][a-z0-9-]+\.[a-z]{2,}$/i const matches = document.location.hostname.match(hostMatchRegex) const domain = matches ? "; domain=." + matches[0] : "" document.cookie = name + "=; Max-Age=-99999999; path=/" + "; domain=." + domain } catch (error) { console.info("Error:", error) } } // Method to check if localStorage is accessible static isLocalStorageAccessible() { try { // Try to use localStorage localStorage.setItem("uf-ls-test", "test") localStorage.removeItem("uf-ls-test") return true } catch (e) { // Catch any errors, including security-related ones return false } } // Method to check if sessionStorage is accessible static isSessionStorageAccessible() { try { const storage = window.sessionStorage const testKey = "uf-ss-test" storage.setItem(testKey, "test") storage.removeItem(testKey) return true } catch (e) { // Catch any errors, including security-related ones return false } } // Method to check if a strings value is null or empty // Handles edges cases where values retrieve from storage come back as string values instead of null static isStringNullOrBlank(value) { if (typeof value !== "string") return true return !value || value == null || value == undefined || value == "" || value == "null" || value == "undefined" } // Method to remove null properties from an object // Used for cleaning up the properties object of a event before tracking static removeNullProperties(object) { return Object.fromEntries(Object.entries(object).filter(([key, value]) => value !== null)) } static isBotUserAgent(userAgent) { // Convert to lowercase for case-insensitive matching const lowerUA = userAgent.toLowerCase() // Check for empty or missing user agent, if so, assume it's not a bot if (!userAgent || userAgent.trim() === "") { return false } // List of common bot keywords const botKeywords = [ "bot", "crawler", "spider", "scraper", "indexer", "archiver", "slurp", "googlebot", "bingbot", "yandexbot", "duckduckbot", "baiduspider", "twitterbot", "facebookexternalhit", "linkedinbot", "msnbot", "slackbot", "telegrambot", "applebot", "pingdom", "ia_archiver", "semrushbot", "ahrefsbot", "monotybot", "amazon-qbusiness", "google-safety", "amazon-kendra", ] // Check for bot keywords for (const keyword of botKeywords) { if (lowerUA.includes(keyword)) { return true } } // Check for common bot patterns if ( /(?:^|\W)spider(?:$|\W)/i.test(userAgent) || /(?:^|\W)crawl(?:er|ing)(?:$|\W)/i.test(userAgent) || /(?:^|\W)bot(?:$|\W)/i.test(userAgent) || /\+https?:\/\//i.test(userAgent) ) { return true } // If none of the above conditions are met, it's likely not a bot return false } } module.exports = UserFlux