UNPKG

overcentric

Version:

A lightweight, privacy-focused toolkit for modern SaaS web applications

369 lines (368 loc) 11.7 kB
import { sendEvent } from './eventSender'; import { initRecording } from './recorder'; import { startAutoCapture } from './autoCapture'; import { initDock, updateDockIdentity } from './dock'; import { getDeviceId, storeDeviceId, generateUuid } from './identity'; import { getSessionId, updateSessionActivity, initVisibilityTracking } from './session'; const isBrowser = () => typeof window !== 'undefined'; // Default configuration export const CONFIG = { basePath: 'https://app.overcentric.com/api', debugMode: false, isDockEnabled: false, dockColor: '#35b8a6', isRecordingEnabled: false, autoCapture: { click: true, scroll: false, formSubmit: false, inputFocus: false, historyChange: true, visibilityChange: false }, initDefaults: { basePath: '', debugMode: false, enableDock: false, enableRecording: false, enableAutoCapture: true, enableErrorCapture: false, dockColor: '#35b8a6' } }; // Logging utilities export const log = { debug: (message) => { if (!CONFIG.debugMode) return; console.log(`Overcentric: ${message}`); }, error: (message, info) => { console.error(`Overcentric: ${message}`, info); }, warn: (message) => { console.warn(`Overcentric: ${message}`); } }; /** * Test function to show that the library was imported. */ function test() { return 'Overcentric: Test ran successfully'; } /** * Handle auto-initialize from script tag only in browser environment. */ 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; 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; } log.debug('Initializing from script tag'); init(projectId, { context: ctx, debugMode: isDebugEnabled }); } /** * Fetch configuration from the API * @param id The project ID to use * @param basePath The base path to fetch configuration from * @returns Promise<Partial<InitOptions>> */ async function fetchConfig(id, basePath) { try { const url = `${basePath}/config/${id}`; const response = await fetch(url); if (!response.ok) { log.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, enableDock: config.dock, enableRecording: config.recording, enableAutoCapture: config.auto_capture, enableErrorCapture: config.error_capture, dockColor: config.dock_color, }; } catch (error) { log.error(`Failed to fetch configuration: ${error}`); return {}; } } /** * Initialize the overcentric library. * @param id The project ID to use. * @param options Optional configuration options. * @returns Promise<void> */ async function init(id, options) { if (!isBrowser()) { log.warn('Not initializing in non-browser environment'); return; } if (!options.context) { log.error('Context must be specified as either "website" or "product"'); return; } if (!id) { log.error('Project ID is required'); return; } try { window.overcentricProjectId = id; window.overcentricContext = options.context; const bp = options.basePath || CONFIG.basePath; if (!bp) { log.error('Base path is required'); return; } const remoteConfig = await fetchConfig(id, bp); if (!remoteConfig || Object.keys(remoteConfig).length === 0) { return; } window.overcentricProjectId = id; window.overcentricInitialized = true; // Merge configurations, prioritize manual options over remote config const finalOptions = { ...CONFIG.initDefaults, ...remoteConfig, ...options }; CONFIG.basePath = finalOptions.basePath || CONFIG.basePath; CONFIG.debugMode = finalOptions.debugMode || CONFIG.debugMode; CONFIG.isDockEnabled = finalOptions.enableDock || CONFIG.isDockEnabled; CONFIG.isRecordingEnabled = finalOptions.enableRecording || CONFIG.isRecordingEnabled; CONFIG.dockColor = finalOptions.dockColor || CONFIG.dockColor; const deviceId = getDeviceId(); if (!deviceId) { const newDeviceId = generateUuid(); storeDeviceId(newDeviceId); window.overcentricDeviceId = newDeviceId; } else { window.overcentricDeviceId = deviceId; } // Timeout to allow for any calls to identify() setTimeout(() => { if (finalOptions.enableAutoCapture) { const config = { ...CONFIG.autoCapture, ...finalOptions.autoCaptureConfig }; initAutoCapture(trackEvent, config); } if (finalOptions.enableErrorCapture) { initErrorCapture(); } if (CONFIG.isRecordingEnabled) { initRecording(); } if (CONFIG.isDockEnabled) { initDock(id, window.overcentricUserIdentity, { color: CONFIG.dockColor }); } // Initialize visibility tracking for session management initVisibilityTracking(); handleInitialVisit(); }, 750); } catch (e) { log.error('Overcentric: Failed to initialize:', e); } } /** * Handle initial visit data - store and send if not already sent. * @returns void */ function handleInitialVisit() { const sentKey = 'overcentric_initial_visit_sent'; const alreadySent = document.cookie .split('; ') .find(row => row.startsWith(sentKey)); if (alreadySent) { return; } const utmParams = getUtmParameters(); const initialData = { initial_referrer: document.referrer || null, initial_landing_page: window.location.href, ...utmParams, timestamp: new Date().toISOString() }; // Store and send the initial visit data 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'); }); } /** * Identify a user. * @param id The user ID to identify. * @param info Optional user information to attach to the identify event. * @returns void */ function identify(id, info = {}) { if (!window.overcentricInitialized) { log.error('Not initialized'); return; } if (!id) { log.error('User ID is required'); return; } const userIdentity = { uniqueIdentifier: id, ...info }; window.overcentricUserIdentity = userIdentity; log.debug(`Identified user: ${id}, info: ${JSON.stringify(info)}`); if (CONFIG.isDockEnabled) { updateDockIdentity(userIdentity); } trackEvent('$identify', userIdentity); } /** * Track an event. * @param eventName The name of the event. * @param properties Optional properties to attach to the event. * @returns void */ function trackEvent(eventName, properties = {}, cb = null) { const projectId = window.overcentricProjectId; if (!projectId) { log.error('Library not initialized with project id'); return; } if (!window.overcentricInitialized) { return; } const context = window.overcentricContext; if (!context) { log.error('Library not initialized with context'); return; } const sessionId = getSessionId(); updateSessionActivity(); 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}`); }); } /** * Start the auto-capture feature. * @returns void */ function initAutoCapture(trackEvent, config) { log.debug('Starting auto-capture'); startAutoCapture(trackEvent, config); } /** * Initialize the automatic error capturing feature. * @returns void */ 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() // Convert error to string to ensure it's serializable }); // Call existing handler if present if (existingErrorHandler && typeof existingErrorHandler === 'function') { return existingErrorHandler(message, url, line, column, error); } // Return false to allow error to propagate return false; }; } /** * Get UTM parameters from URL */ 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 setContext(ctx) { window.overcentricContext = ctx; } function isInitialized() { return window.overcentricInitialized || false; } export default { test, init, identify, trackEvent, setContext, isInitialized };