overcentric
Version:
A lightweight, privacy-focused toolkit for modern SaaS web applications
369 lines (368 loc) • 11.7 kB
JavaScript
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
};