const MOUSE_MOVE_TIMEOUT_MS = 100; globalThis.Logger = class Logger { #abort; #mouseMoveTimeout; static get url() { const url = new URL(window.location); url.pathname = "/log"; return url; } /** * Return a best-effort unique string that identifies this client * @returns {String} */ static async #fingerprint() { // Create a hash from various identifying browser data const buffer = await window.crypto.subtle.digest("SHA-1", new TextEncoder().encode(JSON.stringify([ navigator.userAgent, navigator.buildId, navigator.languages ]))); return new DataView(buffer).getBigUint64().toString(); } /** * Extract desired MouseEvent data into an object literal * @param {MouseEvent} event * @returns {Object} */ static #mouseEvent(event) { return { e: event.type, w: window.innerWidth, h: window.innerHeight, x: event.x, y: event.y } } /** * Extract desired KeyboardEvent data into an object literal * @param {KeyboardEvent} event * @returns {Object} */ static #keyEvent(event) { return { e: event.type, c: event.key, s: event.shiftKey } } constructor() {} /** * Start logging user activities */ start() { this.#abort = new AbortController(); document.addEventListener("keyup", (event) => this.log(Logger.#keyEvent(event)), { signal: this.#abort.signal }); document.addEventListener("keydown", (event) => this.log(Logger.#keyEvent(event)), { signal: this.#abort.signal }); document.addEventListener("click", (event) => this.log(Logger.#mouseEvent(event)), { signal: this.#abort.signal }); document.addEventListener("mousemove", (event) => { // Throttle mousemove events clearTimeout(this.#mouseMoveTimeout); //this.#mouseMoveTimeout = setTimeout(() => this.#log(mouseEvent(event)), MOUSE_MOVE_TIMEOUT_MS); }, { signal: this.#abort.signal }); } /** * Stop logging user activitiers */ stop() { this.#abort.abort(); } /** * Log user data * @param {Object} data * @returns {Response} */ async log(data) { return await fetch(Logger.url, { body: JSON.stringify({ client: data, fingerprint: (await Logger.#fingerprint()) }), method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, VV.header) }); }; }