interface ClicksWindow extends Window { clicksWsUrl?: string } const CLICKS_WSURL: string | undefined = (window).clicksWsUrl class Session { private wss: WebSocketStream private queue: string[] = [] private writer: WritableStreamDefaultWriter | undefined private url: string | undefined private header: string | undefined constructor(url: string, header: string) { this.wss = new WebSocketStream(url) this.header = header } async connect() { const openInfo = await this.wss.opened this.writer = openInfo.writable.getWriter() 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.header !== undefined) { this.pushEvent(this.header) this.header = undefined } 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) } else { void this.writer.write(event) } } } function startSession(data: string): Session | undefined { if (CLICKS_WSURL === undefined) 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, } ) } })