UNPKG

overcentric

Version:

Overcentric watches your website, product, and users - and tells you what matters and what to do about it.

484 lines (483 loc) 17.4 kB
import { sendEvent } from "./eventSender"; import { initRecording, updateRecordingIdentity } from "./recorder"; import { startAutoCapture } from "./autoCapture"; import { getDeviceId, storeDeviceId, generateUuid } from "./identity"; import { getSessionId, updateSessionActivity, } from "./session"; import { CONFIG, log } from "./config"; import { initVisitorIdentification, updateVisitorSession, } from "./visitorIdentification"; const isBrowser = () => typeof window !== "undefined"; function extractBaseDomain(input) { if (!input || typeof input !== "string") { return ""; } try { let hostname; if (input.includes("://")) { hostname = new URL(input).hostname; } else { hostname = input.split(":")[0]; } hostname = hostname.toLowerCase().trim(); if (!hostname.includes(".") || !isNaN(Number(hostname.split(".").join("")))) { return hostname; } hostname = hostname.replace(/^www\./, ""); const parts = hostname.split("."); if (parts.length <= 2) { return hostname; } return parts.slice(-2).join("."); } catch (_a) { return input.toLowerCase().trim(); } } function isDomainAllowed(projectURL) { if (!projectURL) return false; const currentDomain = extractBaseDomain(window.location.hostname); const allowedDomain = extractBaseDomain(projectURL); log.debug(`Domain check: current="${currentDomain}", allowed="${allowedDomain}"`); return currentDomain === allowedDomain; } function initVisibilityTracking(projectId) { document.addEventListener("visibilitychange", () => { if (document.visibilityState !== "visible") return; const currentSessionId = getSessionId(projectId); window.overcentricSessionId = currentSessionId; log.debug(`Tab visible, session: ${currentSessionId}`); if (CONFIG.isRecordingEnabled) { initRecording(currentSessionId); } updateVisitorSession(currentSessionId); }); } export { CONFIG, log }; let commandQueue = []; let isInitializing = false; function queueOrExecute(fn, args) { if (window.overcentricInitialized && !isInitializing) { fn(...args); } else { commandQueue.push({ fn, args }); log.debug(`Queued function call: ${fn.name}`); } } function processQueuedCommands() { log.debug(`Processing ${commandQueue.length} queued commands`); commandQueue.forEach(({ fn, args }) => { try { fn(...args); } catch (error) { log.error(`Error executing queued command ${fn.name}:`, error); } }); commandQueue = []; isInitializing = false; } function test() { return "Overcentric: Test ran successfully"; } if (typeof window !== "undefined" && (document === null || document === void 0 ? void 0 : document.readyState) !== undefined) { document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", handleInitFromScriptTag) : handleInitFromScriptTag(); } function handleInitFromScriptTag() { const scripts = Array.from(document.getElementsByTagName("script")); const overcentricScript = scripts.find((s) => s.hasAttribute("data-project-id")); if (!overcentricScript) return; try { initFromScriptTag(overcentricScript); } catch (error) { log.error("Error during auto-initialization:", error); } } function initFromScriptTag(script) { var _a, _b, _c; const isDebugEnabled = script.hasAttribute("data-debug-mode"); const hasProjectId = script.hasAttribute("data-project-id"); if (!hasProjectId) { log.error("No project ID found in script tag 2"); return; } const projectId = (_a = script.getAttribute("data-project-id")) === null || _a === void 0 ? void 0 : _a.trim(); if (!projectId) { log.error("Empty project ID in script tag"); return; } if (window.overcentricInitialized) { log.warn("Already initialized, skipping duplicate initialization"); return; } const hasContext = script.hasAttribute("data-context"); if (!hasContext) { log.error("No context found in script tag"); return; } const ctx = (_b = script.getAttribute("data-context")) === null || _b === void 0 ? void 0 : _b.trim(); if (!ctx) { log.error("Empty context in script tag"); return; } const basePath = (_c = script.getAttribute("data-base-path")) === null || _c === void 0 ? void 0 : _c.trim(); log.debug("Initializing from script tag"); init(projectId, { context: ctx, debugMode: isDebugEnabled, basePath, }); } async function fetchConfig(id, basePath) { try { const url = `${basePath}/config/${id}`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error fetching configuration: ${response.status}`); } const res = await response.json(); const config = res.data; if (!config) { throw new Error("No configuration found"); } return { debugMode: config.debug_mode, enableRecording: config.recording, enableErrorCapture: config.error_capture, projectURL: config.url, enableVisitorId: config.wvid || false, }; } catch (error) { log.error(`Failed to fetch configuration: ${error}`); return {}; } } async function init(id, options) { if (!isBrowser()) { log.warn("Not initializing in non-browser environment"); return; } if (window.overcentricInitialized || isInitializing) { log.warn("Library already initialized or initialization in progress"); return; } isInitializing = true; if (!options.context) { log.error('Context must be specified as either "website" or "product"'); isInitializing = false; return; } if (!id || typeof id !== "string" || id.trim().length === 0) { log.error("Project ID is required and must be a non-empty string"); isInitializing = false; return; } try { window.overcentricProjectId = id; window.overcentricContext = options.context; window.overcentricSessionId = getSessionId(id); const bp = options.basePath || CONFIG.basePath; if (!bp) { log.error("Base path is required"); isInitializing = false; return; } const remoteConfig = await fetchConfig(id, bp); if (!remoteConfig || Object.keys(remoteConfig).length === 0) { return; } if (!remoteConfig.projectURL) { log.error("Project URL is required. Set it in your Overcentric dashboard."); return; } if (!isDomainAllowed(remoteConfig.projectURL)) { log.error(`Domain mismatch: This library is configured for "${remoteConfig.projectURL}" but is running on "${window.location.hostname}".`); isInitializing = false; return; } window.overcentricProjectId = id; window.overcentricInitialized = true; const finalOptions = { ...CONFIG.initDefaults, ...remoteConfig, ...options, }; CONFIG.basePath = finalOptions.basePath || CONFIG.basePath; CONFIG.debugMode = finalOptions.debugMode || CONFIG.debugMode; if (finalOptions.ignoredNetworkErrorUrlPatterns) { CONFIG.ignoredNetworkErrorUrlPatterns = finalOptions.ignoredNetworkErrorUrlPatterns; } CONFIG.isRecordingEnabled = finalOptions.enableRecording || CONFIG.isRecordingEnabled; const deviceId = getDeviceId(); if (!deviceId) { const newDeviceId = generateUuid(); storeDeviceId(newDeviceId); window.overcentricDeviceId = newDeviceId; } else { window.overcentricDeviceId = deviceId; } setTimeout(() => { initAutoCapture(trackEvent); if (finalOptions.enableErrorCapture) { initErrorCapture(); } if (CONFIG.isRecordingEnabled) { initRecording(window.overcentricSessionId); } if (finalOptions.enableVisitorId && options.context === "website") { initVisitorIdentification(id, window.overcentricSessionId); } initVisibilityTracking(id); handleInitialVisit(id); processQueuedCommands(); console.log("%cOvercentric initialized ✅", "color: #03ab14; font-weight: 700; font-size: 14px;"); }, 1000); } catch (e) { log.error("Overcentric: Failed to initialize:", e); isInitializing = false; } } function handleInitialVisit(projectId) { const sentKey = `overcentric_initial_visit_sent_${projectId}`; const alreadySent = document.cookie .split("; ") .find((row) => row.startsWith(sentKey)); if (alreadySent) { return; } const utmParams = getUtmParameters(); const refParam = getRefParameter(); const initialData = { initial_referrer: document.referrer || null, initial_landing_page: window.location.href, initial_ref: refParam, ...utmParams, timestamp: new Date().toISOString(), }; const domain = window.location.hostname.split(".").slice(-2).join("."); trackEvent("$initial_visit", initialData, () => { document.cookie = `${sentKey}=true; domain=.${domain}; path=/; max-age=31536000; SameSite=Strict; Secure`; log.debug("Initial visit data sent"); }); } function identify(id, info = {}) { function _identify() { if (!window.overcentricInitialized) { log.error("Not initialized"); return; } const userIdentity = { uniqueIdentifier: id, ...info, }; window.overcentricUserIdentity = userIdentity; log.debug(`Identified user: ${id}, info: ${JSON.stringify(info)}`); if (CONFIG.isRecordingEnabled && window.overcentricSessionId) { updateRecordingIdentity(window.overcentricSessionId, userIdentity); } trackEvent("$identify", userIdentity); } queueOrExecute(_identify, []); } function trackEvent(eventName, properties = {}, cb) { if (!eventName || typeof eventName !== "string" || eventName.trim().length === 0) { log.error("Event name is required and must be a non-empty string"); return; } function _trackEvent() { const projectId = window.overcentricProjectId; if (!projectId) { log.error("Library not initialized with project id"); return; } if (!window.overcentricInitialized) { log.error("Library not initialized"); return; } const context = window.overcentricContext; if (!context) { log.error("Library not initialized with context"); return; } const sessionId = window.overcentricSessionId; updateSessionActivity(projectId); const deviceId = window.overcentricDeviceId; const userIdentity = window.overcentricUserIdentity; const event = { eventName, properties, deviceId, sessionId, context, }; sendEvent(projectId, CONFIG.basePath, event, userIdentity) .then(() => { if (cb) { cb(); } }) .catch((error) => { log.debug(`Error sending event: ${error}`); }); } queueOrExecute(_trackEvent, [eventName, properties, cb]); } function initAutoCapture(trackEvent) { log.debug("Starting auto-capture"); startAutoCapture(trackEvent); } function isIgnoredUrl(url) { return CONFIG.ignoredNetworkErrorUrlPatterns.some((pattern) => url.includes(pattern)); } function initErrorCapture() { const existingErrorHandler = window.onerror; window.onerror = (message, url, line, column, error) => { log.debug(`Error caught: ${message}`); trackEvent("$error", { message, url, line, column, error: error === null || error === void 0 ? void 0 : error.toString(), }); if (existingErrorHandler && typeof existingErrorHandler === "function") { return existingErrorHandler(message, url, line, column, error); } return false; }; if (typeof window.fetch === "function" && !window.fetch.__overcentricPatched) { const originalFetch = window.fetch.bind(window); window.fetch = async (...args) => { const url = typeof args[0] === "string" ? args[0] : args[0] instanceof Request ? args[0].url : String(args[0]); let response; try { response = await originalFetch(...args); } catch (networkError) { try { if (!url.startsWith(CONFIG.basePath) && !isIgnoredUrl(url)) { log.debug(`Network error caught for: ${url}`); trackEvent("$error", { message: networkError instanceof Error ? networkError.message : String(networkError), url, type: "network", }); } } catch (_) { } throw networkError; } try { if (response.status >= 400 && !url.startsWith(CONFIG.basePath) && !isIgnoredUrl(url)) { log.debug(`HTTP error caught: ${response.status} ${url}`); trackEvent("$error", { message: `HTTP ${response.status} ${response.statusText}`, url, status: response.status, statusText: response.statusText, type: "http", }); } } catch (_) { } return response; }; window.fetch.__overcentricPatched = true; } if (typeof XMLHttpRequest !== "undefined" && !XMLHttpRequest.prototype.open.__overcentricPatched) { const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, async, username, password) { this.addEventListener("load", function () { try { if (this.status >= 400 && !String(url).startsWith(CONFIG.basePath) && !isIgnoredUrl(String(url))) { log.debug(`HTTP error caught: ${this.status} ${url}`); trackEvent("$error", { message: `HTTP ${this.status} ${this.statusText}`, url: String(url), status: this.status, statusText: this.statusText, type: "http", }); } } catch (_) { } }); this.addEventListener("error", function () { try { if (!String(url).startsWith(CONFIG.basePath) && !isIgnoredUrl(String(url))) { log.debug(`Network error caught for: ${url}`); trackEvent("$error", { message: "Network request failed", url: String(url), type: "network", }); } } catch (_) { } }); return originalOpen.call(this, method, url, async !== null && async !== void 0 ? async : true, username, password); }; XMLHttpRequest.prototype.open.__overcentricPatched = true; } } function getUtmParameters() { const utmParams = [ "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", ]; const searchParams = new URLSearchParams(window.location.search); const params = {}; utmParams.forEach((param) => { const value = searchParams.get(param); if (value) { params[param] = value; } }); return params; } function getRefParameter() { const searchParams = new URLSearchParams(window.location.search); return searchParams.get("ref"); } function setContext(ctx) { if (!ctx || (ctx !== "website" && ctx !== "product")) { log.error('Context must be either "website" or "product"'); return; } window.overcentricContext = ctx; } function isInitialized() { return window.overcentricInitialized || false; } export default { test, init, identify, trackEvent, setContext, isInitialized, };