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
JavaScript
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,
};