pulsex
Version:
A lightweight and powerful JavaScript library for tracking user activity on websites. Easily monitor user interactions, including page visits, clicks, time spent, and engagement patterns. Designed for flexibility and performance, PulseX integrates seamles
343 lines (308 loc) • 10.7 kB
text/typescript
import { sessionKey } from "./constants";
import {
PulseXConfig,
EventPayload,
SectionEngagement,
EngagementTrackingTask,
ClickEvent,
HoverEvent,
FormSubmissionEvent,
} from "./types";
import {
generateSessionId,
saveToLocalStorage,
loadQueueFromLocalStorage,
deepCopy,
} from "./utils";
export default class PulseX {
private config: PulseXConfig;
private sessionId: string;
private queue: EventPayload[] = [];
private engagementTrackingTasks: EngagementTrackingTask[] = [];
constructor(config: PulseXConfig) {
this.config = {
maxQueueSize: 100,
...config,
};
const storedSession = localStorage.getItem(sessionKey);
if (storedSession) {
this.sessionId = storedSession;
} else {
this.sessionId = generateSessionId();
localStorage.setItem(sessionKey, this.sessionId);
}
this.loadQueue();
}
public start(): void {
this.startEngagementTracking();
this.setupVisibilityListener();
}
/**
* Tracks user engagement for a specific section of the webpage.
*
* @param {string} sectionId - The ID of the HTML element to track.
* @param {number} [threshold] - The minimum time (in milliseconds) the user must view the section for it to be considered engaged.
*
* @remarks
* - If the element with the given `sectionId` is not found, a warning will be logged.
* - The section will be added to the tracking list, and engagement will be recorded only if the user views it for at least `threshold` milliseconds.
* - If `threshold` is not provided, it may default to a predefined value in the tracking system.
*
* @example
* ```ts
* tracker.trackSectionEngagement("homepage", 3000); // Track "homepage" section with a 3-second threshold
* ```
*/
public trackSectionEngagement(sectionId: string, threshold?: number): void {
const element = document.getElementById(sectionId);
if (!element) {
console.warn(`PulseX: Element with ID ${sectionId} not found`);
return;
}
const task: EngagementTrackingTask = {
element,
threshold,
};
// Add this section to the list of sections to track
this.engagementTrackingTasks.push(task);
}
private startEngagementTracking(): void {
// Track user activity on those sections
// like time of section on viewport, clicks on the section, hover on the section, etc.
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const sectionId = entry.target.id;
const data: SectionEngagement = {
sectionId,
startTime: Date.now(),
endTime: 0,
totalDuration: 0,
};
const payload = this.createBasePayload("section-engagement", data);
this.queue.push(payload);
}
// stored the exiting time of the section after the user exits the section
else {
const sectionId = entry.target.id;
const startedEvent = this.queue.find(
(event) =>
event.data.endTime === 0 && event.data.sectionId === sectionId
);
if (startedEvent) {
startedEvent.data.endTime = Date.now();
startedEvent.data.totalDuration =
startedEvent.data.endTime - startedEvent.data.startTime;
}
// get the task for the section to calculate the threshold
const taskIndex = this.engagementTrackingTasks.findIndex(
(task) => task.element.id === sectionId
);
const task = this.engagementTrackingTasks[taskIndex];
if (
task &&
startedEvent &&
startedEvent?.data.totalDuration >= (task.threshold || 0)
) {
// if the user has viewed the section for the minimum threshold time
// then we can consider the user has engaged with the section
this.saveQueueToLocalStorage();
} else if (startedEvent) {
// if the user has not viewed the section for the minimum threshold time
// then we can remove the event from the queue
// this.queue.splice(this.queue.indexOf(startedEvent), 1);
this.queue.splice(taskIndex, 1);
}
}
});
},
{
threshold: 0.5,
}
);
this.engagementTrackingTasks.forEach((task) => {
if (task) {
observer.observe(task.element);
}
});
}
private createBasePayload(
type: string,
data: SectionEngagement | ClickEvent | HoverEvent | FormSubmissionEvent
): EventPayload {
return {
_id: Math.random().toString(16).substring(2, 18),
sessionId: this.sessionId,
type,
data,
pageUrl: window.location.href,
referrer: document.referrer,
createdAt: new Date().toISOString(),
};
}
private getQueue(): EventPayload[] {
const queue = localStorage.getItem("pulsex_events");
return queue ? JSON.parse(queue) : [];
}
private async sendData(): Promise<void> {
const queue = this.getQueue();
if (queue.length === 0) return;
const eventsToSend = deepCopy(queue);
this.clearQueue();
console.log("PulseX: Sending data...", eventsToSend);
try {
const response = await fetch(this.config.apiEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(eventsToSend),
});
if (!response.ok) {
throw new Error("Failed to send data");
}
} catch (error) {
console.error("PulseX: Error sending data:", error);
this.queue.unshift(...eventsToSend);
this.saveQueueToLocalStorage();
}
}
private setupVisibilityListener(): void {
// empty the queue when the tab is closed
window.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
this.sendData();
}
});
}
private saveQueueToLocalStorage(): void {
saveToLocalStorage(this.queue);
}
private clearQueue(): void {
this.queue = [];
saveToLocalStorage([]);
}
private loadQueue(): void {
this.queue = loadQueueFromLocalStorage();
}
// New Methods for Additional Event Tracking
/**
* Tracks click events on a specified element.
*
* @param {string} elementId - The ID of the element to track clicks on.
*
* @example
* ```ts
* tracker.trackClick("loginBtn"); // Tracks click events on element with ID 'loginBtn'
* ```
*/
public trackClick(elementId: string): void {
const element = document.getElementById(elementId);
if (!element) {
console.warn(`PulseX: Element with ID ${elementId} not found`);
return;
}
element.addEventListener("click", (event: MouseEvent) => {
const target = event.target as HTMLElement;
const clickData: ClickEvent = {
elementId: target.id,
textContent: target.textContent || "",
timestamp: Date.now(),
x: event.clientX,
y: event.clientY,
button: event.button,
};
const payload = this.createBasePayload("click", clickData);
this.savePayloadToLocalStorage(payload);
});
}
/**
* Tracks hover events on a specified element.
* Records hover duration and, if a click occurs during the hover, includes the click data.
*
* @param {string} elementId - The ID of the element to track hover events on.
*
* @example
* ```ts
* tracker.trackHover("product-card"); // Tracks hover events on element with ID 'product-card'
* ```
*/
public trackHover(elementId: string): void {
const element = document.getElementById(elementId);
if (!element) {
console.warn(`PulseX: Element with ID ${elementId} not found`);
return;
}
let hoverStartTime: number = 0;
let clickOccurred: boolean = false;
let clickData: any = null;
element.addEventListener("mouseover", () => {
hoverStartTime = Date.now();
clickOccurred = false;
clickData = null;
});
element.addEventListener("click", (event: MouseEvent) => {
clickOccurred = true;
const target = event.target as HTMLElement;
clickData = {
elementId: target.id,
textContent: target.textContent || "",
timestamp: Date.now(),
x: event.clientX,
y: event.clientY,
button: event.button,
};
});
element.addEventListener("mouseout", () => {
const hoverEndTime = Date.now();
const hoverDuration = hoverEndTime - hoverStartTime;
const hoverData: HoverEvent = {
elementId,
startTime: hoverStartTime,
endTime: hoverEndTime,
hoverDuration,
clicked: clickOccurred,
clickData: clickOccurred ? clickData : null,
};
const payload = this.createBasePayload("hover", hoverData);
this.savePayloadToLocalStorage(payload);
});
}
/**
* Tracks form submission events on a specified form.
*
* @param {string} formId - The ID of the form to track submissions for.
*
* @example
* ```ts
* tracker.trackFormSubmission("login-form"); // Tracks form submissions on form with ID 'login-form'
* ```
*/
public trackFormSubmission(formId: string): void {
const form = document.getElementById(formId) as HTMLFormElement;
if (!form) {
console.warn(`PulseX: Form with ID ${formId} not found`);
return;
}
form.addEventListener("submit", (event: Event) => {
event.preventDefault();
const inputValues: Record<string, string | boolean | number> = {};
new FormData(form).forEach((value, key) => {
inputValues[key] = value.toString();
});
const formData = {
formId,
timestamp: Date.now(),
inputValues,
};
const payload = this.createBasePayload("form-submission", formData);
this.savePayloadToLocalStorage(payload);
});
}
savePayloadToLocalStorage(payload: EventPayload) {
const currDataInLocalStorage = this.getQueue();
currDataInLocalStorage.push(payload);
saveToLocalStorage(currDataInLocalStorage);
}
}