@scrubbe-auth/user-tracker
Version:
User activity and behavior tracking for analytics
1,093 lines (1,083 loc) • 35.8 kB
JavaScript
class DOMUtils {
static getSelector(element) {
if (element.id) {
return `#${element.id}`;
}
if (element.className) {
const classes = element.className.split(' ').filter(Boolean);
if (classes.length > 0) {
return `.${classes.join('.')}`;
}
}
const tagName = element.tagName.toLowerCase();
const parent = element.parentElement;
if (!parent) {
return tagName;
}
const siblings = Array.from(parent.children);
const index = siblings.indexOf(element);
return `${this.getSelector(parent)} > ${tagName}:nth-child(${index + 1})`;
}
static getElementText(element) {
// Get visible text content
const text = element.textContent?.trim();
return text && text.length > 0 ? text : null;
}
static getFormId(form) {
return form.id || form.name || this.getSelector(form);
}
static isElementVisible(element) {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return (rect.width > 0 &&
rect.height > 0 &&
style.visibility !== 'hidden' &&
style.display !== 'none' &&
style.opacity !== '0');
}
static getElementPosition(element) {
const rect = element.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
}
class EventUtils {
static throttle(func, delay) {
let timeoutId = null;
let lastExecTime = 0;
return (...args) => {
const currentTime = Date.now();
if (currentTime - lastExecTime > delay) {
func(...args);
lastExecTime = currentTime;
}
else {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func(...args);
lastExecTime = Date.now();
}, delay - (currentTime - lastExecTime));
}
};
}
static debounce(func, delay) {
let timeoutId = null;
return (...args) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func(...args);
}, delay);
};
}
static once(func) {
let called = false;
let result;
return (...args) => {
if (!called) {
called = true;
result = func(...args);
return result;
}
return undefined;
};
}
}
class Loggers {
constructor(debug = false, logLevel = 'info', outputs = [new ConsoleOutput()]) {
this.debugs = debug;
this.logLevel = logLevel;
this.outputs = outputs;
this.context = {};
}
setContext(context) {
this.context = { ...this.context, ...context };
}
clearContext() {
this.context = {};
}
debug(message, ...args) {
if (this.debugs && this.shouldLog('debug')) {
this.log('debug', message, args);
}
}
info(message, ...args) {
if (this.shouldLog('info')) {
this.log('info', message, args);
}
}
warn(message, ...args) {
if (this.shouldLog('warn')) {
this.log('warn', message, args);
}
}
error(message, ...args) {
if (this.shouldLog('error')) {
this.log('error', message, args);
}
}
shouldLog(level) {
const levels = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
return levels[level] >= levels[this.logLevel];
}
log(level, message, args) {
const logEntry = {
timestamp: new Date().toISOString(),
level,
message,
args,
context: this.context
};
this.outputs.forEach(output => {
try {
output.write(logEntry);
}
catch (error) {
console.error('Failed to write log:', error);
}
});
}
// Create child logger with additional context
child(context) {
const child = new Loggers(this.debugs, this.logLevel, this.outputs);
child.setContext({ ...this.context, ...context });
return child;
}
// Add output
addOutput(output) {
this.outputs.push(output);
}
// Remove output
removeOutput(output) {
const index = this.outputs.indexOf(output);
if (index > -1) {
this.outputs.splice(index, 1);
}
}
}
class ConsoleOutput {
write(entry) {
const prefix = `[${entry.timestamp}] [${entry.level.toUpperCase()}] [Scrubbe Analytics]`;
const contextStr = Object.keys(entry.context).length > 0
? ` ${JSON.stringify(entry.context)}`
: '';
const fullMessage = `${prefix}${contextStr} ${entry.message}`;
switch (entry.level) {
case 'debug':
console.debug(fullMessage, ...entry.args);
break;
case 'info':
console.info(fullMessage, ...entry.args);
break;
case 'warn':
console.warn(fullMessage, ...entry.args);
break;
case 'error':
console.error(fullMessage, ...entry.args);
break;
}
}
}
class BufferedOutput {
constructor(maxSize = 1000) {
this.buffer = [];
this.maxSize = maxSize;
}
write(entry) {
this.buffer.push(entry);
if (this.buffer.length > this.maxSize) {
this.buffer.shift();
}
}
getBuffer() {
return [...this.buffer];
}
clear() {
this.buffer = [];
}
flush(output) {
this.buffer.forEach(entry => output.write(entry));
this.clear();
}
}
class FileOutput {
constructor(filename) {
this.filename = filename;
}
write(entry) {
// This would write to file in Node.js environment
// For browser environment, could use IndexedDB or send to server
if (typeof window === 'undefined' && typeof require !== 'undefined') {
try {
const fs = require('fs');
const logLine = JSON.stringify(entry) + '\n';
fs.appendFileSync(this.filename, logLine);
}
catch (error) {
console.error('Failed to write to log file:', error);
}
}
}
}
class PageViewTracker {
constructor(config) {
this.config = config;
}
track(page, properties = {}) {
const activity = {
type: 'pageview',
timestamp: Date.now(),
data: {
page,
url: typeof window !== 'undefined' ? window.location.href : page,
title: typeof document !== 'undefined' ? document.title : undefined,
referrer: typeof document !== 'undefined' ? document.referrer : undefined,
search: typeof window !== 'undefined' ? window.location.search : undefined,
hash: typeof window !== 'undefined' ? window.location.hash : undefined,
...properties
}
};
this.lastPage = page;
return activity;
}
getLastPage() {
return this.lastPage;
}
}
class ClickTracker {
constructor(config) {
this.clickHistory = [];
this.config = config;
}
handleClick(event) {
const target = event.target;
if (!target)
return null;
// Check blacklist/whitelist
if (!this.shouldTrackElement(target)) {
return null;
}
const activity = this.createClickActivity(event, target);
// Track rage clicks
if (this.config.clickTracking?.trackRageClicks) {
this.trackRageClicks(target);
}
// Track dead clicks
if (this.config.clickTracking?.trackDeadClicks) {
this.trackDeadClicks(event, target);
}
return activity;
}
trackManual(selector, properties = {}) {
return {
type: 'click',
timestamp: Date.now(),
data: {
selector,
manual: true,
...properties
}
};
}
createClickActivity(event, target) {
const selector = DOMUtils.getSelector(target);
const text = DOMUtils.getElementText(target);
return {
type: 'click',
timestamp: Date.now(),
data: {
selector,
tagName: target.tagName.toLowerCase(),
text: text?.substring(0, 100), // Limit text length
href: target.getAttribute('href'),
id: target.id,
className: target.className,
x: event.clientX,
y: event.clientY,
pageX: event.pageX,
pageY: event.pageY,
button: event.button,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey
}
};
}
shouldTrackElement(element) {
const selector = DOMUtils.getSelector(element);
// Check whitelist first
if (this.config.clickTracking?.whitelistSelectors?.length) {
return this.config.clickTracking.whitelistSelectors.some(pattern => this.matchesSelector(selector, pattern));
}
// Check blacklist
if (this.config.clickTracking?.blacklistSelectors?.length) {
return !this.config.clickTracking.blacklistSelectors.some(pattern => this.matchesSelector(selector, pattern));
}
return true;
}
matchesSelector(selector, pattern) {
try {
return selector.includes(pattern) || selector.match(new RegExp(pattern));
}
catch {
return selector.includes(pattern);
}
}
trackRageClicks(element) {
const now = Date.now();
const rageWindow = 2000; // 2 seconds
const rageThreshold = 3; // 3 clicks
// Add to history
this.clickHistory.push({ timestamp: now, element });
// Clean old entries
this.clickHistory = this.clickHistory.filter(click => now - click.timestamp < rageWindow);
// Check for rage clicks on same element
const sameElementClicks = this.clickHistory.filter(click => click.element === element);
if (sameElementClicks.length >= rageThreshold) {
const activity = {
type: 'rage_click',
timestamp: now,
data: {
selector: DOMUtils.getSelector(element),
clickCount: sameElementClicks.length,
timeWindow: rageWindow
}
};
this.onClick?.(activity);
}
}
trackDeadClicks(event, target) {
// A dead click is a click that doesn't result in a page change or visible action
let hasAction = false;
// Check for immediate actions
const checkForAction = () => {
// Check if page changed
const currentUrl = window.location.href;
setTimeout(() => {
if (window.location.href !== currentUrl) {
hasAction = true;
}
}, 100);
// Check for CSS changes (simplified)
const originalDisplay = target.style.display;
setTimeout(() => {
if (target.style.display !== originalDisplay) {
hasAction = true;
}
}, 100);
};
checkForAction();
// Check after a delay
setTimeout(() => {
if (!hasAction) {
const activity = {
type: 'dead_click',
timestamp: Date.now(),
data: {
selector: DOMUtils.getSelector(target),
tagName: target.tagName.toLowerCase(),
x: event.clientX,
y: event.clientY
}
};
this.onClick?.(activity);
}
}, 500);
}
}
class ScrollTracker {
constructor(config) {
this.maxScrollDepth = 0;
this.scrollCheckpoints = new Set();
this.isThrottled = false;
this.config = config;
}
handleScroll() {
if (this.isThrottled)
return null;
const scrollDepth = this.calculateScrollDepth();
// Update max scroll depth
if (scrollDepth > this.maxScrollDepth) {
this.maxScrollDepth = scrollDepth;
}
// Check if we've hit a new checkpoint
const threshold = this.config.scrollThreshold || 25;
const checkpoint = Math.floor(scrollDepth / threshold) * threshold;
if (checkpoint > 0 && !this.scrollCheckpoints.has(checkpoint)) {
this.scrollCheckpoints.add(checkpoint);
// Throttle scroll events
this.throttle();
return this.createScrollActivity(scrollDepth, checkpoint);
}
return null;
}
trackManual(depth, properties = {}) {
return {
type: 'scroll',
timestamp: Date.now(),
data: {
depth,
manual: true,
...properties
}
};
}
calculateScrollDepth() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
const windowHeight = window.innerHeight;
const scrollableHeight = docHeight - windowHeight;
if (scrollableHeight <= 0)
return 100;
return Math.min(100, Math.round((scrollTop / scrollableHeight) * 100));
}
createScrollActivity(depth, checkpoint) {
return {
type: 'scroll',
timestamp: Date.now(),
data: {
depth,
checkpoint,
maxDepth: this.maxScrollDepth,
url: window.location.href,
title: document.title
}
};
}
throttle() {
this.isThrottled = true;
setTimeout(() => {
this.isThrottled = false;
}, 100);
}
getMaxScrollDepth() {
return this.maxScrollDepth;
}
getScrollCheckpoints() {
return Array.from(this.scrollCheckpoints).sort((a, b) => a - b);
}
}
class FormTracker {
constructor(config) {
this.formData = new Map();
this.config = config;
}
handleSubmit(event) {
const form = event.target;
if (!form)
return null;
return this.trackSubmission(DOMUtils.getFormId(form));
}
handleChange(event) {
if (!this.config.formTracking?.trackFieldChanges)
return null;
const target = event.target;
if (!target || !target.form)
return null;
return this.trackFieldChange(target, 'change');
}
handleInput(event) {
if (!this.config.formTracking?.trackFieldChanges)
return null;
const target = event.target;
if (!target || !target.form)
return null;
return this.trackFieldChange(target, 'input');
}
trackSubmission(formId, properties = {}) {
const form = document.getElementById(formId);
const formData = form ? this.extractFormData(form) : {};
return {
type: 'form_submit',
timestamp: Date.now(),
data: {
formId,
formData: this.config.formTracking?.anonymizeFormData ?
this.anonymizeFormData(formData) : formData,
url: window.location.href,
...properties
}
};
}
trackFieldChange(field, eventType) {
const formId = DOMUtils.getFormId(field.form);
const fieldName = field.name || field.id || DOMUtils.getSelector(field);
return {
type: 'form_field_change',
timestamp: Date.now(),
data: {
formId,
fieldName,
fieldType: field.type,
eventType,
hasValue: !!field.value,
valueLength: field.value?.length || 0
}
};
}
extractFormData(form) {
const data = {};
const formData = new FormData(form);
//@ts-ignore
for (const [key, value] of formData) {
data[key] = value;
}
return data;
}
anonymizeFormData(data) {
const anonymized = {};
for (const [key, value] of Object.entries(data)) {
if (this.isSensitiveField(key)) {
anonymized[key] = '[REDACTED]';
}
else {
anonymized[key] = value;
}
}
return anonymized;
}
isSensitiveField(fieldName) {
const sensitivePatterns = [
/password/i,
/credit.?card/i,
/ssn/i,
/social.?security/i,
/email/i,
/phone/i,
/address/i
];
return sensitivePatterns.some(pattern => pattern.test(fieldName));
}
}
class SessionManager {
constructor(config) {
this.config = config;
this.session = this.createSession();
}
start() {
this.startHeartbeat();
this.resetTimeout();
}
stop() {
this.stopHeartbeat();
this.stopTimeout();
this.session.endTime = Date.now();
this.session.duration = this.session.endTime - this.session.startTime;
}
reset() {
this.stop();
this.session = this.createSession();
this.start();
}
getSession() {
this.updateSession();
return { ...this.session };
}
getCurrentSessionId() {
return this.session.id;
}
createSession() {
return {
id: this.generateSessionId(),
startTime: Date.now(),
lastActivity: Date.now(),
duration: 0,
pageViews: 0,
clicks: 0,
scrollDepth: 0,
events: 0,
isActive: true
};
}
updateSession() {
const now = Date.now();
this.session.duration = now - this.session.startTime;
this.session.lastActivity = now;
this.session.events = (this.session.events || 0) + 1;
}
generateSessionId() {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
this.updateSession();
}, 30000); // 30 seconds
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined;
}
}
resetTimeout() {
this.stopTimeout();
this.timeoutTimer = setTimeout(() => {
this.session.isActive = false;
this.stop();
}, this.config.sessionTimeout);
}
stopTimeout() {
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = undefined;
}
}
}
class UserTracker {
constructor(config = {}) {
this.isTracking = false;
this.eventListeners = [];
this.activityBuffer = [];
this.throttledMouseMove = EventUtils.throttle((event) => {
const activity = {
type: 'mousemove',
timestamp: Date.now(),
data: {
x: event.clientX,
y: event.clientY,
sessionId: this.sessionManager.getCurrentSessionId()
}
};
this.addActivity(activity);
}, 100);
this.config = {
enablePageViews: config.enablePageViews ?? true,
enableClicks: config.enableClicks ?? true,
enableScrolling: config.enableScrolling ?? true,
enableForms: config.enableForms ?? true,
enableSessions: config.enableSessions ?? true,
enableMouseMovement: config.enableMouseMovement ?? false,
enableKeyboardEvents: config.enableKeyboardEvents ?? false,
enableVisibilityTracking: config.enableVisibilityTracking ?? true,
sessionTimeout: config.sessionTimeout || 1800000, // 30 minutes
scrollThreshold: config.scrollThreshold || 25, // 25% increments
clickTracking: {
trackAllClicks: config.clickTracking?.trackAllClicks ?? true,
trackRageClicks: config.clickTracking?.trackRageClicks ?? true,
trackDeadClicks: config.clickTracking?.trackDeadClicks ?? true,
blacklistSelectors: config.clickTracking?.blacklistSelectors || [],
whitelistSelectors: config.clickTracking?.whitelistSelectors || [],
...config.clickTracking
},
formTracking: {
trackSubmissions: config.formTracking?.trackSubmissions ?? true,
trackFieldChanges: config.formTracking?.trackFieldChanges ?? false,
trackValidationErrors: config.formTracking?.trackValidationErrors ?? true,
anonymizeFormData: config.formTracking?.anonymizeFormData ?? true,
...config.formTracking
},
batchSize: config.batchSize || 50,
flushInterval: config.flushInterval || 10000, // 10 seconds
debug: config.debug ?? false,
respectPrivacy: config.respectPrivacy ?? true,
...config
};
this.logger = new Loggers(this.config.debug);
// Initialize tracking components
this.sessionManager = new SessionManager(this.config);
this.pageViewTracker = new PageViewTracker(this.config);
this.clickTracker = new ClickTracker(this.config);
this.scrollTracker = new ScrollTracker(this.config);
this.formTracker = new FormTracker(this.config);
// Set up event handlers
this.setupEventHandlers();
this.logger.debug('UserTracker initialized', this.config);
}
start() {
if (this.isTracking) {
this.logger.warn('User tracking already started');
return;
}
if (!this.canTrack()) {
this.logger.info('User tracking disabled due to privacy settings');
return;
}
try {
this.isTracking = true;
// Start session management
if (this.config.enableSessions) {
this.sessionManager.start();
}
// Set up DOM event listeners
this.setupEventListeners();
// Start automatic flushing
this.startAutoFlush();
// Track initial page view
if (this.config.enablePageViews) {
this.trackInitialPageView();
}
this.logger.info('User tracking started');
}
catch (error) {
this.logger.error('Failed to start user tracking:', error);
this.isTracking = false;
}
}
stop() {
if (!this.isTracking) {
return;
}
try {
this.isTracking = false;
// Remove all event listeners
this.removeEventListeners();
// Stop auto-flush
this.stopAutoFlush();
// Flush remaining activities
this.flush();
// Stop session
if (this.config.enableSessions) {
this.sessionManager.stop();
}
this.logger.info('User tracking stopped');
}
catch (error) {
this.logger.error('Error stopping user tracking:', error);
}
}
// Manual tracking methods
trackPageView(page, properties = {}) {
if (!this.isTracking)
return;
const activity = this.pageViewTracker.track(page, properties);
this.addActivity(activity);
}
trackClick(selector, properties = {}) {
if (!this.isTracking)
return;
const activity = this.clickTracker.trackManual(selector, properties);
this.addActivity(activity);
}
trackFormSubmit(formId, properties = {}) {
if (!this.isTracking)
return;
const activity = this.formTracker.trackSubmission(formId, properties);
this.addActivity(activity);
}
trackScroll(depth, properties = {}) {
if (!this.isTracking)
return;
const activity = this.scrollTracker.trackManual(depth, properties);
this.addActivity(activity);
}
trackCustomEvent(eventName, properties = {}) {
if (!this.isTracking)
return;
const activity = {
type: 'custom',
timestamp: Date.now(),
data: {
eventName,
...properties,
sessionId: this.sessionManager.getCurrentSessionId(),
url: window.location.href,
referrer: document.referrer
}
};
this.addActivity(activity);
}
// Session management
getSession() {
return this.sessionManager.getSession();
}
resetSession() {
this.sessionManager.reset();
}
// Activity management
addActivity(activity) {
this.activityBuffer.push(activity);
// Auto-flush if buffer is full
if (this.activityBuffer.length >= this.config.batchSize) {
this.flush();
}
}
flush() {
const activities = [...this.activityBuffer];
this.activityBuffer = [];
if (activities.length > 0) {
this.emitActivities(activities);
this.logger.debug(`Flushed ${activities.length} activities`);
}
return activities;
}
emitActivities(activities) {
// Emit events for external consumption
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('userActivities', {
detail: { activities, session: this.getSession() }
}));
}
}
// Event listener setup
setupEventHandlers() {
// Page view tracking
if (this.config.enablePageViews) {
this.pageViewTracker.onPageView = (activity) => this.addActivity(activity);
}
// Click tracking
if (this.config.enableClicks) {
this.clickTracker.onClick = (activity) => this.addActivity(activity);
}
// Scroll tracking
if (this.config.enableScrolling) {
this.scrollTracker.onScroll = (activity) => this.addActivity(activity);
}
// Form tracking
if (this.config.enableForms) {
this.formTracker.onFormEvent = (activity) => this.addActivity(activity);
}
}
setupEventListeners() {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}
// Click events
if (this.config.enableClicks) {
this.addEventListener(document, 'click', this.handleClick.bind(this));
}
// Scroll events
if (this.config.enableScrolling) {
this.addEventListener(window, 'scroll', this.handleScroll.bind(this));
}
// Form events
if (this.config.enableForms) {
this.addEventListener(document, 'submit', this.handleFormSubmit.bind(this));
if (this.config.formTracking.trackFieldChanges) {
this.addEventListener(document, 'change', this.handleFormChange.bind(this));
this.addEventListener(document, 'input', this.handleFormInput.bind(this));
}
}
// Mouse movement (if enabled)
if (this.config.enableMouseMovement) {
this.addEventListener(document, 'mousemove', this.handleMouseMove.bind(this));
}
// Keyboard events (if enabled)
if (this.config.enableKeyboardEvents) {
this.addEventListener(document, 'keydown', this.handleKeyDown.bind(this));
}
// Visibility tracking
if (this.config.enableVisibilityTracking) {
this.addEventListener(document, 'visibilitychange', this.handleVisibilityChange.bind(this));
}
// Page navigation
if (this.config.enablePageViews) {
this.addEventListener(window, 'popstate', this.handlePopState.bind(this));
this.interceptHistoryAPI();
}
// Page unload
this.addEventListener(window, 'beforeunload', this.handleBeforeUnload.bind(this));
}
addEventListener(element, event, handler) {
const wrappedHandler = (event) => handler(event);
element.addEventListener(event, wrappedHandler);
this.eventListeners.push({ element, event, handler: wrappedHandler });
}
removeEventListeners() {
this.eventListeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
this.eventListeners = [];
}
// Event handlers
handleClick(event) {
if (!this.isTracking)
return;
const activity = this.clickTracker.handleClick(event);
if (activity) {
this.addActivity(activity);
}
}
handleScroll() {
if (!this.isTracking)
return;
const activity = this.scrollTracker.handleScroll();
if (activity) {
this.addActivity(activity);
}
}
handleFormSubmit(event) {
if (!this.isTracking)
return;
const activity = this.formTracker.handleSubmit(event);
if (activity) {
this.addActivity(activity);
}
}
handleFormChange(event) {
if (!this.isTracking)
return;
const activity = this.formTracker.handleChange(event);
if (activity) {
this.addActivity(activity);
}
}
handleFormInput(event) {
if (!this.isTracking)
return;
const activity = this.formTracker.handleInput(event);
if (activity) {
this.addActivity(activity);
}
}
handleMouseMove(event) {
if (!this.isTracking)
return;
// Throttle mouse move events
this.throttledMouseMove(event);
}
handleKeyDown(event) {
if (!this.isTracking)
return;
// Only track non-sensitive keys
if (this.isSensitiveKey(event.key))
return;
const activity = {
type: 'keydown',
timestamp: Date.now(),
data: {
key: event.key,
code: event.code,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey,
sessionId: this.sessionManager.getCurrentSessionId()
}
};
this.addActivity(activity);
}
handleVisibilityChange() {
if (!this.isTracking)
return;
const activity = {
type: 'visibility',
timestamp: Date.now(),
data: {
hidden: document.hidden,
visibilityState: document.visibilityState,
sessionId: this.sessionManager.getCurrentSessionId()
}
};
this.addActivity(activity);
}
handlePopState() {
if (!this.isTracking)
return;
setTimeout(() => {
this.trackPageView(window.location.pathname);
}, 0);
}
handleBeforeUnload() {
// Flush remaining activities before page unload
this.flush();
}
// History API interception for SPA navigation
interceptHistoryAPI() {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = (...args) => {
originalPushState.apply(history, args);
setTimeout(() => {
this.trackPageView(window.location.pathname);
}, 0);
};
history.replaceState = (...args) => {
originalReplaceState.apply(history, args);
setTimeout(() => {
this.trackPageView(window.location.pathname);
}, 0);
};
}
trackInitialPageView() {
if (typeof window !== 'undefined') {
this.trackPageView(window.location.pathname, {
title: document.title,
referrer: document.referrer,
isInitial: true
});
}
}
// Auto-flush management
startAutoFlush() {
this.flushTimer = setInterval(() => {
this.flush();
}, this.config.flushInterval);
}
stopAutoFlush() {
if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = undefined;
}
}
// Utility methods
canTrack() {
if (!this.config.respectPrivacy)
return true;
// Check Do Not Track
if (typeof navigator !== 'undefined') {
const dnt = navigator.doNotTrack;
if (dnt === '1' || dnt === 'yes') {
return false;
}
}
// Check consent (if implemented)
if (typeof localStorage !== 'undefined') {
const consent = localStorage.getItem('tracking_consent');
if (consent === 'false') {
return false;
}
}
return true;
}
isSensitiveKey(key) {
const sensitiveKeys = [
'Tab', 'CapsLock', 'Shift', 'Control', 'Alt', 'Meta',
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'
];
return sensitiveKeys.includes(key);
}
// Statistics and analytics
getTrackingStats() {
const session = this.getSession();
return {
isTracking: this.isTracking,
sessionInfo: session,
activitiesBuffered: this.activityBuffer.length,
totalActivities: session.events || 0,
trackingStartTime: session.startTime,
lastActivity: session.lastActivity
};
}
// Configuration updates
updateConfig(config) {
Object.assign(this.config, config);
this.logger.debug('Configuration updated', config);
}
getConfig() {
return { ...this.config };
}
// Event listener for external integration
onActivity(callback) {
if (typeof window !== 'undefined') {
window.addEventListener('userActivities', (event) => {
callback(event.detail.activities, event.detail.session);
});
}
}
// Cleanup
destroy() {
this.stop();
this.logger.debug('UserTracker destroyed');
}
}
export { BufferedOutput, ClickTracker, ConsoleOutput, DOMUtils, EventUtils, FileOutput, FormTracker, Loggers, PageViewTracker, ScrollTracker, SessionManager, UserTracker, UserTracker as default };
//# sourceMappingURL=index.esm.js.map