visitory
Version:
Visitory Session Tracking SDK
350 lines (349 loc) • 12.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionTracker = void 0;
const DEFAULT_API_ENDPOINT = 'https://qualified-hound-398.convex.site';
class SessionTracker {
constructor(apiKey, apiEndpoint = DEFAULT_API_ENDPOINT) {
this.events = [];
this.isRecording = false;
this.consoleHandlers = new Map();
this.retryCount = 0;
this.maxRetries = 3;
this.retryDelay = 1000; // 1 second
this.lastUrl = window.location.href;
this.stopTimeout = null;
this.minSessionDuration = 1000; // Minimum 1 second duration
this.unloadHandler = null;
this.STORAGE_KEY = 'visitory_session';
this.SESSION_TIMEOUT = 60 * 1000; // 1 minute
this.lastEventType = null;
this.lastEventTimestamp = null;
this.EVENT_DEBOUNCE = 100; // 100ms debounce for similar events
if (SessionTracker.instance) {
console.log('[SessionTracker] Using existing session:', SessionTracker.instance.sessionId);
return SessionTracker.instance;
}
this.apiKey = apiKey;
this.apiEndpoint = apiEndpoint;
// Try to restore existing session
const storedSession = this.getStoredSession();
if (storedSession && !this.isSessionExpired(storedSession.startTime)) {
this.sessionId = storedSession.sessionId;
this.startTime = storedSession.startTime;
this.events = storedSession.events;
console.log('[SessionTracker] Restored existing session:', {
sessionId: this.sessionId,
startTime: new Date(this.startTime).toISOString(),
eventCount: this.events.length,
});
}
else {
// Create new session
this.sessionId = Math.random().toString(36).substring(7);
this.startTime = Date.now();
this.events = [];
console.log('[SessionTracker] Created new session:', {
sessionId: this.sessionId,
startTime: new Date(this.startTime).toISOString(),
apiEndpoint: this.apiEndpoint,
});
}
SessionTracker.instance = this;
// Set up unload handler
this.unloadHandler = () => {
if (this.isRecording) {
console.log('[SessionTracker] Page unloading, saving session:', this.sessionId);
this.saveSession();
}
};
window.addEventListener('beforeunload', this.unloadHandler);
// Start recording immediately
this.start();
}
getStoredSession() {
try {
const stored = localStorage.getItem(this.STORAGE_KEY);
if (!stored)
return null;
return JSON.parse(stored);
}
catch (error) {
console.error('[SessionTracker] Error reading stored session:', error);
return null;
}
}
saveSessionState() {
try {
const sessionState = {
sessionId: this.sessionId,
startTime: this.startTime,
events: this.events,
};
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(sessionState));
console.log('[SessionTracker] Session state saved:', {
sessionId: this.sessionId,
eventCount: this.events.length,
});
}
catch (error) {
console.error('[SessionTracker] Error saving session state:', error);
}
}
isSessionExpired(startTime) {
return Date.now() - startTime > this.SESSION_TIMEOUT;
}
addEvent(event) {
this.events.push(event);
console.log('[SessionTracker] Event recorded:', {
sessionId: this.sessionId,
type: event.type,
timestamp: new Date(event.timestamp).toISOString(),
});
this.saveSessionState();
}
start() {
if (this.isRecording) {
console.log('[SessionTracker] Session already recording:', this.sessionId);
return;
}
this.isRecording = true;
console.log('[SessionTracker] Starting session recording:', {
sessionId: this.sessionId,
startTime: new Date(this.startTime).toISOString(),
});
// Add initial page load event
this.addEvent({
type: 'PAGE_LOAD',
timestamp: Date.now(),
data: {
url: window.location.href,
pageTitle: document.title,
},
});
// Track button clicks
document.addEventListener('click', (event) => {
const target = event.target;
// Track button clicks
if (target.tagName === 'BUTTON' || target.closest('button')) {
const button = target.tagName === 'BUTTON' ? target : target.closest('button');
this.addEvent({
type: 'BUTTON_CLICK',
timestamp: Date.now(),
data: {
url: window.location.href,
pageTitle: document.title,
elementId: button.id,
elementClass: button.className,
elementText: button.textContent || undefined,
},
});
}
// Track link clicks
if (target.tagName === 'A' || target.closest('a')) {
const link = (target.tagName === 'A' ? target : target.closest('a'));
this.addEvent({
type: 'LINK_CLICK',
timestamp: Date.now(),
data: {
url: window.location.href,
pageTitle: document.title,
elementId: link.id,
elementClass: link.className,
elementText: link.textContent || undefined,
linkHref: link.href,
},
});
}
});
// Track form submissions
document.addEventListener('submit', (event) => {
const form = event.target;
this.addEvent({
type: 'FORM_SUBMIT',
timestamp: Date.now(),
data: {
url: window.location.href,
pageTitle: document.title,
formId: form.id,
formAction: form.action,
},
});
});
// Track input changes with debounce
let inputTimeout = null;
document.addEventListener('input', (event) => {
const target = event.target;
if (inputTimeout) {
clearTimeout(inputTimeout);
}
inputTimeout = window.setTimeout(() => {
this.addEvent({
type: 'INPUT_CHANGE',
timestamp: Date.now(),
data: {
url: window.location.href,
pageTitle: document.title,
elementId: target.id,
elementClass: target.className,
value: target.value,
},
});
}, 500); // 500ms debounce
});
// Track scroll events with throttling
let lastScrollTime = 0;
window.addEventListener('scroll', () => {
const now = Date.now();
if (now - lastScrollTime > 100) {
// Throttle to 100ms
lastScrollTime = now;
this.addEvent({
type: 'SCROLL',
timestamp: now,
data: {
url: window.location.href,
pageTitle: document.title,
scrollPosition: window.scrollY,
},
});
}
});
// Track mouse movements with throttling
let lastMouseMoveTime = 0;
document.addEventListener('mousemove', (event) => {
const now = Date.now();
if (now - lastMouseMoveTime > 100) {
// Throttle to 100ms
lastMouseMoveTime = now;
this.addEvent({
type: 'MOUSE_MOVE',
timestamp: now,
data: {
url: window.location.href,
pageTitle: document.title,
mouseX: event.clientX,
mouseY: event.clientY,
},
});
}
});
// Track key presses
document.addEventListener('keydown', (event) => {
this.addEvent({
type: 'KEY_PRESS',
timestamp: Date.now(),
data: {
url: window.location.href,
pageTitle: document.title,
keyCode: event.keyCode,
key: event.key,
},
});
});
// Track page loads
window.addEventListener('load', () => {
this.addEvent({
type: 'PAGE_LOAD',
timestamp: Date.now(),
data: {
url: window.location.href,
pageTitle: document.title,
},
});
});
}
stop() {
if (!this.isRecording) {
console.log('[SessionTracker] Session not recording:', this.sessionId);
return;
}
this.isRecording = false;
this.saveSession();
}
async saveSession() {
const endTime = Date.now();
const duration = endTime - this.startTime;
const sessionData = {
sessionId: this.sessionId,
startTime: this.startTime,
endTime,
duration,
events: this.events,
metadata: {
url: window.location.href,
userAgent: navigator.userAgent,
deviceType: this.getDeviceType(),
browser: this.getBrowser(),
os: this.getOS(),
screenSize: {
width: window.innerWidth,
height: window.innerHeight,
},
},
};
try {
const response = await fetch(`${this.apiEndpoint}/session-replays`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify(sessionData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
console.log('[SessionTracker] Session saved successfully:', {
sessionId: this.sessionId,
eventCount: this.events.length,
});
// Clear stored session after successful save
localStorage.removeItem(this.STORAGE_KEY);
}
catch (error) {
console.error('[SessionTracker] Error saving session:', error);
}
}
getDeviceType() {
const ua = navigator.userAgent;
if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) {
return 'tablet';
}
if (/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(ua)) {
return 'mobile';
}
return 'desktop';
}
getBrowser() {
const ua = navigator.userAgent;
let browser = 'unknown';
if (ua.includes('Firefox'))
browser = 'firefox';
else if (ua.includes('Chrome'))
browser = 'chrome';
else if (ua.includes('Safari'))
browser = 'safari';
else if (ua.includes('Edge'))
browser = 'edge';
else if (ua.includes('MSIE') || ua.includes('Trident/'))
browser = 'ie';
return browser;
}
getOS() {
const ua = navigator.userAgent;
let os = 'unknown';
if (ua.includes('Windows'))
os = 'windows';
else if (ua.includes('Mac'))
os = 'macos';
else if (ua.includes('Linux'))
os = 'linux';
else if (ua.includes('Android'))
os = 'android';
else if (ua.includes('iOS'))
os = 'ios';
return os;
}
}
exports.SessionTracker = SessionTracker;
SessionTracker.instance = null;