UNPKG

@hubhorizonllc/tracker

Version:

Tracks and analyzes user behavior using Chrome's TextClassifier

224 lines (223 loc) 8.75 kB
import { generateSummary } from "./summary-generator"; import { classifyText } from "./classifier"; export class UserBehaviorTracker { constructor(options = {}) { this.interactions = []; this.timeoutId = null; this.callback = null; this.isSupported = false; this.handleClick = (event) => { const target = event.target; const targetInfo = this.getElementInfo(target); this.recordInteraction({ type: "click", target: targetInfo, timestamp: Date.now(), metadata: { x: event.clientX, y: event.clientY, path: this.getElementPath(target), }, }); }; this.handleScroll = () => { // Throttle scroll events if (!this.lastScrollTime || Date.now() - this.lastScrollTime > 1000) { this.lastScrollTime = Date.now(); const scrollPercentage = Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100); this.recordInteraction({ type: "scroll", value: `${scrollPercentage}%`, timestamp: Date.now(), metadata: { scrollY: window.scrollY, viewportHeight: window.innerHeight, documentHeight: document.documentElement.scrollHeight, }, }); } }; this.lastScrollTime = null; this.handleInput = (event) => { const target = event.target; const targetInfo = this.getElementInfo(target); // Don't record actual input values for privacy, just the fact that input occurred this.recordInteraction({ type: "input", target: targetInfo, timestamp: Date.now(), metadata: { inputType: target.type || "text", hasValue: !!target.value, path: this.getElementPath(target), }, }); }; this.handleError = (event) => { this.recordInteraction({ type: "error", value: event.message, timestamp: Date.now(), metadata: { filename: event.filename, lineno: event.lineno, colno: event.colno, }, }); }; this.options = { debug: options.debug ?? false, autoSuggest: options.autoSuggest ?? false, minInteractions: options.minInteractions ?? 5, maxCollectionTime: options.maxCollectionTime ?? 30000, // 30 seconds promptTemplate: options.promptTemplate ?? "Analyze this user behavior and classify the sentiment: {{summary}}", }; this.startTime = Date.now(); this.checkSupport(); } async checkSupport() { try { // Check if TextClassifier API is available this.isSupported = "textClassifier" in navigator && typeof navigator.textClassifier?.classify === "function"; if (this.options.debug) { console.log(`TextClassifier API ${this.isSupported ? "is" : "is not"} supported`); } } catch (error) { this.isSupported = false; if (this.options.debug) { console.error("Error checking TextClassifier support:", error); } } } start(callback) { if (!this.isSupported) { console.warn("TextClassifier API is not supported in this browser. SDK will collect data but classification will not work."); } this.callback = callback; this.attachEventListeners(); // Set timeout for max collection time this.timeoutId = window.setTimeout(() => { this.processInteractions(); }, this.options.maxCollectionTime); if (this.options.debug) { console.log("UserBehaviorTracker started"); } } stop() { this.detachEventListeners(); if (this.timeoutId) { window.clearTimeout(this.timeoutId); this.timeoutId = null; } if (this.options.debug) { console.log("UserBehaviorTracker stopped"); } } attachEventListeners() { // Click events document.addEventListener("click", this.handleClick); // Scroll events window.addEventListener("scroll", this.handleScroll); // Input events document.addEventListener("input", this.handleInput); // Error events window.addEventListener("error", this.handleError); } detachEventListeners() { document.removeEventListener("click", this.handleClick); window.removeEventListener("scroll", this.handleScroll); document.removeEventListener("input", this.handleInput); window.removeEventListener("error", this.handleError); } getElementInfo(element) { // Try to get the most descriptive identifier for the element const id = element.id ? `#${element.id}` : ""; const classes = element.className && typeof element.className === "string" ? `.${element.className.split(" ").join(".")}` : ""; const tagName = element.tagName.toLowerCase(); const text = element.textContent?.trim().substring(0, 20); return (id || (element.getAttribute("data-testid") ? `[data-testid="${element.getAttribute("data-testid")}"]` : "") || (text ? `${tagName}:contains("${text}")` : tagName + classes)); } getElementPath(element, maxDepth = 3) { const path = []; let current = element; let depth = 0; while (current && depth < maxDepth) { path.unshift(this.getElementInfo(current)); current = current.parentElement; depth++; } return path.join(" > "); } recordInteraction(interaction) { this.interactions.push(interaction); if (this.options.debug) { console.log("Recorded interaction:", interaction); } // Check if we've reached the minimum number of interactions if (this.interactions.length >= this.options.minInteractions) { this.processInteractions(); } } async processInteractions() { if (this.interactions.length === 0) return; // Generate summary text const summary = generateSummary(this.interactions, this.startTime); if (this.options.debug) { console.log("Generated summary:", summary); } // Only attempt classification if the API is supported if (this.isSupported) { try { // Apply prompt template const prompt = this.options.promptTemplate.replace("{{summary}}", summary); // Classify the text const result = await classifyText(prompt); if (this.callback) { this.callback(result); } if (this.options.debug) { console.log("Classification result:", result); } if (this.options.autoSuggest && result.score > 0.7 && result.sentiment === "frustrated") { console.warn("User appears frustrated. Consider improving the UX in this area."); } } catch (error) { if (this.options.debug) { console.error("Classification error:", error); } } } else if (this.callback) { // If not supported, return a default result this.callback({ sentiment: "unknown", score: 0, rawResponse: { error: "TextClassifier API not supported" }, }); } // Reset interactions this.interactions = []; this.startTime = Date.now(); // Reset timeout if (this.timeoutId) { window.clearTimeout(this.timeoutId); this.timeoutId = window.setTimeout(() => { this.processInteractions(); }, this.options.maxCollectionTime); } } addCustomInteraction(type, data) { this.recordInteraction({ type: type, value: JSON.stringify(data), timestamp: Date.now(), metadata: { custom: true, data }, }); } }