import { Fingerprinter } from "./fingerprinter.js"; import type { FingerprintAttributes } from "./fingerprinter.js"; const NAVIGATION_API_SUPPORTED = !!window.navigation; class ActivityTracker { static get instance() { return (this.#instance ??= new this()); } static #instance: ActivityTracker | undefined; private constructor() {} #activity: ActivityEvent[] = []; #connected = false; #fingerprintAttributes?: FingerprintAttributes; isImpersonated = false; #boundNavigationHandler?: (event: any) => void; async connect(init: ActivityTrackerInit) { this.isImpersonated = init.isImpersonated; if (!this.#fingerprintAttributes) { this.#fingerprintAttributes = await Fingerprinter.getAttributes({ cookieDomain: init.cookieDomain, storageTtlDays: 365, }); } if (!this.#connected) { // add initial navigation event this.#activity.push(this.#buildNavigationEvent()); this.#configureNavigationWatchers(); } this.#connected = true; } disconnect() { this.#activity = []; this.#connected = false; if (!this.#boundNavigationHandler) { return; } if (NAVIGATION_API_SUPPORTED) { window.navigation.removeEventListener("currententrychange", this.#boundNavigationHandler); } else { window.removeEventListener("zapiPushState", this.#boundNavigationHandler); window.removeEventListener("zapiReplaceState", this.#boundNavigationHandler); } } async retrieveActivity() { return new Promise((resolve) => { const result = [...this.#activity]; this.#activity = []; resolve(result); }); } #configureNavigationWatchers() { if (NAVIGATION_API_SUPPORTED) { this.#boundNavigationHandler = this.#onNavigate.bind(this); window.navigation.addEventListener("currententrychange", this.#boundNavigationHandler); } else { window.addEventListener("zapiPushState", this.#onNavigate.bind(this)); window.addEventListener("zapiReplaceState", this.#onNavigate.bind(this)); } } #onNavigate() { this.#activity.push(this.#buildNavigationEvent()); } #buildNavigationEvent() { return { type: "Navigation", location: window.location.href, eventDateTime: new Date(), isImpersonated: this.isImpersonated, // bf = browserFingerprint bf: this.#fingerprintAttributes?.storage, // sw = screenWidth sw: this.#fingerprintAttributes?.screenWidth, // sh = screenHeight sh: this.#fingerprintAttributes?.screenHeight, // cf = canvasFingerprint cf: this.#fingerprintAttributes?.canvas, // tz = timeZone tz: this.#fingerprintAttributes?.timeZone, // dnt = doNotTrack dnt: this.#fingerprintAttributes?.doNotTrack, // gpc = globalPrivacyControl gpc: this.#fingerprintAttributes?.gpc, // prm = prefersReducedMotion prm: this.#fingerprintAttributes?.prefersReducedMotion, // pcs = prefersColorScheme pcs: this.#fingerprintAttributes?.prefersColorScheme, }; } } const activityTracker = ActivityTracker.instance; export { activityTracker as ActivityTracker }; type ActivityTrackerInit = { isImpersonated: boolean; cookieDomain?: string | null | undefined; }; type ActivityEvent = { type: string; location: string; eventDateTime: Date; isImpersonated: boolean; };