interface ClicksWindow extends Window { clicksWsUrl?: string } const CLICKS_WSURL: string | undefined = (window).clicksWsUrl const IDLE_TIMEOUT = 10 * 60 * 1000 class Session { private wss: WebSocketStream private queue: string[] = [] private writer: WritableStreamDefaultWriter | undefined private url: string | undefined private header: string | undefined private sentHeader = false private lastEvent = 0 private timeoutInterval: number | undefined constructor(url: string, header: string) { this.wss = new WebSocketStream(url) this.header = header } async connect() { if (this.timeoutInterval === undefined) { this.timeoutInterval = setInterval( this.checkIdle.bind(this), IDLE_TIMEOUT ) } const openInfo = await this.wss.opened this.writer = openInfo.writable.getWriter() this.lastEvent = Date.now() for (const event of this.queue) { void this.writer.write(event) } } reportEvent(event: string) { // Could do this with NavigateEvent instead, // but then we would report all navigations, // even if there are no clicks, which is unnecessary. if (window.location.href !== this.url) { if (!this.sentHeader && this.header !== undefined) { this.pushEvent(this.header) this.sentHeader = true } this.url = window.location.href this.pushEvent(`u|${this.url}`) } this.pushEvent(event) } private pushEvent(event: string) { if (this.writer === undefined) { this.queue.push(event) if (this.timeoutInterval === undefined) { void this.connect() } } else { this.lastEvent = Date.now() void this.writer.write(event) } } private checkIdle() { if (Date.now() - this.lastEvent >= IDLE_TIMEOUT) { this.closeOnIdle() } } private closeOnIdle() { if (this.timeoutInterval !== undefined) { clearInterval(this.timeoutInterval) this.timeoutInterval = undefined } this.writer = undefined const url = this.wss.url this.wss.close() this.wss = new WebSocketStream(url) this.sentHeader = false this.url = undefined } } function startSession(data: string): Session | undefined { if (CLICKS_WSURL === undefined) return undefined if (window.self !== window.top) return undefined const session = new Session(CLICKS_WSURL, data) void session.connect() return session } document.addEventListener("DOMContentLoaded", () => { const ua = navigator.userAgentData?.brands ?.map((brand) => `${brand.brand}:${brand.version}`) .join(",") const session = startSession( `${document.documentElement.clientWidth}|${document.documentElement.clientHeight}|${ua}` ) if (session !== undefined) { document.documentElement.addEventListener( "click", (e: MouseEvent) => { const target = e.target instanceof HTMLElement ? e.target.tagName : "" const x = e.pageX / document.documentElement.clientWidth // clientWidth is NOT a typo, using width as scale, // to not have to know height of rendered document when // showing. const y = e.pageY / document.documentElement.clientWidth session.reportEvent(`${x}|${y}|${target}`) }, { capture: true, } ) } })