diff --git a/tracker/tracker/.prettierrc.json b/tracker/tracker/.prettierrc.json index 5e2863a11..9806a4dc3 100644 --- a/tracker/tracker/.prettierrc.json +++ b/tracker/tracker/.prettierrc.json @@ -1,5 +1,6 @@ { "printWidth": 100, "singleQuote": true, - "trailingComma": "all" + "trailingComma": "all", + "semi": false } diff --git a/tracker/tracker/src/common/interaction.ts b/tracker/tracker/src/common/interaction.ts index fdc6f90e4..19d8fa906 100644 --- a/tracker/tracker/src/common/interaction.ts +++ b/tracker/tracker/src/common/interaction.ts @@ -1,22 +1,22 @@ -import Message from './messages.gen.js'; +import Message from './messages.gen.js' export interface Options { - connAttemptCount?: number; - connAttemptGap?: number; + connAttemptCount?: number + connAttemptGap?: number } type Start = { - type: 'start'; - ingestPoint: string; - pageNo: number; - timestamp: number; - url: string; -} & Options; + type: 'start' + ingestPoint: string + pageNo: number + timestamp: number + url: string +} & Options type Auth = { - type: 'auth'; - token: string; - beaconSizeLimit?: number; -}; + type: 'auth' + token: string + beaconSizeLimit?: number +} -export type WorkerMessageData = null | 'stop' | Start | Auth | Array; +export type WorkerMessageData = null | 'stop' | Start | Auth | Array diff --git a/tracker/tracker/src/main/app/guards.ts b/tracker/tracker/src/main/app/guards.ts index 5304950de..384c9a184 100644 --- a/tracker/tracker/src/main/app/guards.ts +++ b/tracker/tracker/src/main/app/guards.ts @@ -1,34 +1,34 @@ export function isSVGElement(node: Element): node is SVGElement { - return node.namespaceURI === 'http://www.w3.org/2000/svg'; + return node.namespaceURI === 'http://www.w3.org/2000/svg' } export function isElementNode(node: Node): node is Element { - return node.nodeType === Node.ELEMENT_NODE; + return node.nodeType === Node.ELEMENT_NODE } export function isTextNode(node: Node): node is Text { - return node.nodeType === Node.TEXT_NODE; + return node.nodeType === Node.TEXT_NODE } export function isRootNode(node: Node): boolean { - return node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE; + return node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE } type TagTypeMap = { - HTML: HTMLHtmlElement; - IMG: HTMLImageElement; - INPUT: HTMLInputElement; - TEXTAREA: HTMLTextAreaElement; - SELECT: HTMLSelectElement; - LABEL: HTMLLabelElement; - IFRAME: HTMLIFrameElement; - STYLE: HTMLStyleElement; - style: SVGStyleElement; - LINK: HTMLLinkElement; -}; + HTML: HTMLHtmlElement + IMG: HTMLImageElement + INPUT: HTMLInputElement + TEXTAREA: HTMLTextAreaElement + SELECT: HTMLSelectElement + LABEL: HTMLLabelElement + IFRAME: HTMLIFrameElement + STYLE: HTMLStyleElement + style: SVGStyleElement + LINK: HTMLLinkElement +} export function hasTag( el: Node, tagName: T, ): el is TagTypeMap[typeof tagName] { - return el.nodeName === tagName; + return el.nodeName === tagName } diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index b3a62f726..145ee1d01 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -1,45 +1,45 @@ -import type Message from './messages.gen.js'; -import { Timestamp, Metadata, UserID } from './messages.gen.js'; -import { timestamp, deprecationWarn } from '../utils.js'; -import Nodes from './nodes.js'; -import Observer from './observer/top_observer.js'; -import Sanitizer from './sanitizer.js'; -import Ticker from './ticker.js'; -import Logger, { LogLevel } from './logger.js'; -import Session from './session.js'; +import type Message from './messages.gen.js' +import { Timestamp, Metadata, UserID } from './messages.gen.js' +import { timestamp, deprecationWarn } from '../utils.js' +import Nodes from './nodes.js' +import Observer from './observer/top_observer.js' +import Sanitizer from './sanitizer.js' +import Ticker from './ticker.js' +import Logger, { LogLevel } from './logger.js' +import Session from './session.js' -import { deviceMemory, jsHeapSizeLimit } from '../modules/performance.js'; +import { deviceMemory, jsHeapSizeLimit } from '../modules/performance.js' -import type { Options as ObserverOptions } from './observer/top_observer.js'; -import type { Options as SanitizerOptions } from './sanitizer.js'; -import type { Options as LoggerOptions } from './logger.js'; -import type { Options as WebworkerOptions, WorkerMessageData } from '../../common/interaction.js'; +import type { Options as ObserverOptions } from './observer/top_observer.js' +import type { Options as SanitizerOptions } from './sanitizer.js' +import type { Options as LoggerOptions } from './logger.js' +import type { Options as WebworkerOptions, WorkerMessageData } from '../../common/interaction.js' // TODO: Unify and clearly describe options logic export interface StartOptions { - userID?: string; - metadata?: Record; - forceNew?: boolean; + userID?: string + metadata?: Record + forceNew?: boolean } interface OnStartInfo { - sessionID: string; - sessionToken: string; - userUUID: string; + sessionID: string + sessionToken: string + userUUID: string } -const CANCELED = 'canceled' as const; -const START_ERROR = ':(' as const; -type SuccessfulStart = OnStartInfo & { success: true }; +const CANCELED = 'canceled' as const +const START_ERROR = ':(' as const +type SuccessfulStart = OnStartInfo & { success: true } type UnsuccessfulStart = { - reason: typeof CANCELED | string; - success: false; -}; -const UnsuccessfulStart = (reason: string): UnsuccessfulStart => ({ reason, success: false }); -const SuccessfulStart = (body: OnStartInfo): SuccessfulStart => ({ ...body, success: true }); -export type StartPromiseReturn = SuccessfulStart | UnsuccessfulStart; + reason: typeof CANCELED | string + success: false +} +const UnsuccessfulStart = (reason: string): UnsuccessfulStart => ({ reason, success: false }) +const SuccessfulStart = (body: OnStartInfo): SuccessfulStart => ({ ...body, success: true }) +export type StartPromiseReturn = SuccessfulStart | UnsuccessfulStart -type StartCallback = (i: OnStartInfo) => void; -type CommitCallback = (messages: Array) => void; +type StartCallback = (i: OnStartInfo) => void +type CommitCallback = (messages: Array) => void enum ActivityState { NotActive, Starting, @@ -47,51 +47,51 @@ enum ActivityState { } type AppOptions = { - revID: string; - node_id: string; - session_token_key: string; - session_pageno_key: string; - session_reset_key: string; - local_uuid_key: string; - ingestPoint: string; - resourceBaseHref: string | null; // resourceHref? + revID: string + node_id: string + session_token_key: string + session_pageno_key: string + session_reset_key: string + local_uuid_key: string + ingestPoint: string + resourceBaseHref: string | null // resourceHref? //resourceURLRewriter: (url: string) => string | boolean, - verbose: boolean; - __is_snippet: boolean; - __debug_report_edp: string | null; - __debug__?: LoggerOptions; - localStorage: Storage; - sessionStorage: Storage; + verbose: boolean + __is_snippet: boolean + __debug_report_edp: string | null + __debug__?: LoggerOptions + localStorage: Storage + sessionStorage: Storage // @deprecated - onStart?: StartCallback; -} & WebworkerOptions; + onStart?: StartCallback +} & WebworkerOptions -export type Options = AppOptions & ObserverOptions & SanitizerOptions; +export type Options = AppOptions & ObserverOptions & SanitizerOptions // TODO: use backendHost only -export const DEFAULT_INGEST_POINT = 'https://api.openreplay.com/ingest'; +export const DEFAULT_INGEST_POINT = 'https://api.openreplay.com/ingest' export default class App { - readonly nodes: Nodes; - readonly ticker: Ticker; - readonly projectKey: string; - readonly sanitizer: Sanitizer; - readonly debug: Logger; - readonly notify: Logger; - readonly session: Session; - readonly localStorage: Storage; - readonly sessionStorage: Storage; - private readonly messages: Array = []; - private readonly observer: Observer; - private readonly startCallbacks: Array = []; - private readonly stopCallbacks: Array<() => any> = []; - private readonly commitCallbacks: Array = []; - private readonly options: AppOptions; - private readonly revID: string; - private activityState: ActivityState = ActivityState.NotActive; - private readonly version = 'TRACKER_VERSION'; // TODO: version compatability check inside each plugin. - private readonly worker?: Worker; + readonly nodes: Nodes + readonly ticker: Ticker + readonly projectKey: string + readonly sanitizer: Sanitizer + readonly debug: Logger + readonly notify: Logger + readonly session: Session + readonly localStorage: Storage + readonly sessionStorage: Storage + private readonly messages: Array = [] + private readonly observer: Observer + private readonly startCallbacks: Array = [] + private readonly stopCallbacks: Array<() => any> = [] + private readonly commitCallbacks: Array = [] + private readonly options: AppOptions + private readonly revID: string + private activityState: ActivityState = ActivityState.NotActive + private readonly version = 'TRACKER_VERSION' // TODO: version compatability check inside each plugin. + private readonly worker?: Worker constructor( projectKey: string, sessionToken: string | null | undefined, @@ -101,7 +101,7 @@ export default class App { // deprecationWarn("'onStart' option", "tracker.start().then(/* handle session info */)") // } ?? maybe onStart is good - this.projectKey = projectKey; + this.projectKey = projectKey this.options = Object.assign( { revID: '', @@ -119,61 +119,61 @@ export default class App { sessionStorage: window.sessionStorage, }, options, - ); + ) - this.revID = this.options.revID; - this.sanitizer = new Sanitizer(this, options); - this.nodes = new Nodes(this.options.node_id); - this.observer = new Observer(this, options); - this.ticker = new Ticker(this); - this.ticker.attach(() => this.commit()); - this.debug = new Logger(this.options.__debug__); - this.notify = new Logger(this.options.verbose ? LogLevel.Warnings : LogLevel.Silent); - this.session = new Session(); + this.revID = this.options.revID + this.sanitizer = new Sanitizer(this, options) + this.nodes = new Nodes(this.options.node_id) + this.observer = new Observer(this, options) + this.ticker = new Ticker(this) + this.ticker.attach(() => this.commit()) + this.debug = new Logger(this.options.__debug__) + this.notify = new Logger(this.options.verbose ? LogLevel.Warnings : LogLevel.Silent) + this.session = new Session() this.session.attachUpdateCallback(({ userID, metadata }) => { if (userID != null) { // TODO: nullable userID - this.send(UserID(userID)); + this.send(UserID(userID)) } if (metadata != null) { - Object.entries(metadata).forEach(([key, value]) => this.send(Metadata(key, value))); + Object.entries(metadata).forEach(([key, value]) => this.send(Metadata(key, value))) } - }); - this.localStorage = this.options.localStorage; - this.sessionStorage = this.options.sessionStorage; + }) + this.localStorage = this.options.localStorage + this.sessionStorage = this.options.sessionStorage if (sessionToken != null) { - this.sessionStorage.setItem(this.options.session_token_key, sessionToken); + this.sessionStorage.setItem(this.options.session_token_key, sessionToken) } try { this.worker = new Worker( URL.createObjectURL(new Blob(['WEBWORKER_BODY'], { type: 'text/javascript' })), - ); + ) this.worker.onerror = (e) => { - this._debug('webworker_error', e); - }; + this._debug('webworker_error', e) + } this.worker.onmessage = ({ data }: MessageEvent) => { if (data === 'failed') { - this.stop(); - this._debug('worker_failed', {}); // add context (from worker) + this.stop() + this._debug('worker_failed', {}) // add context (from worker) } else if (data === 'restart') { - this.stop(); - this.start({ forceNew: true }); + this.stop() + this.start({ forceNew: true }) } - }; + } const alertWorker = () => { if (this.worker) { - this.worker.postMessage(null); + this.worker.postMessage(null) } - }; + } // keep better tactics, discard others? - this.attachEventListener(window, 'beforeunload', alertWorker, false); - this.attachEventListener(document.body, 'mouseleave', alertWorker, false, false); + this.attachEventListener(window, 'beforeunload', alertWorker, false) + this.attachEventListener(document.body, 'mouseleave', alertWorker, false, false) // TODO: stop session after inactivity timeout (make configurable) - this.attachEventListener(document, 'visibilitychange', alertWorker, false); + this.attachEventListener(document, 'visibilitychange', alertWorker, false) } catch (e) { - this._debug('worker_start', e); + this._debug('worker_start', e) } } @@ -186,56 +186,56 @@ export default class App { context, error: `${e}`, }), - }); + }) } - this.debug.error('OpenReplay error: ', context, e); + this.debug.error('OpenReplay error: ', context, e) } send(message: Message, urgent = false): void { if (this.activityState === ActivityState.NotActive) { - return; + return } - this.messages.push(message); + this.messages.push(message) // TODO: commit on start if there were `urgent` sends; // Clearify where urgent can be used for; // Clearify workflow for each type of message in case it was sent before start // (like Fetch before start; maybe add an option "preCapture: boolean" or sth alike) if (this.activityState === ActivityState.Active && urgent) { - this.commit(); + this.commit() } } private commit(): void { if (this.worker && this.messages.length) { - this.messages.unshift(Timestamp(timestamp())); - this.worker.postMessage(this.messages); - this.commitCallbacks.forEach((cb) => cb(this.messages)); - this.messages.length = 0; + this.messages.unshift(Timestamp(timestamp())) + this.worker.postMessage(this.messages) + this.commitCallbacks.forEach((cb) => cb(this.messages)) + this.messages.length = 0 } } safe void>(fn: T): T { - const app = this; + const app = this return function (this: any, ...args: any) { try { - fn.apply(this, args); + fn.apply(this, args) } catch (e) { - app._debug('safe_fn_call', e); + app._debug('safe_fn_call', e) // time: timestamp(), // name: e.name, // message: e.message, // stack: e.stack } - } as any; // TODO: correct typing + } as any // TODO: correct typing } attachCommitCallback(cb: CommitCallback): void { - this.commitCallbacks.push(cb); + this.commitCallbacks.push(cb) } attachStartCallback(cb: StartCallback): void { - this.startCallbacks.push(cb); + this.startCallbacks.push(cb) } attachStopCallback(cb: () => any): void { - this.stopCallbacks.push(cb); + this.stopCallbacks.push(cb) } attachEventListener( target: EventTarget, @@ -245,22 +245,22 @@ export default class App { useCapture = true, ): void { if (useSafe) { - listener = this.safe(listener); + listener = this.safe(listener) } - this.attachStartCallback(() => target.addEventListener(type, listener, useCapture)); - this.attachStopCallback(() => target.removeEventListener(type, listener, useCapture)); + this.attachStartCallback(() => target.addEventListener(type, listener, useCapture)) + this.attachStopCallback(() => target.removeEventListener(type, listener, useCapture)) } // TODO: full correct semantic checkRequiredVersion(version: string): boolean { - const reqVer = version.split(/[.-]/); - const ver = this.version.split(/[.-]/); + const reqVer = version.split(/[.-]/) + const ver = this.version.split(/[.-]/) for (let i = 0; i < 3; i++) { if (Number(ver[i]) < Number(reqVer[i]) || isNaN(Number(ver[i])) || isNaN(Number(reqVer[i]))) { - return false; + return false } } - return true; + return true } private getStartInfo() { @@ -271,88 +271,88 @@ export default class App { timestamp: timestamp(), // shouldn't it be set once? trackerVersion: this.version, isSnippet: this.options.__is_snippet, - }; + } } getSessionInfo() { return { ...this.session.getInfo(), ...this.getStartInfo(), - }; + } } getSessionToken(): string | undefined { - const token = this.sessionStorage.getItem(this.options.session_token_key); + const token = this.sessionStorage.getItem(this.options.session_token_key) if (token !== null) { - return token; + return token } } getSessionID(): string | undefined { - return this.session.getInfo().sessionID || undefined; + return this.session.getInfo().sessionID || undefined } getHost(): string { - return new URL(this.options.ingestPoint).hostname; + return new URL(this.options.ingestPoint).hostname } getProjectKey(): string { - return this.projectKey; + return this.projectKey } getBaseHref(): string { if (typeof this.options.resourceBaseHref === 'string') { - return this.options.resourceBaseHref; + return this.options.resourceBaseHref } else if (typeof this.options.resourceBaseHref === 'object') { //switch between types } if (document.baseURI) { - return document.baseURI; + return document.baseURI } // IE only return ( document.head?.getElementsByTagName('base')[0]?.getAttribute('href') || location.origin + location.pathname - ); + ) } resolveResourceURL(resourceURL: string): string { - const base = new URL(this.getBaseHref()); - base.pathname += '/' + new URL(resourceURL).pathname; - base.pathname.replace(/\/+/g, '/'); - return base.toString(); + const base = new URL(this.getBaseHref()) + base.pathname += '/' + new URL(resourceURL).pathname + base.pathname.replace(/\/+/g, '/') + return base.toString() } isServiceURL(url: string): boolean { - return url.startsWith(this.options.ingestPoint); + return url.startsWith(this.options.ingestPoint) } active(): boolean { - return this.activityState === ActivityState.Active; + return this.activityState === ActivityState.Active } resetNextPageSession(flag: boolean) { if (flag) { - this.sessionStorage.setItem(this.options.session_reset_key, 't'); + this.sessionStorage.setItem(this.options.session_reset_key, 't') } else { - this.sessionStorage.removeItem(this.options.session_reset_key); + this.sessionStorage.removeItem(this.options.session_reset_key) } } private _start(startOpts: StartOptions): Promise { if (!this.worker) { - return Promise.resolve(UnsuccessfulStart('No worker found: perhaps, CSP is not set.')); + return Promise.resolve(UnsuccessfulStart('No worker found: perhaps, CSP is not set.')) } if (this.activityState !== ActivityState.NotActive) { return Promise.resolve( UnsuccessfulStart( 'OpenReplay: trying to call `start()` on the instance that has been started already.', ), - ); + ) } - this.activityState = ActivityState.Starting; + this.activityState = ActivityState.Starting - let pageNo = 0; - const pageNoStr = this.sessionStorage.getItem(this.options.session_pageno_key); + let pageNo = 0 + const pageNoStr = this.sessionStorage.getItem(this.options.session_pageno_key) if (pageNoStr != null) { - pageNo = parseInt(pageNoStr); - pageNo++; + pageNo = parseInt(pageNoStr) + pageNo++ } - this.sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()); + this.sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()) - const startInfo = this.getStartInfo(); + const startInfo = this.getStartInfo() const startWorkerMsg: WorkerMessageData = { type: 'start', pageNo, @@ -361,8 +361,8 @@ export default class App { url: document.URL, connAttemptCount: this.options.connAttemptCount, connAttemptGap: this.options.connAttemptGap, - }; - this.worker.postMessage(startWorkerMsg); + } + this.worker.postMessage(startWorkerMsg) this.session.update({ // TODO: transparent "session" module logic AND explicit internal api for plugins. @@ -370,10 +370,10 @@ export default class App { // (for the case of internal .start() calls, like on "restart" webworker signal or assistent connection in tracker-assist ) metadata: startOpts.metadata || this.session.getInfo().metadata, userID: startOpts.userID, - }); + }) - const sReset = this.sessionStorage.getItem(this.options.session_reset_key); - this.sessionStorage.removeItem(this.options.session_reset_key); + const sReset = this.sessionStorage.getItem(this.options.session_reset_key) + this.sessionStorage.removeItem(this.options.session_reset_key) return window .fetch(this.options.ingestPoint + '/v1/web/start', { @@ -392,7 +392,7 @@ export default class App { }) .then((r) => { if (r.status === 200) { - return r.json(); + return r.json() } else { return r .text() @@ -400,96 +400,96 @@ export default class App { text === CANCELED ? Promise.reject(CANCELED) : Promise.reject(`Server error: ${r.status}. ${text}`), - ); + ) } }) .then((r) => { if (!this.worker) { - return Promise.reject('no worker found after start request (this might not happen)'); + return Promise.reject('no worker found after start request (this might not happen)') } - const { token, userUUID, sessionID, beaconSizeLimit } = r; + const { token, userUUID, sessionID, beaconSizeLimit } = r if ( typeof token !== 'string' || typeof userUUID !== 'string' || (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined') ) { - return Promise.reject(`Incorrect server response: ${JSON.stringify(r)}`); + return Promise.reject(`Incorrect server response: ${JSON.stringify(r)}`) } - this.sessionStorage.setItem(this.options.session_token_key, token); - this.localStorage.setItem(this.options.local_uuid_key, userUUID); - this.session.update({ sessionID }); // TODO: no no-explicit 'any' + this.sessionStorage.setItem(this.options.session_token_key, token) + this.localStorage.setItem(this.options.local_uuid_key, userUUID) + this.session.update({ sessionID }) // TODO: no no-explicit 'any' const startWorkerMsg: WorkerMessageData = { type: 'auth', token, beaconSizeLimit, - }; - this.worker.postMessage(startWorkerMsg); + } + this.worker.postMessage(startWorkerMsg) - this.activityState = ActivityState.Active; + this.activityState = ActivityState.Active - const onStartInfo = { sessionToken: token, userUUID, sessionID }; + const onStartInfo = { sessionToken: token, userUUID, sessionID } - this.startCallbacks.forEach((cb) => cb(onStartInfo)); // TODO: start as early as possible (before receiving the token) - this.observer.observe(); - this.ticker.start(); + this.startCallbacks.forEach((cb) => cb(onStartInfo)) // TODO: start as early as possible (before receiving the token) + this.observer.observe() + this.ticker.start() - this.notify.log('OpenReplay tracking started.'); + this.notify.log('OpenReplay tracking started.') // get rid of onStart ? if (typeof this.options.onStart === 'function') { - this.options.onStart(onStartInfo); + this.options.onStart(onStartInfo) } - return SuccessfulStart(onStartInfo); + return SuccessfulStart(onStartInfo) }) .catch((reason) => { - this.sessionStorage.removeItem(this.options.session_token_key); - this.stop(); + this.sessionStorage.removeItem(this.options.session_token_key) + this.stop() if (reason === CANCELED) { - return UnsuccessfulStart(CANCELED); + return UnsuccessfulStart(CANCELED) } - this.notify.log('OpenReplay was unable to start. ', reason); - this._debug('session_start', reason); - return UnsuccessfulStart(START_ERROR); - }); + this.notify.log('OpenReplay was unable to start. ', reason) + this._debug('session_start', reason) + return UnsuccessfulStart(START_ERROR) + }) } start(options: StartOptions = {}): Promise { if (!document.hidden) { - return this._start(options); + return this._start(options) } else { return new Promise((resolve) => { const onVisibilityChange = () => { if (!document.hidden) { - document.removeEventListener('visibilitychange', onVisibilityChange); - resolve(this._start(options)); + document.removeEventListener('visibilitychange', onVisibilityChange) + resolve(this._start(options)) } - }; - document.addEventListener('visibilitychange', onVisibilityChange); - }); + } + document.addEventListener('visibilitychange', onVisibilityChange) + }) } } stop(calledFromAPI = false, restarting = false): void { if (this.activityState !== ActivityState.NotActive) { try { - this.sanitizer.clear(); - this.observer.disconnect(); - this.nodes.clear(); - this.ticker.stop(); - this.stopCallbacks.forEach((cb) => cb()); + this.sanitizer.clear() + this.observer.disconnect() + this.nodes.clear() + this.ticker.stop() + this.stopCallbacks.forEach((cb) => cb()) if (calledFromAPI) { - this.session.reset(); + this.session.reset() } - this.notify.log('OpenReplay tracking stopped.'); + this.notify.log('OpenReplay tracking stopped.') if (this.worker && !restarting) { - this.worker.postMessage('stop'); + this.worker.postMessage('stop') } } finally { - this.activityState = ActivityState.NotActive; + this.activityState = ActivityState.NotActive } } } restart() { - this.stop(false, true); - this.start({ forceNew: false }); + this.stop(false, true) + this.start({ forceNew: false }) } } diff --git a/tracker/tracker/src/main/app/logger.ts b/tracker/tracker/src/main/app/logger.ts index c810cb4ce..80012ba5f 100644 --- a/tracker/tracker/src/main/app/logger.ts +++ b/tracker/tracker/src/main/app/logger.ts @@ -4,35 +4,35 @@ export const LogLevel = { Warnings: 3, Errors: 2, Silent: 0, -} as const; -type LogLevel = typeof LogLevel[keyof typeof LogLevel]; +} as const +type LogLevel = typeof LogLevel[keyof typeof LogLevel] type CustomLevel = { - error: boolean; - warn: boolean; - log: boolean; -}; + error: boolean + warn: boolean + log: boolean +} function IsCustomLevel(l: LogLevel | CustomLevel): l is CustomLevel { - return typeof l === 'object'; + return typeof l === 'object' } interface _Options { - level: LogLevel | CustomLevel; - messages?: number[]; + level: LogLevel | CustomLevel + messages?: number[] } -export type Options = true | _Options | LogLevel; +export type Options = true | _Options | LogLevel export default class Logger { - private readonly options: _Options; + private readonly options: _Options constructor(options: Options = LogLevel.Silent) { this.options = options === true ? { level: LogLevel.Verbose } : typeof options === 'number' ? { level: options } - : options; + : options } log(...args: any) { if ( @@ -40,7 +40,7 @@ export default class Logger { ? this.options.level.log : this.options.level >= LogLevel.Log ) { - console.log(...args); + console.log(...args) } } warn(...args: any) { @@ -49,7 +49,7 @@ export default class Logger { ? this.options.level.warn : this.options.level >= LogLevel.Warnings ) { - console.warn(...args); + console.warn(...args) } } error(...args: any) { @@ -58,7 +58,7 @@ export default class Logger { ? this.options.level.error : this.options.level >= LogLevel.Errors ) { - console.error(...args); + console.error(...args) } } } diff --git a/tracker/tracker/src/main/app/messages.ts b/tracker/tracker/src/main/app/messages.ts index e4ff47744..f76d434a8 100644 --- a/tracker/tracker/src/main/app/messages.ts +++ b/tracker/tracker/src/main/app/messages.ts @@ -1,7 +1,7 @@ // Auto-generated, do not edit -import * as Messages from '../../common/messages.gen.js'; -export { default } from '../../common/messages.gen.js'; +import * as Messages from '../../common/messages.gen.js' +export { default } from '../../common/messages.gen.js' export function BatchMetadata( version: number, @@ -10,15 +10,15 @@ export function BatchMetadata( timestamp: number, location: string, ): Messages.BatchMetadata { - return [Messages.Type.BatchMetadata, version, pageNo, firstIndex, timestamp, location]; + return [Messages.Type.BatchMetadata, version, pageNo, firstIndex, timestamp, location] } export function PartitionedMessage(partNo: number, partTotal: number): Messages.PartitionedMessage { - return [Messages.Type.PartitionedMessage, partNo, partTotal]; + return [Messages.Type.PartitionedMessage, partNo, partTotal] } export function Timestamp(timestamp: number): Messages.Timestamp { - return [Messages.Type.Timestamp, timestamp]; + return [Messages.Type.Timestamp, timestamp] } export function SetPageLocation( @@ -26,19 +26,19 @@ export function SetPageLocation( referrer: string, navigationStart: number, ): Messages.SetPageLocation { - return [Messages.Type.SetPageLocation, url, referrer, navigationStart]; + return [Messages.Type.SetPageLocation, url, referrer, navigationStart] } export function SetViewportSize(width: number, height: number): Messages.SetViewportSize { - return [Messages.Type.SetViewportSize, width, height]; + return [Messages.Type.SetViewportSize, width, height] } export function SetViewportScroll(x: number, y: number): Messages.SetViewportScroll { - return [Messages.Type.SetViewportScroll, x, y]; + return [Messages.Type.SetViewportScroll, x, y] } export function CreateDocument(): Messages.CreateDocument { - return [Messages.Type.CreateDocument]; + return [Messages.Type.CreateDocument] } export function CreateElementNode( @@ -48,7 +48,7 @@ export function CreateElementNode( tag: string, svg: boolean, ): Messages.CreateElementNode { - return [Messages.Type.CreateElementNode, id, parentID, index, tag, svg]; + return [Messages.Type.CreateElementNode, id, parentID, index, tag, svg] } export function CreateTextNode( @@ -56,15 +56,15 @@ export function CreateTextNode( parentID: number, index: number, ): Messages.CreateTextNode { - return [Messages.Type.CreateTextNode, id, parentID, index]; + return [Messages.Type.CreateTextNode, id, parentID, index] } export function MoveNode(id: number, parentID: number, index: number): Messages.MoveNode { - return [Messages.Type.MoveNode, id, parentID, index]; + return [Messages.Type.MoveNode, id, parentID, index] } export function RemoveNode(id: number): Messages.RemoveNode { - return [Messages.Type.RemoveNode, id]; + return [Messages.Type.RemoveNode, id] } export function SetNodeAttribute( @@ -72,39 +72,39 @@ export function SetNodeAttribute( name: string, value: string, ): Messages.SetNodeAttribute { - return [Messages.Type.SetNodeAttribute, id, name, value]; + return [Messages.Type.SetNodeAttribute, id, name, value] } export function RemoveNodeAttribute(id: number, name: string): Messages.RemoveNodeAttribute { - return [Messages.Type.RemoveNodeAttribute, id, name]; + return [Messages.Type.RemoveNodeAttribute, id, name] } export function SetNodeData(id: number, data: string): Messages.SetNodeData { - return [Messages.Type.SetNodeData, id, data]; + return [Messages.Type.SetNodeData, id, data] } export function SetNodeScroll(id: number, x: number, y: number): Messages.SetNodeScroll { - return [Messages.Type.SetNodeScroll, id, x, y]; + return [Messages.Type.SetNodeScroll, id, x, y] } export function SetInputTarget(id: number, label: string): Messages.SetInputTarget { - return [Messages.Type.SetInputTarget, id, label]; + return [Messages.Type.SetInputTarget, id, label] } export function SetInputValue(id: number, value: string, mask: number): Messages.SetInputValue { - return [Messages.Type.SetInputValue, id, value, mask]; + return [Messages.Type.SetInputValue, id, value, mask] } export function SetInputChecked(id: number, checked: boolean): Messages.SetInputChecked { - return [Messages.Type.SetInputChecked, id, checked]; + return [Messages.Type.SetInputChecked, id, checked] } export function MouseMove(x: number, y: number): Messages.MouseMove { - return [Messages.Type.MouseMove, x, y]; + return [Messages.Type.MouseMove, x, y] } export function ConsoleLog(level: string, value: string): Messages.ConsoleLog { - return [Messages.Type.ConsoleLog, level, value]; + return [Messages.Type.ConsoleLog, level, value] } export function PageLoadTiming( @@ -129,7 +129,7 @@ export function PageLoadTiming( loadEventEnd, firstPaint, firstContentfulPaint, - ]; + ] } export function PageRenderTiming( @@ -137,35 +137,35 @@ export function PageRenderTiming( visuallyComplete: number, timeToInteractive: number, ): Messages.PageRenderTiming { - return [Messages.Type.PageRenderTiming, speedIndex, visuallyComplete, timeToInteractive]; + return [Messages.Type.PageRenderTiming, speedIndex, visuallyComplete, timeToInteractive] } export function JSException(name: string, message: string, payload: string): Messages.JSException { - return [Messages.Type.JSException, name, message, payload]; + return [Messages.Type.JSException, name, message, payload] } export function RawCustomEvent(name: string, payload: string): Messages.RawCustomEvent { - return [Messages.Type.RawCustomEvent, name, payload]; + return [Messages.Type.RawCustomEvent, name, payload] } export function UserID(id: string): Messages.UserID { - return [Messages.Type.UserID, id]; + return [Messages.Type.UserID, id] } export function UserAnonymousID(id: string): Messages.UserAnonymousID { - return [Messages.Type.UserAnonymousID, id]; + return [Messages.Type.UserAnonymousID, id] } export function Metadata(key: string, value: string): Messages.Metadata { - return [Messages.Type.Metadata, key, value]; + return [Messages.Type.Metadata, key, value] } export function CSSInsertRule(id: number, rule: string, index: number): Messages.CSSInsertRule { - return [Messages.Type.CSSInsertRule, id, rule, index]; + return [Messages.Type.CSSInsertRule, id, rule, index] } export function CSSDeleteRule(id: number, index: number): Messages.CSSDeleteRule { - return [Messages.Type.CSSDeleteRule, id, index]; + return [Messages.Type.CSSDeleteRule, id, index] } export function Fetch( @@ -177,7 +177,7 @@ export function Fetch( timestamp: number, duration: number, ): Messages.Fetch { - return [Messages.Type.Fetch, method, url, request, response, status, timestamp, duration]; + return [Messages.Type.Fetch, method, url, request, response, status, timestamp, duration] } export function Profiler( @@ -186,31 +186,31 @@ export function Profiler( args: string, result: string, ): Messages.Profiler { - return [Messages.Type.Profiler, name, duration, args, result]; + return [Messages.Type.Profiler, name, duration, args, result] } export function OTable(key: string, value: string): Messages.OTable { - return [Messages.Type.OTable, key, value]; + return [Messages.Type.OTable, key, value] } export function StateAction(type: string): Messages.StateAction { - return [Messages.Type.StateAction, type]; + return [Messages.Type.StateAction, type] } export function Redux(action: string, state: string, duration: number): Messages.Redux { - return [Messages.Type.Redux, action, state, duration]; + return [Messages.Type.Redux, action, state, duration] } export function Vuex(mutation: string, state: string): Messages.Vuex { - return [Messages.Type.Vuex, mutation, state]; + return [Messages.Type.Vuex, mutation, state] } export function MobX(type: string, payload: string): Messages.MobX { - return [Messages.Type.MobX, type, payload]; + return [Messages.Type.MobX, type, payload] } export function NgRx(action: string, state: string, duration: number): Messages.NgRx { - return [Messages.Type.NgRx, action, state, duration]; + return [Messages.Type.NgRx, action, state, duration] } export function GraphQL( @@ -219,7 +219,7 @@ export function GraphQL( variables: string, response: string, ): Messages.GraphQL { - return [Messages.Type.GraphQL, operationKind, operationName, variables, response]; + return [Messages.Type.GraphQL, operationKind, operationName, variables, response] } export function PerformanceTrack( @@ -228,7 +228,7 @@ export function PerformanceTrack( totalJSHeapSize: number, usedJSHeapSize: number, ): Messages.PerformanceTrack { - return [Messages.Type.PerformanceTrack, frames, ticks, totalJSHeapSize, usedJSHeapSize]; + return [Messages.Type.PerformanceTrack, frames, ticks, totalJSHeapSize, usedJSHeapSize] } export function ResourceTiming( @@ -251,18 +251,18 @@ export function ResourceTiming( decodedBodySize, url, initiator, - ]; + ] } export function ConnectionInformation( downlink: number, type: string, ): Messages.ConnectionInformation { - return [Messages.Type.ConnectionInformation, downlink, type]; + return [Messages.Type.ConnectionInformation, downlink, type] } export function SetPageVisibility(hidden: boolean): Messages.SetPageVisibility { - return [Messages.Type.SetPageVisibility, hidden]; + return [Messages.Type.SetPageVisibility, hidden] } export function LongTask( @@ -283,7 +283,7 @@ export function LongTask( containerSrc, containerId, containerName, - ]; + ] } export function SetNodeAttributeURLBased( @@ -292,7 +292,7 @@ export function SetNodeAttributeURLBased( value: string, baseURL: string, ): Messages.SetNodeAttributeURLBased { - return [Messages.Type.SetNodeAttributeURLBased, id, name, value, baseURL]; + return [Messages.Type.SetNodeAttributeURLBased, id, name, value, baseURL] } export function SetCSSDataURLBased( @@ -300,15 +300,15 @@ export function SetCSSDataURLBased( data: string, baseURL: string, ): Messages.SetCSSDataURLBased { - return [Messages.Type.SetCSSDataURLBased, id, data, baseURL]; + return [Messages.Type.SetCSSDataURLBased, id, data, baseURL] } export function TechnicalInfo(type: string, value: string): Messages.TechnicalInfo { - return [Messages.Type.TechnicalInfo, type, value]; + return [Messages.Type.TechnicalInfo, type, value] } export function CustomIssue(name: string, payload: string): Messages.CustomIssue { - return [Messages.Type.CustomIssue, name, payload]; + return [Messages.Type.CustomIssue, name, payload] } export function CSSInsertRuleURLBased( @@ -317,7 +317,7 @@ export function CSSInsertRuleURLBased( index: number, baseURL: string, ): Messages.CSSInsertRuleURLBased { - return [Messages.Type.CSSInsertRuleURLBased, id, rule, index, baseURL]; + return [Messages.Type.CSSInsertRuleURLBased, id, rule, index, baseURL] } export function MouseClick( @@ -326,9 +326,9 @@ export function MouseClick( label: string, selector: string, ): Messages.MouseClick { - return [Messages.Type.MouseClick, id, hesitationTime, label, selector]; + return [Messages.Type.MouseClick, id, hesitationTime, label, selector] } export function CreateIFrameDocument(frameID: number, id: number): Messages.CreateIFrameDocument { - return [Messages.Type.CreateIFrameDocument, frameID, id]; + return [Messages.Type.CreateIFrameDocument, frameID, id] } diff --git a/tracker/tracker/src/main/app/nodes.ts b/tracker/tracker/src/main/app/nodes.ts index f7b167192..67bb51e05 100644 --- a/tracker/tracker/src/main/app/nodes.ts +++ b/tracker/tracker/src/main/app/nodes.ts @@ -1,53 +1,53 @@ -type NodeCallback = (node: Node, isStart: boolean) => void; -type ElementListener = [string, EventListener]; +type NodeCallback = (node: Node, isStart: boolean) => void +type ElementListener = [string, EventListener] export default class Nodes { - private nodes: Array = []; - private readonly nodeCallbacks: Array = []; - private readonly elementListeners: Map> = new Map(); + private nodes: Array = [] + private readonly nodeCallbacks: Array = [] + private readonly elementListeners: Map> = new Map() constructor(private readonly node_id: string) {} attachNodeCallback(nodeCallback: NodeCallback): void { - this.nodeCallbacks.push(nodeCallback); + this.nodeCallbacks.push(nodeCallback) } attachElementListener(type: string, node: Element, elementListener: EventListener): void { - const id = this.getID(node); + const id = this.getID(node) if (id === undefined) { - return; + return } - node.addEventListener(type, elementListener); - let listeners = this.elementListeners.get(id); + node.addEventListener(type, elementListener) + let listeners = this.elementListeners.get(id) if (listeners === undefined) { - listeners = []; - this.elementListeners.set(id, listeners); - return; + listeners = [] + this.elementListeners.set(id, listeners) + return } - listeners.push([type, elementListener]); + listeners.push([type, elementListener]) } registerNode(node: Node): [id: number, isNew: boolean] { - let id: number = (node as any)[this.node_id]; - const isNew = id === undefined; + let id: number = (node as any)[this.node_id] + const isNew = id === undefined if (isNew) { - id = this.nodes.length; - this.nodes[id] = node; - (node as any)[this.node_id] = id; + id = this.nodes.length + this.nodes[id] = node + ;(node as any)[this.node_id] = id } - return [id, isNew]; + return [id, isNew] } unregisterNode(node: Node): number | undefined { - const id = (node as any)[this.node_id]; + const id = (node as any)[this.node_id] if (id !== undefined) { - delete (node as any)[this.node_id]; - delete this.nodes[id]; - const listeners = this.elementListeners.get(id); + delete (node as any)[this.node_id] + delete this.nodes[id] + const listeners = this.elementListeners.get(id) if (listeners !== undefined) { - this.elementListeners.delete(id); - listeners.forEach((listener) => node.removeEventListener(listener[0], listener[1])); + this.elementListeners.delete(id) + listeners.forEach((listener) => node.removeEventListener(listener[0], listener[1])) } } - return id; + return id } cleanTree() { // sadly we keep empty items in array here resulting in some memory still being used @@ -55,30 +55,30 @@ export default class Nodes { // plus we keep our index positions for new/alive nodes // performance test: 3ms for 30k nodes with 17k dead ones for (let i = 0; i < this.nodes.length; i++) { - const node = this.nodes[i]; + const node = this.nodes[i] if (node && !document.contains(node)) { - this.unregisterNode(node); + this.unregisterNode(node) } } } callNodeCallbacks(node: Node, isStart: boolean): void { - this.nodeCallbacks.forEach((cb) => cb(node, isStart)); + this.nodeCallbacks.forEach((cb) => cb(node, isStart)) } getID(node: Node): number | undefined { - return (node as any)[this.node_id]; + return (node as any)[this.node_id] } getNode(id: number) { - return this.nodes[id]; + return this.nodes[id] } clear(): void { for (let id = 0; id < this.nodes.length; id++) { - const node = this.nodes[id]; + const node = this.nodes[id] if (node === undefined) { - continue; + continue } - this.unregisterNode(node); + this.unregisterNode(node) } - this.nodes.length = 0; + this.nodes.length = 0 } } diff --git a/tracker/tracker/src/main/app/observer/iframe_observer.ts b/tracker/tracker/src/main/app/observer/iframe_observer.ts index 5ac5ffc26..b4dd78016 100644 --- a/tracker/tracker/src/main/app/observer/iframe_observer.ts +++ b/tracker/tracker/src/main/app/observer/iframe_observer.ts @@ -1,20 +1,20 @@ -import Observer from './observer.js'; -import { CreateIFrameDocument } from '../messages.gen.js'; +import Observer from './observer.js' +import { CreateIFrameDocument } from '../messages.gen.js' export default class IFrameObserver extends Observer { observe(iframe: HTMLIFrameElement) { - const doc = iframe.contentDocument; - const hostID = this.app.nodes.getID(iframe); + const doc = iframe.contentDocument + const hostID = this.app.nodes.getID(iframe) if (!doc || hostID === undefined) { - return; + return } //log TODO common app.logger // Have to observe document, because the inner might be changed this.observeRoot(doc, (docID) => { if (docID === undefined) { - console.log('OpenReplay: Iframe document not bound'); - return; + console.log('OpenReplay: Iframe document not bound') + return } - this.app.send(CreateIFrameDocument(hostID, docID)); - }); + this.app.send(CreateIFrameDocument(hostID, docID)) + }) } } diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts index c888788b7..775f3cd21 100644 --- a/tracker/tracker/src/main/app/observer/observer.ts +++ b/tracker/tracker/src/main/app/observer/observer.ts @@ -8,33 +8,33 @@ import { CreateElementNode, MoveNode, RemoveNode, -} from '../messages.gen.js'; -import App from '../index.js'; -import { isRootNode, isTextNode, isElementNode, isSVGElement, hasTag } from '../guards.js'; +} from '../messages.gen.js' +import App from '../index.js' +import { isRootNode, isTextNode, isElementNode, isSVGElement, hasTag } from '../guards.js' function isIgnored(node: Node): boolean { if (isTextNode(node)) { - return false; + return false } if (!isElementNode(node)) { - return true; + return true } - const tag = node.tagName.toUpperCase(); + const tag = node.tagName.toUpperCase() if (tag === 'LINK') { - const rel = node.getAttribute('rel'); - const as = node.getAttribute('as'); - return !(rel?.includes('stylesheet') || as === 'style' || as === 'font'); + const rel = node.getAttribute('rel') + const as = node.getAttribute('as') + return !(rel?.includes('stylesheet') || as === 'style' || as === 'font') } return ( tag === 'SCRIPT' || tag === 'NOSCRIPT' || tag === 'META' || tag === 'TITLE' || tag === 'BASE' - ); + ) } function isObservable(node: Node): boolean { if (isRootNode(node)) { - return true; + return true } - return !isIgnored(node); + return !isIgnored(node) } /* @@ -57,84 +57,84 @@ enum RecentsType { } export default abstract class Observer { - private readonly observer: MutationObserver; - private readonly commited: Array = []; - private readonly recents: Map = new Map(); - private readonly indexes: Array = []; - private readonly attributesMap: Map> = new Map(); - private readonly textSet: Set = new Set(); + private readonly observer: MutationObserver + private readonly commited: Array = [] + private readonly recents: Map = new Map() + private readonly indexes: Array = [] + private readonly attributesMap: Map> = new Map() + private readonly textSet: Set = new Set() constructor(protected readonly app: App, protected readonly isTopContext = false) { this.observer = new MutationObserver( this.app.safe((mutations) => { for (const mutation of mutations) { // mutations order is sequential - const target = mutation.target; - const type = mutation.type; + const target = mutation.target + const type = mutation.type if (!isObservable(target)) { - continue; + continue } if (type === 'childList') { for (let i = 0; i < mutation.removedNodes.length; i++) { - this.bindTree(mutation.removedNodes[i], true); + this.bindTree(mutation.removedNodes[i], true) } for (let i = 0; i < mutation.addedNodes.length; i++) { - this.bindTree(mutation.addedNodes[i]); + this.bindTree(mutation.addedNodes[i]) } - continue; + continue } - const id = this.app.nodes.getID(target); + const id = this.app.nodes.getID(target) if (id === undefined) { - continue; + continue } if (!this.recents.has(id)) { - this.recents.set(id, RecentsType.Changed); // TODO only when altered + this.recents.set(id, RecentsType.Changed) // TODO only when altered } if (type === 'attributes') { - const name = mutation.attributeName; + const name = mutation.attributeName if (name === null) { - continue; + continue } - let attr = this.attributesMap.get(id); + let attr = this.attributesMap.get(id) if (attr === undefined) { - this.attributesMap.set(id, (attr = new Set())); + this.attributesMap.set(id, (attr = new Set())) } - attr.add(name); - continue; + attr.add(name) + continue } if (type === 'characterData') { - this.textSet.add(id); - continue; + this.textSet.add(id) + continue } } - this.commitNodes(); + this.commitNodes() }), - ); + ) } private clear(): void { - this.commited.length = 0; - this.recents.clear(); - this.indexes.length = 1; - this.attributesMap.clear(); - this.textSet.clear(); + this.commited.length = 0 + this.recents.clear() + this.indexes.length = 1 + this.attributesMap.clear() + this.textSet.clear() } private sendNodeAttribute(id: number, node: Element, name: string, value: string | null): void { if (isSVGElement(node)) { if (name.substr(0, 6) === 'xlink:') { - name = name.substr(6); + name = name.substr(6) } if (value === null) { - this.app.send(RemoveNodeAttribute(id, name)); + this.app.send(RemoveNodeAttribute(id, name)) } else if (name === 'href') { if (value.length > 1e5) { - value = ''; + value = '' } - this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())); + this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())) } else { - this.app.send(SetNodeAttribute(id, name, value)); + this.app.send(SetNodeAttribute(id, name, value)) } - return; + return } if ( name === 'src' || @@ -144,7 +144,7 @@ export default abstract class Observer { name === 'autocomplete' || name.substr(0, 2) === 'on' ) { - return; + return } if ( name === 'value' && @@ -153,50 +153,50 @@ export default abstract class Observer { node.type !== 'reset' && node.type !== 'submit' ) { - return; + return } if (value === null) { - this.app.send(RemoveNodeAttribute(id, name)); - return; + this.app.send(RemoveNodeAttribute(id, name)) + return } if (name === 'style' || (name === 'href' && hasTag(node, 'LINK'))) { - this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())); - return; + this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())) + return } if (name === 'href' || value.length > 1e5) { - value = ''; + value = '' } - this.app.send(SetNodeAttribute(id, name, value)); + this.app.send(SetNodeAttribute(id, name, value)) } private sendNodeData(id: number, parentElement: Element, data: string): void { if (hasTag(parentElement, 'STYLE') || hasTag(parentElement, 'style')) { - this.app.send(SetCSSDataURLBased(id, data, this.app.getBaseHref())); - return; + this.app.send(SetCSSDataURLBased(id, data, this.app.getBaseHref())) + return } - data = this.app.sanitizer.sanitize(id, data); - this.app.send(SetNodeData(id, data)); + data = this.app.sanitizer.sanitize(id, data) + this.app.send(SetNodeData(id, data)) } private bindNode(node: Node): void { - const [id, isNew] = this.app.nodes.registerNode(node); + const [id, isNew] = this.app.nodes.registerNode(node) if (isNew) { - this.recents.set(id, RecentsType.New); + this.recents.set(id, RecentsType.New) } else if (this.recents.get(id) !== RecentsType.New) { // can we do just `else` here? - this.recents.set(id, RecentsType.Removed); + this.recents.set(id, RecentsType.Removed) } } private unbindChildNode(node: Node): void { - const [id] = this.app.nodes.registerNode(node); - this.recents.set(id, RecentsType.RemovedChild); + const [id] = this.app.nodes.registerNode(node) + this.recents.set(id, RecentsType.RemovedChild) } private bindTree(node: Node, isChildUnbinding = false): void { if (!isObservable(node)) { - return; + return } - this.bindNode(node); + this.bindNode(node) const walker = document.createTreeWalker( node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, @@ -208,30 +208,30 @@ export default abstract class Observer { }, // @ts-ignore false, - ); + ) while (walker.nextNode()) { if (isChildUnbinding) { - this.unbindChildNode(walker.currentNode); + this.unbindChildNode(walker.currentNode) } else { - this.bindNode(walker.currentNode); + this.bindNode(walker.currentNode) } } } private unbindNode(node: Node) { - const id = this.app.nodes.unregisterNode(node); + const id = this.app.nodes.unregisterNode(node) if (id !== undefined && this.recents.get(id) === RecentsType.Removed) { - this.app.send(RemoveNode(id)); + this.app.send(RemoveNode(id)) } } // A top-consumption function on the infinite lists test. (~1% of performance resources) private _commitNode(id: number, node: Node): boolean { if (isRootNode(node)) { - return true; + return true } - const parent = node.parentNode; - let parentID: number | undefined; + const parent = node.parentNode + let parentID: number | undefined // Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before) // TODO: get rid of "special" cases (there is an issue with CreateDocument altered behaviour though) @@ -240,109 +240,109 @@ export default abstract class Observer { if (parent === null) { // Sometimes one observation contains attribute mutations for the removimg node, which gets ignored here. // That shouldn't affect the visual rendering ( should it? ) - this.unbindNode(node); - return false; + this.unbindNode(node) + return false } - parentID = this.app.nodes.getID(parent); + parentID = this.app.nodes.getID(parent) if (parentID === undefined) { - this.unbindNode(node); - return false; + this.unbindNode(node) + return false } if (!this.commitNode(parentID)) { - this.unbindNode(node); - return false; + this.unbindNode(node) + return false } - this.app.sanitizer.handleNode(id, parentID, node); + this.app.sanitizer.handleNode(id, parentID, node) if (this.app.sanitizer.isMaskedContainer(parentID)) { - return false; + return false } } // From here parentID === undefined if node is top context HTML node - let sibling = node.previousSibling; + let sibling = node.previousSibling while (sibling !== null) { - const siblingID = this.app.nodes.getID(sibling); + const siblingID = this.app.nodes.getID(sibling) if (siblingID !== undefined) { - this.commitNode(siblingID); - this.indexes[id] = this.indexes[siblingID] + 1; - break; + this.commitNode(siblingID) + this.indexes[id] = this.indexes[siblingID] + 1 + break } - sibling = sibling.previousSibling; + sibling = sibling.previousSibling } if (sibling === null) { - this.indexes[id] = 0; + this.indexes[id] = 0 } - const recentsType = this.recents.get(id); - const isNew = recentsType === RecentsType.New; - const index = this.indexes[id]; + const recentsType = this.recents.get(id) + const isNew = recentsType === RecentsType.New + const index = this.indexes[id] if (index === undefined) { - throw 'commitNode: missing node index'; + throw 'commitNode: missing node index' } if (isNew) { if (isElementNode(node)) { - let el: Element = node; + let el: Element = node if (parentID !== undefined) { if (this.app.sanitizer.isMaskedContainer(id)) { - const width = el.clientWidth; - const height = el.clientHeight; - el = node.cloneNode() as Element; - (el as HTMLElement | SVGElement).style.width = width + 'px'; - (el as HTMLElement | SVGElement).style.height = height + 'px'; + const width = el.clientWidth + const height = el.clientHeight + el = node.cloneNode() as Element + ;(el as HTMLElement | SVGElement).style.width = width + 'px' + ;(el as HTMLElement | SVGElement).style.height = height + 'px' } - this.app.send(CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node))); + this.app.send(CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node))) } for (let i = 0; i < el.attributes.length; i++) { - const attr = el.attributes[i]; - this.sendNodeAttribute(id, el, attr.nodeName, attr.value); + const attr = el.attributes[i] + this.sendNodeAttribute(id, el, attr.nodeName, attr.value) } } else if (isTextNode(node)) { // for text node id != 0, hence parentID !== undefined and parent is Element - this.app.send(CreateTextNode(id, parentID as number, index)); - this.sendNodeData(id, parent as Element, node.data); + this.app.send(CreateTextNode(id, parentID as number, index)) + this.sendNodeData(id, parent as Element, node.data) } - return true; + return true } if (recentsType === RecentsType.Removed && parentID !== undefined) { - this.app.send(MoveNode(id, parentID, index)); + this.app.send(MoveNode(id, parentID, index)) } - const attr = this.attributesMap.get(id); + const attr = this.attributesMap.get(id) if (attr !== undefined) { if (!isElementNode(node)) { - throw 'commitNode: node is not an element'; + throw 'commitNode: node is not an element' } for (const name of attr) { - this.sendNodeAttribute(id, node, name, node.getAttribute(name)); + this.sendNodeAttribute(id, node, name, node.getAttribute(name)) } } if (this.textSet.has(id)) { if (!isTextNode(node)) { - throw 'commitNode: node is not a text'; + throw 'commitNode: node is not a text' } // for text node id != 0, hence parent is Element - this.sendNodeData(id, parent as Element, node.data); + this.sendNodeData(id, parent as Element, node.data) } - return true; + return true } private commitNode(id: number): boolean { - const node = this.app.nodes.getNode(id); + const node = this.app.nodes.getNode(id) if (node === undefined) { - return false; + return false } - const cmt = this.commited[id]; + const cmt = this.commited[id] if (cmt !== undefined) { - return cmt; + return cmt } - return (this.commited[id] = this._commitNode(id, node)); + return (this.commited[id] = this._commitNode(id, node)) } private commitNodes(isStart = false): void { - let node; + let node this.recents.forEach((type, id) => { - this.commitNode(id); + this.commitNode(id) if (type === RecentsType.New && (node = this.app.nodes.getNode(id))) { - this.app.nodes.callNodeCallbacks(node, isStart); + this.app.nodes.callNodeCallbacks(node, isStart) } - }); - this.clear(); + }) + this.clear() } // ISSSUE @@ -358,14 +358,14 @@ export default abstract class Observer { subtree: true, attributeOldValue: false, characterDataOldValue: false, - }); - this.bindTree(nodeToBind); - beforeCommit(this.app.nodes.getID(node)); - this.commitNodes(true); + }) + this.bindTree(nodeToBind) + beforeCommit(this.app.nodes.getID(node)) + this.commitNodes(true) } disconnect(): void { - this.observer.disconnect(); - this.clear(); + this.observer.disconnect() + this.clear() } } diff --git a/tracker/tracker/src/main/app/observer/shadow_root_observer.ts b/tracker/tracker/src/main/app/observer/shadow_root_observer.ts index 149121311..36dcb3215 100644 --- a/tracker/tracker/src/main/app/observer/shadow_root_observer.ts +++ b/tracker/tracker/src/main/app/observer/shadow_root_observer.ts @@ -1,19 +1,19 @@ -import Observer from './observer.js'; -import { CreateIFrameDocument } from '../messages.gen.js'; +import Observer from './observer.js' +import { CreateIFrameDocument } from '../messages.gen.js' export default class ShadowRootObserver extends Observer { observe(el: Element) { - const shRoot = el.shadowRoot; - const hostID = this.app.nodes.getID(el); + const shRoot = el.shadowRoot + const hostID = this.app.nodes.getID(el) if (!shRoot || hostID === undefined) { - return; + return } // log this.observeRoot(shRoot, (rootID) => { if (rootID === undefined) { - console.log('OpenReplay: Shadow Root was not bound'); - return; + console.log('OpenReplay: Shadow Root was not bound') + return } - this.app.send(CreateIFrameDocument(hostID, rootID)); - }); + this.app.send(CreateIFrameDocument(hostID, rootID)) + }) } } diff --git a/tracker/tracker/src/main/app/observer/top_observer.ts b/tracker/tracker/src/main/app/observer/top_observer.ts index fe50ff4a7..a4c9343c6 100644 --- a/tracker/tracker/src/main/app/observer/top_observer.ts +++ b/tracker/tracker/src/main/app/observer/top_observer.ts @@ -1,29 +1,29 @@ -import Observer from './observer.js'; -import { isElementNode, hasTag } from '../guards.js'; +import Observer from './observer.js' +import { isElementNode, hasTag } from '../guards.js' -import IFrameObserver from './iframe_observer.js'; -import ShadowRootObserver from './shadow_root_observer.js'; +import IFrameObserver from './iframe_observer.js' +import ShadowRootObserver from './shadow_root_observer.js' -import { CreateDocument } from '../messages.gen.js'; -import App from '../index.js'; -import { IN_BROWSER, hasOpenreplayAttribute } from '../../utils.js'; +import { CreateDocument } from '../messages.gen.js' +import App from '../index.js' +import { IN_BROWSER, hasOpenreplayAttribute } from '../../utils.js' export interface Options { - captureIFrames: boolean; + captureIFrames: boolean } -const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot(); +const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot() export default class TopObserver extends Observer { - private readonly options: Options; + private readonly options: Options constructor(app: App, options: Partial) { - super(app, true); + super(app, true) this.options = Object.assign( { captureIFrames: true, }, options, - ); + ) // IFrames this.app.nodes.attachNodeCallback((node) => { @@ -32,59 +32,59 @@ export default class TopObserver extends Observer { ((this.options.captureIFrames && !hasOpenreplayAttribute(node, 'obscured')) || hasOpenreplayAttribute(node, 'capture')) ) { - this.handleIframe(node); + this.handleIframe(node) } - }); + }) // ShadowDOM this.app.nodes.attachNodeCallback((node) => { if (isElementNode(node) && node.shadowRoot !== null) { - this.handleShadowRoot(node.shadowRoot); + this.handleShadowRoot(node.shadowRoot) } - }); + }) } - private iframeObservers: IFrameObserver[] = []; + private iframeObservers: IFrameObserver[] = [] private handleIframe(iframe: HTMLIFrameElement): void { - let doc: Document | null = null; + let doc: Document | null = null const handle = this.app.safe(() => { - const id = this.app.nodes.getID(iframe); + const id = this.app.nodes.getID(iframe) if (id === undefined) { - return; + return } //log if (iframe.contentDocument === doc) { - return; + return } // How frequently can it happen? - doc = iframe.contentDocument; + doc = iframe.contentDocument if (!doc || !iframe.contentWindow) { - return; + return } - const observer = new IFrameObserver(this.app); + const observer = new IFrameObserver(this.app) - this.iframeObservers.push(observer); - observer.observe(iframe); - }); - iframe.addEventListener('load', handle); // why app.attachEventListener not working? - handle(); + this.iframeObservers.push(observer) + observer.observe(iframe) + }) + iframe.addEventListener('load', handle) // why app.attachEventListener not working? + handle() } - private shadowRootObservers: ShadowRootObserver[] = []; + private shadowRootObservers: ShadowRootObserver[] = [] private handleShadowRoot(shRoot: ShadowRoot) { - const observer = new ShadowRootObserver(this.app); - this.shadowRootObservers.push(observer); - observer.observe(shRoot.host); + const observer = new ShadowRootObserver(this.app) + this.shadowRootObservers.push(observer) + observer.observe(shRoot.host) } observe(): void { // Protection from several subsequent calls? - const observer = this; + const observer = this Element.prototype.attachShadow = function () { // eslint-disable-next-line - const shadow = attachShadowNativeFn.apply(this, arguments); - observer.handleShadowRoot(shadow); - return shadow; - }; + const shadow = attachShadowNativeFn.apply(this, arguments) + observer.handleShadowRoot(shadow) + return shadow + } // Can observe documentElement () here, because it is not supposed to be changing. // However, it is possible in some exotic cases and may cause an ignorance of the newly created @@ -95,18 +95,18 @@ export default class TopObserver extends Observer { this.observeRoot( window.document, () => { - this.app.send(CreateDocument()); + this.app.send(CreateDocument()) }, window.document.documentElement, - ); + ) } disconnect() { - Element.prototype.attachShadow = attachShadowNativeFn; - this.iframeObservers.forEach((o) => o.disconnect()); - this.iframeObservers = []; - this.shadowRootObservers.forEach((o) => o.disconnect()); - this.shadowRootObservers = []; - super.disconnect(); + Element.prototype.attachShadow = attachShadowNativeFn + this.iframeObservers.forEach((o) => o.disconnect()) + this.iframeObservers = [] + this.shadowRootObservers.forEach((o) => o.disconnect()) + this.shadowRootObservers = [] + super.disconnect() } } diff --git a/tracker/tracker/src/main/app/sanitizer.ts b/tracker/tracker/src/main/app/sanitizer.ts index 358913f93..f3e91d32d 100644 --- a/tracker/tracker/src/main/app/sanitizer.ts +++ b/tracker/tracker/src/main/app/sanitizer.ts @@ -1,16 +1,16 @@ -import type App from './index.js'; -import { stars, hasOpenreplayAttribute } from '../utils.js'; -import { isElementNode } from './guards.js'; +import type App from './index.js' +import { stars, hasOpenreplayAttribute } from '../utils.js' +import { isElementNode } from './guards.js' export interface Options { - obscureTextEmails: boolean; - obscureTextNumbers: boolean; + obscureTextEmails: boolean + obscureTextNumbers: boolean } export default class Sanitizer { - private readonly masked: Set = new Set(); - private readonly maskedContainers: Set = new Set(); - private readonly options: Options; + private readonly masked: Set = new Set() + private readonly maskedContainers: Set = new Set() + private readonly options: Options constructor(private readonly app: App, options: Partial) { this.options = Object.assign( @@ -19,7 +19,7 @@ export default class Sanitizer { obscureTextNumbers: false, }, options, - ); + ) } handleNode(id: number, parentID: number, node: Node) { @@ -27,13 +27,13 @@ export default class Sanitizer { this.masked.has(parentID) || (isElementNode(node) && hasOpenreplayAttribute(node, 'masked')) ) { - this.masked.add(id); + this.masked.add(id) } if ( this.maskedContainers.has(parentID) || (isElementNode(node) && hasOpenreplayAttribute(node, 'htmlmasked')) ) { - this.maskedContainers.add(id); + this.maskedContainers.add(id) } } @@ -42,40 +42,37 @@ export default class Sanitizer { // TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases? return data .trim() - .replace( - /[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, - '█', - ); + .replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█') } if (this.options.obscureTextNumbers) { - data = data.replace(/\d/g, '0'); + data = data.replace(/\d/g, '0') } if (this.options.obscureTextEmails) { data = data.replace( /([^\s]+)@([^\s]+)\.([^\s]+)/g, (...f: Array) => stars(f[1]) + '@' + stars(f[2]) + '.' + stars(f[3]), - ); + ) } - return data; + return data } isMasked(id: number): boolean { - return this.masked.has(id); + return this.masked.has(id) } isMaskedContainer(id: number) { - return this.maskedContainers.has(id); + return this.maskedContainers.has(id) } getInnerTextSecure(el: HTMLElement): string { - const id = this.app.nodes.getID(el); + const id = this.app.nodes.getID(el) if (!id) { - return ''; + return '' } - return this.sanitize(id, el.innerText); + return this.sanitize(id, el.innerText) } clear(): void { - this.masked.clear(); - this.maskedContainers.clear(); + this.masked.clear() + this.maskedContainers.clear() } } diff --git a/tracker/tracker/src/main/app/session.ts b/tracker/tracker/src/main/app/session.ts index 357a2aca7..ac46cac5c 100644 --- a/tracker/tracker/src/main/app/session.ts +++ b/tracker/tracker/src/main/app/session.ts @@ -1,50 +1,50 @@ interface SessionInfo { - sessionID: string | null; - metadata: Record; - userID: string | null; + sessionID: string | null + metadata: Record + userID: string | null } -type OnUpdateCallback = (i: Partial) => void; +type OnUpdateCallback = (i: Partial) => void export default class Session { - private metadata: Record = {}; - private userID: string | null = null; - private sessionID: string | null = null; - private readonly callbacks: OnUpdateCallback[] = []; + private metadata: Record = {} + private userID: string | null = null + private sessionID: string | null = null + private readonly callbacks: OnUpdateCallback[] = [] attachUpdateCallback(cb: OnUpdateCallback) { - this.callbacks.push(cb); + this.callbacks.push(cb) } private handleUpdate(newInfo: Partial) { if (newInfo.userID == null) { - delete newInfo.userID; + delete newInfo.userID } if (newInfo.sessionID == null) { - delete newInfo.sessionID; + delete newInfo.sessionID } - this.callbacks.forEach((cb) => cb(newInfo)); + this.callbacks.forEach((cb) => cb(newInfo)) } update(newInfo: Partial) { if (newInfo.userID !== undefined) { // TODO clear nullable/undefinable types - this.userID = newInfo.userID; + this.userID = newInfo.userID } if (newInfo.metadata !== undefined) { - Object.entries(newInfo.metadata).forEach(([k, v]) => (this.metadata[k] = v)); + Object.entries(newInfo.metadata).forEach(([k, v]) => (this.metadata[k] = v)) } if (newInfo.sessionID !== undefined) { - this.sessionID = newInfo.sessionID; + this.sessionID = newInfo.sessionID } - this.handleUpdate(newInfo); + this.handleUpdate(newInfo) } setMetadata(key: string, value: string) { - this.metadata[key] = value; - this.handleUpdate({ metadata: { [key]: value } }); + this.metadata[key] = value + this.handleUpdate({ metadata: { [key]: value } }) } setUserID(userID: string) { - this.userID = userID; - this.handleUpdate({ userID }); + this.userID = userID + this.handleUpdate({ userID }) } getInfo(): SessionInfo { @@ -52,12 +52,12 @@ export default class Session { sessionID: this.sessionID, metadata: this.metadata, userID: this.userID, - }; + } } reset(): void { - this.metadata = {}; - this.userID = null; - this.sessionID = null; + this.metadata = {} + this.userID = null + this.sessionID = null } } diff --git a/tracker/tracker/src/main/app/ticker.ts b/tracker/tracker/src/main/app/ticker.ts index 81bf7b6a8..70bffdf4c 100644 --- a/tracker/tracker/src/main/app/ticker.ts +++ b/tracker/tracker/src/main/app/ticker.ts @@ -1,31 +1,31 @@ -import App from './index.js'; +import App from './index.js' -type Callback = () => void; +type Callback = () => void function wrap(callback: Callback, n: number): Callback { - let t = 0; + let t = 0 return (): void => { if (t++ >= n) { - t = 0; - callback(); + t = 0 + callback() } - }; + } } export default class Ticker { - private timer: ReturnType | null = null; - private readonly callbacks: Array; + private timer: ReturnType | null = null + private readonly callbacks: Array constructor(private readonly app: App) { - this.callbacks = []; + this.callbacks = [] } attach(callback: Callback, n = 0, useSafe = true, thisArg?: any) { if (thisArg) { - callback = callback.bind(thisArg); + callback = callback.bind(thisArg) } if (useSafe) { - callback = this.app.safe(callback); + callback = this.app.safe(callback) } - this.callbacks.unshift(n ? wrap(callback, n) : callback) - 1; + this.callbacks.unshift(n ? wrap(callback, n) : callback) - 1 } start(): void { @@ -33,17 +33,17 @@ export default class Ticker { this.timer = setInterval( () => this.callbacks.forEach((cb) => { - if (cb) cb(); + if (cb) cb() }), 30, - ); + ) } } stop(): void { if (this.timer !== null) { - clearInterval(this.timer); - this.timer = null; + clearInterval(this.timer) + this.timer = null } } } diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index 09c1dc6f0..fbe53baef 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -1,56 +1,56 @@ -import App, { DEFAULT_INGEST_POINT } from './app/index.js'; -export { default as App } from './app/index.js'; +import App, { DEFAULT_INGEST_POINT } from './app/index.js' +export { default as App } from './app/index.js' -import { UserID, UserAnonymousID, RawCustomEvent, CustomIssue } from './app/messages.gen.js'; -import * as _Messages from './app/messages.gen.js'; -export const Messages = _Messages; +import { UserID, UserAnonymousID, RawCustomEvent, CustomIssue } from './app/messages.gen.js' +import * as _Messages from './app/messages.gen.js' +export const Messages = _Messages -import Connection from './modules/connection.js'; -import Console from './modules/console.js'; +import Connection from './modules/connection.js' +import Console from './modules/console.js' import Exception, { getExceptionMessageFromEvent, getExceptionMessage, -} from './modules/exception.js'; -import Img from './modules/img.js'; -import Input from './modules/input.js'; -import Mouse from './modules/mouse.js'; -import Timing from './modules/timing.js'; -import Performance from './modules/performance.js'; -import Scroll from './modules/scroll.js'; -import Viewport from './modules/viewport.js'; -import CSSRules from './modules/cssrules.js'; -import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js'; +} from './modules/exception.js' +import Img from './modules/img.js' +import Input from './modules/input.js' +import Mouse from './modules/mouse.js' +import Timing from './modules/timing.js' +import Performance from './modules/performance.js' +import Scroll from './modules/scroll.js' +import Viewport from './modules/viewport.js' +import CSSRules from './modules/cssrules.js' +import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js' -import type { Options as AppOptions } from './app/index.js'; -import type { Options as ConsoleOptions } from './modules/console.js'; -import type { Options as ExceptionOptions } from './modules/exception.js'; -import type { Options as InputOptions } from './modules/input.js'; -import type { Options as PerformanceOptions } from './modules/performance.js'; -import type { Options as TimingOptions } from './modules/timing.js'; -import type { StartOptions } from './app/index.js'; +import type { Options as AppOptions } from './app/index.js' +import type { Options as ConsoleOptions } from './modules/console.js' +import type { Options as ExceptionOptions } from './modules/exception.js' +import type { Options as InputOptions } from './modules/input.js' +import type { Options as PerformanceOptions } from './modules/performance.js' +import type { Options as TimingOptions } from './modules/timing.js' +import type { StartOptions } from './app/index.js' //TODO: unique options init -import type { StartPromiseReturn } from './app/index.js'; +import type { StartPromiseReturn } from './app/index.js' export type Options = Partial< AppOptions & ConsoleOptions & ExceptionOptions & InputOptions & PerformanceOptions & TimingOptions > & { - projectID?: number; // For the back compatibility only (deprecated) - projectKey: string; - sessionToken?: string; - respectDoNotTrack?: boolean; - autoResetOnWindowOpen?: boolean; + projectID?: number // For the back compatibility only (deprecated) + projectKey: string + sessionToken?: string + respectDoNotTrack?: boolean + autoResetOnWindowOpen?: boolean // dev only - __DISABLE_SECURE_MODE?: boolean; -}; + __DISABLE_SECURE_MODE?: boolean +} -const DOCS_SETUP = '/installation/setup-or'; +const DOCS_SETUP = '/installation/setup-or' function processOptions(obj: any): obj is Options { if (obj == null) { console.error( `OpenReplay: invalid options argument type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`, - ); - return false; + ) + return false } if (typeof obj.projectKey !== 'string') { if (typeof obj.projectKey !== 'number') { @@ -58,46 +58,46 @@ function processOptions(obj: any): obj is Options { // Back compatability console.error( `OpenReplay: projectKey is missing or wrong type (string is expected). Please, check ${DOCS_HOST}${DOCS_SETUP} for more information.`, - ); - return false; + ) + return false } else { - obj.projectKey = obj.projectID.toString(); - deprecationWarn('`projectID` option', '`projectKey` option', DOCS_SETUP); + obj.projectKey = obj.projectID.toString() + deprecationWarn('`projectID` option', '`projectKey` option', DOCS_SETUP) } } else { - console.warn('OpenReplay: projectKey is expected to have a string type.'); - obj.projectKey = obj.projectKey.toString(); + console.warn('OpenReplay: projectKey is expected to have a string type.') + obj.projectKey = obj.projectKey.toString() } } if (typeof obj.sessionToken !== 'string' && obj.sessionToken != null) { console.warn( `OpenReplay: invalid options argument type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`, - ); + ) } - return true; + return true } export default class API { - private readonly app: App | null = null; + private readonly app: App | null = null constructor(private readonly options: Options) { if (!IN_BROWSER || !processOptions(options)) { - return; + return } if ((window as any).__OPENREPLAY__) { - console.error('OpenReplay: one tracker instance has been initialised already'); - return; + console.error('OpenReplay: one tracker instance has been initialised already') + return } if (!options.__DISABLE_SECURE_MODE && location.protocol !== 'https:') { console.error( 'OpenReplay: Your website must be publicly accessible and running on SSL in order for OpenReplay to properly capture and replay the user session. You can disable this check by setting `__DISABLE_SECURE_MODE` option to `true` if you are testing in localhost. Keep in mind, that asset files on a local machine are not available to the outside world. This might affect tracking if you use css files.', - ); - return; + ) + return } const doNotTrack = options.respectDoNotTrack && (navigator.doNotTrack == '1' || // @ts-ignore - window.doNotTrack == '1'); + window.doNotTrack == '1') const app = (this.app = doNotTrack || !('Map' in window) || @@ -109,42 +109,42 @@ export default class API { !('Blob' in window) || !('Worker' in window) ? null - : new App(options.projectKey, options.sessionToken, options)); + : new App(options.projectKey, options.sessionToken, options)) if (app !== null) { - Viewport(app); - CSSRules(app); - Connection(app); - Console(app, options); - Exception(app, options); - Img(app); - Input(app, options); - Mouse(app); - Timing(app, options); - Performance(app, options); - Scroll(app); - (window as any).__OPENREPLAY__ = this; + Viewport(app) + CSSRules(app) + Connection(app) + Console(app, options) + Exception(app, options) + Img(app) + Input(app, options) + Mouse(app) + Timing(app, options) + Performance(app, options) + Scroll(app) + ;(window as any).__OPENREPLAY__ = this if (options.autoResetOnWindowOpen) { - const wOpen = window.open; + const wOpen = window.open app.attachStartCallback(() => { // @ts-ignore ? window.open = function (...args) { - app.resetNextPageSession(true); - wOpen.call(window, ...args); - app.resetNextPageSession(false); - }; - }); + app.resetNextPageSession(true) + wOpen.call(window, ...args) + app.resetNextPageSession(false) + } + }) app.attachStopCallback(() => { - window.open = wOpen; - }); + window.open = wOpen + }) } } else { console.log( "OpenReplay: browser doesn't support API required for tracking or doNotTrack is set to 1.", - ); - const req = new XMLHttpRequest(); - const orig = options.ingestPoint || DEFAULT_INGEST_POINT; - req.open('POST', orig + '/v1/web/not-started'); + ) + const req = new XMLHttpRequest() + const orig = options.ingestPoint || DEFAULT_INGEST_POINT + req.open('POST', orig + '/v1/web/not-started') // no-cors issue only with text/plain or not-set Content-Type // req.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); req.send( @@ -154,99 +154,99 @@ export default class API { doNotTrack, // TODO: add precise reason (an exact API missing) }), - ); + ) } } use(fn: (app: App | null, options?: Options) => T): T { - return fn(this.app, this.options); + return fn(this.app, this.options) } isActive(): boolean { if (this.app === null) { - return false; + return false } - return this.app.active(); + return this.app.active() } start(startOpts?: Partial): Promise { if (!IN_BROWSER) { console.error( `OpenReplay: you are trying to start Tracker on a node.js environment. If you want to use OpenReplay with SSR, please, use componentDidMount or useEffect API for placing the \`tracker.start()\` line. Check documentation on ${DOCS_HOST}${DOCS_SETUP}`, - ); - return Promise.reject('Trying to start not in browser.'); + ) + return Promise.reject('Trying to start not in browser.') } if (this.app === null) { - return Promise.reject("Browser doesn't support required api, or doNotTrack is active."); + return Promise.reject("Browser doesn't support required api, or doNotTrack is active.") } // TODO: check argument type - return this.app.start(startOpts); + return this.app.start(startOpts) } stop(): void { if (this.app === null) { - return; + return } - this.app.stop(true); + this.app.stop(true) } getSessionToken(): string | null | undefined { if (this.app === null) { - return null; + return null } - return this.app.getSessionToken(); + return this.app.getSessionToken() } getSessionID(): string | null | undefined { if (this.app === null) { - return null; + return null } - return this.app.getSessionID(); + return this.app.getSessionID() } sessionID(): string | null | undefined { - deprecationWarn("'sessionID' method", "'getSessionID' method", '/'); - return this.getSessionID(); + deprecationWarn("'sessionID' method", "'getSessionID' method", '/') + return this.getSessionID() } setUserID(id: string): void { if (typeof id === 'string' && this.app !== null) { - this.app.session.setUserID(id); + this.app.session.setUserID(id) } } userID(id: string): void { - deprecationWarn("'userID' method", "'setUserID' method", '/'); - this.setUserID(id); + deprecationWarn("'userID' method", "'setUserID' method", '/') + this.setUserID(id) } setUserAnonymousID(id: string): void { if (typeof id === 'string' && this.app !== null) { - this.app.send(UserAnonymousID(id)); + this.app.send(UserAnonymousID(id)) } } userAnonymousID(id: string): void { - deprecationWarn("'userAnonymousID' method", "'setUserAnonymousID' method", '/'); - this.setUserAnonymousID(id); + deprecationWarn("'userAnonymousID' method", "'setUserAnonymousID' method", '/') + this.setUserAnonymousID(id) } setMetadata(key: string, value: string): void { if (typeof key === 'string' && typeof value === 'string' && this.app !== null) { - this.app.session.setMetadata(key, value); + this.app.session.setMetadata(key, value) } } metadata(key: string, value: string): void { - deprecationWarn("'metadata' method", "'setMetadata' method", '/'); - this.setMetadata(key, value); + deprecationWarn("'metadata' method", "'setMetadata' method", '/') + this.setMetadata(key, value) } event(key: string, payload: any, issue = false): void { if (typeof key === 'string' && this.app !== null) { if (issue) { - return this.issue(key, payload); + return this.issue(key, payload) } else { try { - payload = JSON.stringify(payload); + payload = JSON.stringify(payload) } catch (e) { - return; + return } - this.app.send(RawCustomEvent(key, payload)); + this.app.send(RawCustomEvent(key, payload)) } } } @@ -254,28 +254,28 @@ export default class API { issue(key: string, payload: any): void { if (typeof key === 'string' && this.app !== null) { try { - payload = JSON.stringify(payload); + payload = JSON.stringify(payload) } catch (e) { - return; + return } - this.app.send(CustomIssue(key, payload)); + this.app.send(CustomIssue(key, payload)) } } handleError = (e: Error | ErrorEvent | PromiseRejectionEvent) => { if (this.app === null) { - return; + return } if (e instanceof Error) { - this.app.send(getExceptionMessage(e, [])); + this.app.send(getExceptionMessage(e, [])) } else if ( e instanceof ErrorEvent || ('PromiseRejectionEvent' in window && e instanceof PromiseRejectionEvent) ) { - const msg = getExceptionMessageFromEvent(e); + const msg = getExceptionMessageFromEvent(e) if (msg != null) { - this.app.send(msg); + this.app.send(msg) } } - }; + } } diff --git a/tracker/tracker/src/main/modules/connection.ts b/tracker/tracker/src/main/modules/connection.ts index 652d38251..d1ff577ca 100644 --- a/tracker/tracker/src/main/modules/connection.ts +++ b/tracker/tracker/src/main/modules/connection.ts @@ -1,25 +1,25 @@ -import App from '../app/index.js'; -import { ConnectionInformation } from '../app/messages.gen.js'; +import App from '../app/index.js' +import { ConnectionInformation } from '../app/messages.gen.js' export default function (app: App): void { const connection: | { - downlink: number; - type?: string; - addEventListener: (type: 'change', cb: () => void) => void; + downlink: number + type?: string + addEventListener: (type: 'change', cb: () => void) => void } | undefined = (navigator as any).connection || (navigator as any).mozConnection || - (navigator as any).webkitConnection; + (navigator as any).webkitConnection if (connection === undefined) { - return; + return } const sendConnectionInformation = (): void => app.send( ConnectionInformation(Math.round(connection.downlink * 1000), connection.type || 'unknown'), - ); - sendConnectionInformation(); - connection.addEventListener('change', sendConnectionInformation); + ) + sendConnectionInformation() + connection.addEventListener('change', sendConnectionInformation) } diff --git a/tracker/tracker/src/main/modules/console.ts b/tracker/tracker/src/main/modules/console.ts index ede1ab99c..941dd761c 100644 --- a/tracker/tracker/src/main/modules/console.ts +++ b/tracker/tracker/src/main/modules/console.ts @@ -1,100 +1,100 @@ -import type App from '../app/index.js'; -import { hasTag } from '../app/guards.js'; -import { IN_BROWSER } from '../utils.js'; -import { ConsoleLog } from '../app/messages.gen.js'; +import type App from '../app/index.js' +import { hasTag } from '../app/guards.js' +import { IN_BROWSER } from '../utils.js' +import { ConsoleLog } from '../app/messages.gen.js' const printError: (e: Error) => string = IN_BROWSER && 'InstallTrigger' in window // detect Firefox ? (e: Error): string => e.message + '\n' + e.stack - : (e: Error): string => e.stack || e.message; + : (e: Error): string => e.stack || e.message function printString(arg: any): string { if (arg === undefined) { - return 'undefined'; + return 'undefined' } if (arg === null) { - return 'null'; + return 'null' } if (arg instanceof Error) { - return printError(arg); + return printError(arg) } if (Array.isArray(arg)) { - return `Array(${arg.length})`; + return `Array(${arg.length})` } - return String(arg); + return String(arg) } function printFloat(arg: any): string { - if (typeof arg !== 'number') return 'NaN'; - return arg.toString(); + if (typeof arg !== 'number') return 'NaN' + return arg.toString() } function printInt(arg: any): string { - if (typeof arg !== 'number') return 'NaN'; - return Math.floor(arg).toString(); + if (typeof arg !== 'number') return 'NaN' + return Math.floor(arg).toString() } function printObject(arg: any): string { if (arg === undefined) { - return 'undefined'; + return 'undefined' } if (arg === null) { - return 'null'; + return 'null' } if (arg instanceof Error) { - return printError(arg); + return printError(arg) } if (Array.isArray(arg)) { - const length = arg.length; - const values = arg.slice(0, 10).map(printString).join(', '); - return `Array(${length})[${values}]`; + const length = arg.length + const values = arg.slice(0, 10).map(printString).join(', ') + return `Array(${length})[${values}]` } if (typeof arg === 'object') { - const res = []; - let i = 0; + const res = [] + let i = 0 for (const k in arg) { if (++i === 10) { - break; + break } - const v = arg[k]; - res.push(k + ': ' + printString(v)); + const v = arg[k] + res.push(k + ': ' + printString(v)) } - return '{' + res.join(', ') + '}'; + return '{' + res.join(', ') + '}' } - return arg.toString(); + return arg.toString() } function printf(args: any[]): string { if (typeof args[0] === 'string') { args.unshift( args.shift().replace(/%(o|s|f|d|i)/g, (s: string, t: string): string => { - const arg = args.shift(); - if (arg === undefined) return s; + const arg = args.shift() + if (arg === undefined) return s switch (t) { case 'o': - return printObject(arg); + return printObject(arg) case 's': - return printString(arg); + return printString(arg) case 'f': - return printFloat(arg); + return printFloat(arg) case 'd': case 'i': - return printInt(arg); + return printInt(arg) default: - return s; + return s } }), - ); + ) } - return args.map(printObject).join(' '); + return args.map(printObject).join(' ') } export interface Options { - consoleMethods: Array | null; - consoleThrottling: number; + consoleMethods: Array | null + consoleThrottling: number } -const consoleMethods = ['log', 'info', 'warn', 'error', 'debug', 'assert']; +const consoleMethods = ['log', 'info', 'warn', 'error', 'debug', 'assert'] export default function (app: App, opts: Partial): void { const options: Options = Object.assign( @@ -103,54 +103,54 @@ export default function (app: App, opts: Partial): void { consoleThrottling: 30, }, opts, - ); + ) if (!Array.isArray(options.consoleMethods) || options.consoleMethods.length === 0) { - return; + return } const sendConsoleLog = app.safe((level: string, args: unknown[]): void => app.send(ConsoleLog(level, printf(args))), - ); + ) - let n: number; + let n: number const reset = (): void => { - n = 0; - }; - app.attachStartCallback(reset); - app.ticker.attach(reset, 33, false); + n = 0 + } + app.attachStartCallback(reset) + app.ticker.attach(reset, 33, false) const patchConsole = (console: Console) => options.consoleMethods!.forEach((method) => { if (consoleMethods.indexOf(method) === -1) { - console.error(`OpenReplay: unsupported console method "${method}"`); - return; + console.error(`OpenReplay: unsupported console method "${method}"`) + return } - const fn = (console as any)[method]; - (console as any)[method] = function (...args: unknown[]): void { - fn.apply(this, args); + const fn = (console as any)[method] + ;(console as any)[method] = function (...args: unknown[]): void { + fn.apply(this, args) if (n++ > options.consoleThrottling) { - return; + return } - sendConsoleLog(method, args); - }; - }); - patchConsole(window.console); + sendConsoleLog(method, args) + } + }) + patchConsole(window.console) app.nodes.attachNodeCallback( app.safe((node) => { if (hasTag(node, 'IFRAME')) { // TODO: newContextCallback - let context = node.contentWindow; + let context = node.contentWindow if (context) { - patchConsole((context as Window & typeof globalThis).console); + patchConsole((context as Window & typeof globalThis).console) } app.attachEventListener(node, 'load', () => { if (node.contentWindow !== context) { - context = node.contentWindow; - patchConsole((context as Window & typeof globalThis).console); + context = node.contentWindow + patchConsole((context as Window & typeof globalThis).console) } - }); + }) } }), - ); + ) } diff --git a/tracker/tracker/src/main/modules/cssrules.ts b/tracker/tracker/src/main/modules/cssrules.ts index 8ade63467..2c1176f99 100644 --- a/tracker/tracker/src/main/modules/cssrules.ts +++ b/tracker/tracker/src/main/modules/cssrules.ts @@ -1,14 +1,14 @@ -import type App from '../app/index.js'; -import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../app/messages.gen.js'; -import { hasTag } from '../app/guards.js'; +import type App from '../app/index.js' +import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../app/messages.gen.js' +import { hasTag } from '../app/guards.js' export default function (app: App | null) { if (app === null) { - return; + return } if (!window.CSSStyleSheet) { - app.send(TechnicalInfo('no_stylesheet_prototype_in_window', '')); - return; + app.send(TechnicalInfo('no_stylesheet_prototype_in_window', '')) + return } const processOperation = app.safe((stylesheet: CSSStyleSheet, index: number, rule?: string) => { @@ -16,38 +16,38 @@ export default function (app: App | null) { typeof rule === 'string' ? (nodeID: number) => app.send(CSSInsertRuleURLBased(nodeID, rule, index, app.getBaseHref())) - : (nodeID: number) => app.send(CSSDeleteRule(nodeID, index)); + : (nodeID: number) => app.send(CSSDeleteRule(nodeID, index)) // TODO: Extend messages to maintain nested rules (CSSGroupingRule prototype, as well as CSSKeyframesRule) if (stylesheet.ownerNode == null) { - throw new Error('Owner Node not found'); + throw new Error('Owner Node not found') } - const nodeID = app.nodes.getID(stylesheet.ownerNode); + const nodeID = app.nodes.getID(stylesheet.ownerNode) if (nodeID !== undefined) { - sendMessage(nodeID); + sendMessage(nodeID) } // else error? - }); + }) - const { insertRule, deleteRule } = CSSStyleSheet.prototype; + const { insertRule, deleteRule } = CSSStyleSheet.prototype CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) { - processOperation(this, index, rule); - return insertRule.call(this, rule, index); - }; + processOperation(this, index, rule) + return insertRule.call(this, rule, index) + } CSSStyleSheet.prototype.deleteRule = function (index: number) { - processOperation(this, index); - return deleteRule.call(this, index); - }; + processOperation(this, index) + return deleteRule.call(this, index) + } app.nodes.attachNodeCallback((node: Node): void => { if (!hasTag(node, 'STYLE') || !node.sheet) { - return; + return } if (node.textContent !== null && node.textContent.trim().length > 0) { - return; // Only fully virtual sheets maintained so far + return // Only fully virtual sheets maintained so far } - const rules = node.sheet.cssRules; + const rules = node.sheet.cssRules for (let i = 0; i < rules.length; i++) { - processOperation(node.sheet, i, rules[i].cssText); + processOperation(node.sheet, i, rules[i].cssText) } - }); + }) } diff --git a/tracker/tracker/src/main/modules/exception.ts b/tracker/tracker/src/main/modules/exception.ts index a30d74059..96e4266c0 100644 --- a/tracker/tracker/src/main/modules/exception.ts +++ b/tracker/tracker/src/main/modules/exception.ts @@ -1,18 +1,18 @@ -import type App from '../app/index.js'; -import type Message from '../app/messages.gen.js'; -import { JSException } from '../app/messages.gen.js'; -import ErrorStackParser from 'error-stack-parser'; +import type App from '../app/index.js' +import type Message from '../app/messages.gen.js' +import { JSException } from '../app/messages.gen.js' +import ErrorStackParser from 'error-stack-parser' export interface Options { - captureExceptions: boolean; + captureExceptions: boolean } interface StackFrame { - columnNumber?: number; - lineNumber?: number; - fileName?: string; - functionName?: string; - source?: string; + columnNumber?: number + lineNumber?: number + fileName?: string + functionName?: string + source?: string } function getDefaultStack(e: ErrorEvent): Array { @@ -24,15 +24,15 @@ function getDefaultStack(e: ErrorEvent): Array { functionName: '', source: '', }, - ]; + ] } export function getExceptionMessage(error: Error, fallbackStack: Array): Message { - let stack = fallbackStack; + let stack = fallbackStack try { - stack = ErrorStackParser.parse(error); + stack = ErrorStackParser.parse(error) } catch (e) {} - return JSException(error.name, error.message, JSON.stringify(stack)); + return JSException(error.name, error.message, JSON.stringify(stack)) } export function getExceptionMessageFromEvent( @@ -40,29 +40,29 @@ export function getExceptionMessageFromEvent( ): Message | null { if (e instanceof ErrorEvent) { if (e.error instanceof Error) { - return getExceptionMessage(e.error, getDefaultStack(e)); + return getExceptionMessage(e.error, getDefaultStack(e)) } else { - let [name, message] = e.message.split(':'); + let [name, message] = e.message.split(':') if (!message) { - name = 'Error'; - message = e.message; + name = 'Error' + message = e.message } - return JSException(name, message, JSON.stringify(getDefaultStack(e))); + return JSException(name, message, JSON.stringify(getDefaultStack(e))) } } else if ('PromiseRejectionEvent' in window && e instanceof PromiseRejectionEvent) { if (e.reason instanceof Error) { - return getExceptionMessage(e.reason, []); + return getExceptionMessage(e.reason, []) } else { - let message: string; + let message: string try { - message = JSON.stringify(e.reason); + message = JSON.stringify(e.reason) } catch (_) { - message = String(e.reason); + message = String(e.reason) } - return JSException('Unhandled Promise Rejection', message, '[]'); + return JSException('Unhandled Promise Rejection', message, '[]') } } - return null; + return null } export default function (app: App, opts: Partial): void { @@ -71,18 +71,18 @@ export default function (app: App, opts: Partial): void { captureExceptions: true, }, opts, - ); + ) if (options.captureExceptions) { const handler = (e: ErrorEvent | PromiseRejectionEvent): void => { - const msg = getExceptionMessageFromEvent(e); + const msg = getExceptionMessageFromEvent(e) if (msg != null) { - app.send(msg); + app.send(msg) } - }; + } app.attachEventListener(window, 'unhandledrejection', (e: PromiseRejectionEvent): void => handler(e), - ); - app.attachEventListener(window, 'error', (e: ErrorEvent): void => handler(e)); + ) + app.attachEventListener(window, 'error', (e: ErrorEvent): void => handler(e)) } } diff --git a/tracker/tracker/src/main/modules/img.ts b/tracker/tracker/src/main/modules/img.ts index 58359bdf7..1ca15e094 100644 --- a/tracker/tracker/src/main/modules/img.ts +++ b/tracker/tracker/src/main/modules/img.ts @@ -1,92 +1,92 @@ -import type App from '../app/index.js'; -import { timestamp, isURL } from '../utils.js'; -import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from '../app/messages.gen.js'; -import { hasTag } from '../app/guards.js'; +import type App from '../app/index.js' +import { timestamp, isURL } from '../utils.js' +import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from '../app/messages.gen.js' +import { hasTag } from '../app/guards.js' function resolveURL(url: string, location: Location = document.location) { - url = url.trim(); + url = url.trim() if (url.startsWith('/')) { - return location.origin + url; + return location.origin + url } else if ( url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:') // any other possible value here? ) { - return url; + return url } else { - return location.origin + location.pathname + url; + return location.origin + location.pathname + url } } -const PLACEHOLDER_SRC = 'https://static.openreplay.com/tracker/placeholder.jpeg'; +const PLACEHOLDER_SRC = 'https://static.openreplay.com/tracker/placeholder.jpeg' export default function (app: App): void { function sendPlaceholder(id: number, node: HTMLImageElement): void { - app.send(SetNodeAttribute(id, 'src', PLACEHOLDER_SRC)); - const { width, height } = node.getBoundingClientRect(); + app.send(SetNodeAttribute(id, 'src', PLACEHOLDER_SRC)) + const { width, height } = node.getBoundingClientRect() if (!node.hasAttribute('width')) { - app.send(SetNodeAttribute(id, 'width', String(width))); + app.send(SetNodeAttribute(id, 'width', String(width))) } if (!node.hasAttribute('height')) { - app.send(SetNodeAttribute(id, 'height', String(height))); + app.send(SetNodeAttribute(id, 'height', String(height))) } } const sendImgSrc = app.safe(function (this: HTMLImageElement): void { - const id = app.nodes.getID(this); + const id = app.nodes.getID(this) if (id === undefined) { - return; + return } - const { src, complete, naturalWidth, naturalHeight, srcset } = this; + const { src, complete, naturalWidth, naturalHeight, srcset } = this if (!complete) { - return; + return } - const resolvedSrc = resolveURL(src || ''); // Src type is null sometimes. - is it true? + const resolvedSrc = resolveURL(src || '') // Src type is null sometimes. - is it true? if (naturalWidth === 0 && naturalHeight === 0) { if (isURL(resolvedSrc)) { - app.send(ResourceTiming(timestamp(), 0, 0, 0, 0, 0, resolvedSrc, 'img')); + app.send(ResourceTiming(timestamp(), 0, 0, 0, 0, 0, resolvedSrc, 'img')) } } else if (resolvedSrc.length >= 1e5 || app.sanitizer.isMasked(id)) { - sendPlaceholder(id, this); + sendPlaceholder(id, this) } else { - app.send(SetNodeAttribute(id, 'src', resolvedSrc)); + app.send(SetNodeAttribute(id, 'src', resolvedSrc)) if (srcset) { const resolvedSrcset = srcset .split(',') .map((str) => resolveURL(str)) - .join(','); - app.send(SetNodeAttribute(id, 'srcset', resolvedSrcset)); + .join(',') + app.send(SetNodeAttribute(id, 'srcset', resolvedSrcset)) } } - }); + }) const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'attributes') { - const target = mutation.target as HTMLImageElement; - const id = app.nodes.getID(target); + const target = mutation.target as HTMLImageElement + const id = app.nodes.getID(target) if (id === undefined) { - return; + return } if (mutation.attributeName === 'src') { - const src = target.src; - app.send(SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref())); + const src = target.src + app.send(SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref())) } if (mutation.attributeName === 'srcset') { - const srcset = target.srcset; - app.send(SetNodeAttribute(id, 'srcset', srcset)); + const srcset = target.srcset + app.send(SetNodeAttribute(id, 'srcset', srcset)) } } } - }); + }) app.nodes.attachNodeCallback((node: Node): void => { if (!hasTag(node, 'IMG')) { - return; + return } - app.nodes.attachElementListener('error', node, sendImgSrc); - app.nodes.attachElementListener('load', node, sendImgSrc); - sendImgSrc.call(node); - observer.observe(node, { attributes: true, attributeFilter: ['src', 'srcset'] }); - }); + app.nodes.attachElementListener('error', node, sendImgSrc) + app.nodes.attachElementListener('load', node, sendImgSrc) + sendImgSrc.call(node) + observer.observe(node, { attributes: true, attributeFilter: ['src', 'srcset'] }) + }) } diff --git a/tracker/tracker/src/main/modules/input.ts b/tracker/tracker/src/main/modules/input.ts index 3654bcf25..8364d886c 100644 --- a/tracker/tracker/src/main/modules/input.ts +++ b/tracker/tracker/src/main/modules/input.ts @@ -1,74 +1,74 @@ -import type App from '../app/index.js'; -import { normSpaces, IN_BROWSER, getLabelAttribute, hasOpenreplayAttribute } from '../utils.js'; -import { hasTag } from '../app/guards.js'; -import { SetInputTarget, SetInputValue, SetInputChecked } from '../app/messages.gen.js'; +import type App from '../app/index.js' +import { normSpaces, IN_BROWSER, getLabelAttribute, hasOpenreplayAttribute } from '../utils.js' +import { hasTag } from '../app/guards.js' +import { SetInputTarget, SetInputValue, SetInputChecked } from '../app/messages.gen.js' -const INPUT_TYPES = ['text', 'password', 'email', 'search', 'number', 'range', 'date']; +const INPUT_TYPES = ['text', 'password', 'email', 'search', 'number', 'range', 'date'] // TODO: take into consideration "contenteditable" attribute -type TextEditableElement = HTMLInputElement | HTMLTextAreaElement; +type TextEditableElement = HTMLInputElement | HTMLTextAreaElement function isTextEditable(node: any): node is TextEditableElement { if (hasTag(node, 'TEXTAREA')) { - return true; + return true } if (!hasTag(node, 'INPUT')) { - return false; + return false } - return INPUT_TYPES.includes(node.type); + return INPUT_TYPES.includes(node.type) } function isCheckable(node: any): node is HTMLInputElement { if (!hasTag(node, 'INPUT')) { - return false; + return false } - const type = node.type; - return type === 'checkbox' || type === 'radio'; + const type = node.type + return type === 'checkbox' || type === 'radio' } const labelElementFor: (element: TextEditableElement) => HTMLLabelElement | undefined = IN_BROWSER && 'labels' in HTMLInputElement.prototype ? (node) => { - let p: Node | null = node; + let p: Node | null = node while ((p = p.parentNode) !== null) { if (hasTag(p, 'LABEL')) { - return p; + return p } } - const labels = node.labels; + const labels = node.labels if (labels !== null && labels.length === 1) { - return labels[0]; + return labels[0] } } : (node) => { - let p: Node | null = node; + let p: Node | null = node while ((p = p.parentNode) !== null) { if (hasTag(p, 'LABEL')) { - return p; + return p } } - const id = node.id; + const id = node.id if (id) { - const labels = document.querySelectorAll('label[for="' + id + '"]'); + const labels = document.querySelectorAll('label[for="' + id + '"]') if (labels !== null && labels.length === 1) { - return labels[0] as HTMLLabelElement; + return labels[0] as HTMLLabelElement } } - }; + } export function getInputLabel(node: TextEditableElement): string { - let label = getLabelAttribute(node); + let label = getLabelAttribute(node) if (label === null) { - const labelElement = labelElementFor(node); + const labelElement = labelElementFor(node) label = (labelElement && labelElement.innerText) || node.placeholder || node.name || node.id || node.className || - node.type; + node.type } - return normSpaces(label).slice(0, 100); + return normSpaces(label).slice(0, 100) } export declare const enum InputMode { @@ -78,10 +78,10 @@ export declare const enum InputMode { } export interface Options { - obscureInputNumbers: boolean; - obscureInputEmails: boolean; - defaultInputMode: InputMode; - obscureInputDates: boolean; + obscureInputNumbers: boolean + obscureInputEmails: boolean + defaultInputMode: InputMode + obscureInputDates: boolean } export default function (app: App, opts: Partial): void { @@ -93,18 +93,18 @@ export default function (app: App, opts: Partial): void { obscureInputDates: false, }, opts, - ); + ) function sendInputTarget(id: number, node: TextEditableElement): void { - const label = getInputLabel(node); + const label = getInputLabel(node) if (label !== '') { - app.send(SetInputTarget(id, label)); + app.send(SetInputTarget(id, label)) } } function sendInputValue(id: number, node: TextEditableElement | HTMLSelectElement): void { - let value = node.value; - let inputMode: InputMode = options.defaultInputMode; + let value = node.value + let inputMode: InputMode = options.defaultInputMode if (node.type === 'password' || hasOpenreplayAttribute(node, 'hidden')) { - inputMode = InputMode.Hidden; + inputMode = InputMode.Hidden } else if ( hasOpenreplayAttribute(node, 'obscured') || (inputMode === InputMode.Plain && @@ -112,88 +112,88 @@ export default function (app: App, opts: Partial): void { (options.obscureInputDates && node.type === 'date') || (options.obscureInputEmails && (node.type === 'email' || !!~value.indexOf('@'))))) ) { - inputMode = InputMode.Obscured; + inputMode = InputMode.Obscured } - let mask = 0; + let mask = 0 switch (inputMode) { case InputMode.Hidden: - mask = -1; - value = ''; - break; + mask = -1 + value = '' + break case InputMode.Obscured: - mask = value.length; - value = ''; - break; + mask = value.length + value = '' + break } - app.send(SetInputValue(id, value, mask)); + app.send(SetInputValue(id, value, mask)) } - const inputValues: Map = new Map(); - const checkableValues: Map = new Map(); - const registeredTargets: Set = new Set(); + const inputValues: Map = new Map() + const checkableValues: Map = new Map() + const registeredTargets: Set = new Set() app.attachStopCallback(() => { - inputValues.clear(); - checkableValues.clear(); - registeredTargets.clear(); - }); + inputValues.clear() + checkableValues.clear() + registeredTargets.clear() + }) app.ticker.attach((): void => { inputValues.forEach((value, id) => { - const node = app.nodes.getNode(id); - if (!node) return; + const node = app.nodes.getNode(id) + if (!node) return if (!isTextEditable(node)) { - inputValues.delete(id); - return; + inputValues.delete(id) + return } if (value !== node.value) { - inputValues.set(id, node.value); + inputValues.set(id, node.value) if (!registeredTargets.has(id)) { - registeredTargets.add(id); - sendInputTarget(id, node); + registeredTargets.add(id) + sendInputTarget(id, node) } - sendInputValue(id, node); + sendInputValue(id, node) } - }); + }) checkableValues.forEach((checked, id) => { - const node = app.nodes.getNode(id); - if (!node) return; + const node = app.nodes.getNode(id) + if (!node) return if (!isCheckable(node)) { - checkableValues.delete(id); - return; + checkableValues.delete(id) + return } if (checked !== node.checked) { - checkableValues.set(id, node.checked); - app.send(SetInputChecked(id, node.checked)); + checkableValues.set(id, node.checked) + app.send(SetInputChecked(id, node.checked)) } - }); - }); - app.ticker.attach(Set.prototype.clear, 100, false, registeredTargets); + }) + }) + app.ticker.attach(Set.prototype.clear, 100, false, registeredTargets) app.nodes.attachNodeCallback( app.safe((node: Node): void => { - const id = app.nodes.getID(node); + const id = app.nodes.getID(node) if (id === undefined) { - return; + return } // TODO: support multiple select (?): use selectedOptions; Need send target? if (hasTag(node, 'SELECT')) { - sendInputValue(id, node); + sendInputValue(id, node) app.attachEventListener(node, 'change', () => { - sendInputValue(id, node); - }); + sendInputValue(id, node) + }) } if (isTextEditable(node)) { - inputValues.set(id, node.value); - sendInputValue(id, node); - return; + inputValues.set(id, node.value) + sendInputValue(id, node) + return } if (isCheckable(node)) { - checkableValues.set(id, node.checked); - app.send(SetInputChecked(id, node.checked)); - return; + checkableValues.set(id, node.checked) + app.send(SetInputChecked(id, node.checked)) + return } }), - ); + ) } diff --git a/tracker/tracker/src/main/modules/longtasks.ts b/tracker/tracker/src/main/modules/longtasks.ts index 068623c77..2789aa99e 100644 --- a/tracker/tracker/src/main/modules/longtasks.ts +++ b/tracker/tracker/src/main/modules/longtasks.ts @@ -1,22 +1,22 @@ -import type App from '../app/index.js'; -import { LongTask } from '../app/messages.gen.js'; +import type App from '../app/index.js' +import { LongTask } from '../app/messages.gen.js' // https://w3c.github.io/performance-timeline/#the-performanceentry-interface interface TaskAttributionTiming extends PerformanceEntry { - readonly containerType: string; - readonly containerSrc: string; - readonly containerId: string; - readonly containerName: string; + readonly containerType: string + readonly containerSrc: string + readonly containerId: string + readonly containerName: string } // https://www.w3.org/TR/longtasks/#performancelongtasktiming interface PerformanceLongTaskTiming extends PerformanceEntry { - readonly attribution: ReadonlyArray; + readonly attribution: ReadonlyArray } export default function (app: App): void { if (!('PerformanceObserver' in window) || !('PerformanceLongTaskTiming' in window)) { - return; + return } const contexts: string[] = [ @@ -29,19 +29,19 @@ export default function (app: App): void { 'cross-origin-descendant', 'cross-origin-unreachable', 'multiple-contexts', - ]; - const containerTypes: string[] = ['window', 'iframe', 'embed', 'object']; + ] + const containerTypes: string[] = ['window', 'iframe', 'embed', 'object'] function longTask(entry: PerformanceLongTaskTiming): void { let type = '', src = '', id = '', - name = ''; - const container = entry.attribution[0]; + name = '' + const container = entry.attribution[0] if (container != null) { - type = container.containerType; - name = container.containerName; - id = container.containerId; - src = container.containerSrc; + type = container.containerType + name = container.containerName + id = container.containerId + src = container.containerSrc } app.send( @@ -54,11 +54,11 @@ export default function (app: App): void { id, src, ), - ); + ) } const observer: PerformanceObserver = new PerformanceObserver((list) => list.getEntries().forEach(longTask), - ); - observer.observe({ entryTypes: ['longtask'] }); + ) + observer.observe({ entryTypes: ['longtask'] }) } diff --git a/tracker/tracker/src/main/modules/mouse.ts b/tracker/tracker/src/main/modules/mouse.ts index 79b4ad0ee..4704dc5be 100644 --- a/tracker/tracker/src/main/modules/mouse.ts +++ b/tracker/tracker/src/main/modules/mouse.ts @@ -1,15 +1,15 @@ -import type App from '../app/index.js'; -import { hasTag, isSVGElement } from '../app/guards.js'; -import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils.js'; -import { MouseMove, MouseClick } from '../app/messages.gen.js'; -import { getInputLabel } from './input.js'; +import type App from '../app/index.js' +import { hasTag, isSVGElement } from '../app/guards.js' +import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils.js' +import { MouseMove, MouseClick } from '../app/messages.gen.js' +import { getInputLabel } from './input.js' function _getSelector(target: Element): string { - let el: Element | null = target; - let selector: string | null = null; + let el: Element | null = target + let selector: string | null = null do { if (el.id) { - return `#${el.id}` + (selector ? ` > ${selector}` : ''); + return `#${el.id}` + (selector ? ` > ${selector}` : '') } selector = el.className @@ -17,17 +17,17 @@ function _getSelector(target: Element): string { .map((cn) => cn.trim()) .filter((cn) => cn !== '') .reduce((sel, cn) => `${sel}.${cn}`, el.tagName.toLowerCase()) + - (selector ? ` > ${selector}` : ''); + (selector ? ` > ${selector}` : '') if (el === document.body) { - return selector; + return selector } - el = el.parentElement; - } while (el !== document.body && el !== null); - return selector; + el = el.parentElement + } while (el !== document.body && el !== null) + return selector } function isClickable(element: Element): boolean { - const tag = element.tagName.toUpperCase(); + const tag = element.tagName.toUpperCase() return ( tag === 'BUTTON' || tag === 'A' || @@ -35,7 +35,7 @@ function isClickable(element: Element): boolean { tag === 'SELECT' || (element as HTMLElement).onclick != null || element.getAttribute('role') === 'button' - ); + ) //|| element.className.includes("btn") // MBTODO: intersect addEventListener } @@ -43,113 +43,113 @@ function isClickable(element: Element): boolean { //TODO: fix (typescript doesn't allow work when the guard is inside the function) function getTarget(target: EventTarget | null): Element | null { if (target instanceof Element) { - return _getTarget(target); + return _getTarget(target) } - return null; + return null } function _getTarget(target: Element): Element | null { - let element: Element | null = target; + let element: Element | null = target while (element !== null && element !== document.documentElement) { if (hasOpenreplayAttribute(element, 'masked')) { - return null; + return null } - element = element.parentElement; + element = element.parentElement } if (isSVGElement(target)) { - let owner = target.ownerSVGElement; + let owner = target.ownerSVGElement while (owner !== null) { - target = owner; - owner = owner.ownerSVGElement; + target = owner + owner = owner.ownerSVGElement } } - element = target; + element = target while (element !== null && element !== document.documentElement) { - const tag = element.tagName.toUpperCase(); + const tag = element.tagName.toUpperCase() if (tag === 'LABEL') { - return null; + return null } if (tag === 'INPUT') { - return element; + return element } if (isClickable(element) || getLabelAttribute(element) !== null) { - return element; + return element } - element = element.parentElement; + element = element.parentElement } - return target === document.documentElement ? null : target; + return target === document.documentElement ? null : target } export default function (app: App): void { function getTargetLabel(target: Element): string { - const dl = getLabelAttribute(target); + const dl = getLabelAttribute(target) if (dl !== null) { - return dl; + return dl } if (hasTag(target, 'INPUT')) { - return getInputLabel(target); + return getInputLabel(target) } if (isClickable(target)) { - let label = ''; + let label = '' if (target instanceof HTMLElement) { - label = app.sanitizer.getInnerTextSecure(target); + label = app.sanitizer.getInnerTextSecure(target) } - label = label || target.id || target.className; - return normSpaces(label).slice(0, 100); + label = label || target.id || target.className + return normSpaces(label).slice(0, 100) } - return ''; + return '' } - let mousePositionX = -1; - let mousePositionY = -1; - let mousePositionChanged = false; - let mouseTarget: Element | null = null; - let mouseTargetTime = 0; + let mousePositionX = -1 + let mousePositionY = -1 + let mousePositionChanged = false + let mouseTarget: Element | null = null + let mouseTargetTime = 0 app.attachStopCallback(() => { - mousePositionX = -1; - mousePositionY = -1; - mousePositionChanged = false; - mouseTarget = null; - }); + mousePositionX = -1 + mousePositionY = -1 + mousePositionChanged = false + mouseTarget = null + }) const sendMouseMove = (): void => { if (mousePositionChanged) { - app.send(MouseMove(mousePositionX, mousePositionY)); - mousePositionChanged = false; + app.send(MouseMove(mousePositionX, mousePositionY)) + mousePositionChanged = false } - }; + } - const selectorMap: { [id: number]: string } = {}; + const selectorMap: { [id: number]: string } = {} function getSelector(id: number, target: Element): string { - return (selectorMap[id] = selectorMap[id] || _getSelector(target)); + return (selectorMap[id] = selectorMap[id] || _getSelector(target)) } app.attachEventListener(document.documentElement, 'mouseover', (e: MouseEvent): void => { - const target = getTarget(e.target); + const target = getTarget(e.target) if (target !== mouseTarget) { - mouseTarget = target; - mouseTargetTime = performance.now(); + mouseTarget = target + mouseTargetTime = performance.now() } - }); + }) app.attachEventListener( document, 'mousemove', (e: MouseEvent): void => { - mousePositionX = e.clientX; - mousePositionY = e.clientY; - mousePositionChanged = true; + mousePositionX = e.clientX + mousePositionY = e.clientY + mousePositionChanged = true }, false, - ); + ) app.attachEventListener(document, 'click', (e: MouseEvent): void => { - const target = getTarget(e.target); + const target = getTarget(e.target) if ((!e.clientX && !e.clientY) || target === null) { - return; + return } - const id = app.nodes.getID(target); + const id = app.nodes.getID(target) if (id !== undefined) { - sendMouseMove(); + sendMouseMove() app.send( MouseClick( id, @@ -158,10 +158,10 @@ export default function (app: App): void { getSelector(id, target), ), true, - ); + ) } - mouseTarget = null; - }); + mouseTarget = null + }) - app.ticker.attach(sendMouseMove, 10); + app.ticker.attach(sendMouseMove, 10) } diff --git a/tracker/tracker/src/main/modules/performance.ts b/tracker/tracker/src/main/modules/performance.ts index c51447a21..3d6ad1458 100644 --- a/tracker/tracker/src/main/modules/performance.ts +++ b/tracker/tracker/src/main/modules/performance.ts @@ -1,25 +1,25 @@ -import type App from '../app/index.js'; -import { IN_BROWSER } from '../utils.js'; -import { PerformanceTrack } from '../app/messages.gen.js'; +import type App from '../app/index.js' +import { IN_BROWSER } from '../utils.js' +import { PerformanceTrack } from '../app/messages.gen.js' type Perf = { memory: { - totalJSHeapSize?: number; - usedJSHeapSize?: number; - jsHeapSizeLimit?: number; - }; -}; + totalJSHeapSize?: number + usedJSHeapSize?: number + jsHeapSizeLimit?: number + } +} const perf: Perf = IN_BROWSER && 'performance' in window && 'memory' in performance // works in Chrome only ? (performance as any) - : { memory: {} }; + : { memory: {} } -export const deviceMemory = IN_BROWSER ? ((navigator as any).deviceMemory || 0) * 1024 : 0; -export const jsHeapSizeLimit = perf.memory.jsHeapSizeLimit || 0; +export const deviceMemory = IN_BROWSER ? ((navigator as any).deviceMemory || 0) * 1024 : 0 +export const jsHeapSizeLimit = perf.memory.jsHeapSizeLimit || 0 export interface Options { - capturePerformance: boolean; + capturePerformance: boolean } export default function (app: App, opts: Partial): void { @@ -28,36 +28,36 @@ export default function (app: App, opts: Partial): void { capturePerformance: true, }, opts, - ); + ) if (!options.capturePerformance) { - return; + return } - let frames: number | undefined; - let ticks: number | undefined; + let frames: number | undefined + let ticks: number | undefined const nextFrame = (): void => { if (frames === undefined || frames === -1) { - return; + return } - frames++; - requestAnimationFrame(nextFrame); - }; + frames++ + requestAnimationFrame(nextFrame) + } app.ticker.attach( (): void => { if (ticks === undefined || ticks === -1) { - return; + return } - ticks++; + ticks++ }, 0, false, - ); + ) const sendPerformanceTrack = (): void => { if (frames === undefined || ticks === undefined) { - return; + return } app.send( PerformanceTrack( @@ -66,21 +66,21 @@ export default function (app: App, opts: Partial): void { perf.memory.totalJSHeapSize || 0, perf.memory.usedJSHeapSize || 0, ), - ); - ticks = frames = document.hidden ? -1 : 0; - }; + ) + ticks = frames = document.hidden ? -1 : 0 + } app.attachStartCallback((): void => { - ticks = frames = -1; - sendPerformanceTrack(); - nextFrame(); - }); + ticks = frames = -1 + sendPerformanceTrack() + nextFrame() + }) app.attachStopCallback((): void => { - ticks = frames = undefined; - }); + ticks = frames = undefined + }) - app.ticker.attach(sendPerformanceTrack, 40, false); + app.ticker.attach(sendPerformanceTrack, 40, false) if (document.hidden !== undefined) { app.attachEventListener( @@ -89,6 +89,6 @@ export default function (app: App, opts: Partial): void { sendPerformanceTrack as EventListener, false, false, - ); + ) } } diff --git a/tracker/tracker/src/main/modules/scroll.ts b/tracker/tracker/src/main/modules/scroll.ts index e170afafb..a14399ea1 100644 --- a/tracker/tracker/src/main/modules/scroll.ts +++ b/tracker/tracker/src/main/modules/scroll.ts @@ -1,10 +1,10 @@ -import type App from '../app/index.js'; -import { SetViewportScroll, SetNodeScroll } from '../app/messages.gen.js'; -import { isElementNode } from '../app/guards.js'; +import type App from '../app/index.js' +import { SetViewportScroll, SetNodeScroll } from '../app/messages.gen.js' +import { isElementNode } from '../app/guards.js' export default function (app: App): void { - let documentScroll = false; - const nodeScroll: Map = new Map(); + let documentScroll = false + const nodeScroll: Map = new Map() const sendSetViewportScroll = app.safe((): void => app.send( @@ -19,49 +19,49 @@ export default function (app: App): void { 0, ), ), - ); + ) const sendSetNodeScroll = app.safe((s: [number, number], node: Node): void => { - const id = app.nodes.getID(node); + const id = app.nodes.getID(node) if (id !== undefined) { - app.send(SetNodeScroll(id, s[0], s[1])); + app.send(SetNodeScroll(id, s[0], s[1])) } - }); + }) - app.attachStartCallback(sendSetViewportScroll); + app.attachStartCallback(sendSetViewportScroll) app.attachStopCallback(() => { - documentScroll = false; - nodeScroll.clear(); - }); + documentScroll = false + nodeScroll.clear() + }) app.nodes.attachNodeCallback((node, isStart) => { if (isStart && isElementNode(node) && node.scrollLeft + node.scrollTop > 0) { - nodeScroll.set(node, [node.scrollLeft, node.scrollTop]); + nodeScroll.set(node, [node.scrollLeft, node.scrollTop]) } - }); + }) app.attachEventListener(window, 'scroll', (e: Event): void => { - const target = e.target; + const target = e.target if (target === document) { - documentScroll = true; - return; + documentScroll = true + return } if (target instanceof Element) { - nodeScroll.set(target, [target.scrollLeft, target.scrollTop]); + nodeScroll.set(target, [target.scrollLeft, target.scrollTop]) } - }); + }) app.ticker.attach( (): void => { if (documentScroll) { - sendSetViewportScroll(); - documentScroll = false; + sendSetViewportScroll() + documentScroll = false } - nodeScroll.forEach(sendSetNodeScroll); - nodeScroll.clear(); + nodeScroll.forEach(sendSetNodeScroll) + nodeScroll.clear() }, 5, false, - ); + ) } diff --git a/tracker/tracker/src/main/modules/timing.ts b/tracker/tracker/src/main/modules/timing.ts index 62b65a27b..2cb1e1767 100644 --- a/tracker/tracker/src/main/modules/timing.ts +++ b/tracker/tracker/src/main/modules/timing.ts @@ -1,62 +1,62 @@ -import type App from '../app/index.js'; -import { hasTag } from '../app/guards.js'; -import { isURL } from '../utils.js'; -import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../app/messages.gen.js'; +import type App from '../app/index.js' +import { hasTag } from '../app/guards.js' +import { isURL } from '../utils.js' +import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../app/messages.gen.js' // Inspired by https://github.com/WPO-Foundation/RUM-SpeedIndex/blob/master/src/rum-speedindex.js interface ResourcesTimeMap { - [k: string]: number; + [k: string]: number } interface PaintBlock { - time: number; - area: number; + time: number + area: number } function getPaintBlocks(resources: ResourcesTimeMap): Array { - const paintBlocks: Array = []; - const elements = document.getElementsByTagName('*'); - const styleURL = /url\(("[^"]*"|'[^']*'|[^)]*)\)/i; + const paintBlocks: Array = [] + const elements = document.getElementsByTagName('*') + const styleURL = /url\(("[^"]*"|'[^']*'|[^)]*)\)/i for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - let src = ''; + const element = elements[i] + let src = '' if (hasTag(element, 'IMG')) { - src = element.currentSrc || element.src; + src = element.currentSrc || element.src } if (!src) { - const backgroundImage = getComputedStyle(element).getPropertyValue('background-image'); + const backgroundImage = getComputedStyle(element).getPropertyValue('background-image') if (backgroundImage) { - const matches = styleURL.exec(backgroundImage); + const matches = styleURL.exec(backgroundImage) if (matches !== null) { - src = matches[1]; + src = matches[1] if (src.startsWith('"') || src.startsWith("'")) { - src = src.substr(1, src.length - 2); + src = src.substr(1, src.length - 2) } } } } - if (!src) continue; - const time = src.substr(0, 10) === 'data:image' ? 0 : resources[src]; - if (time === undefined) continue; - const rect = element.getBoundingClientRect(); - const top = Math.max(rect.top, 0); - const left = Math.max(rect.left, 0); + if (!src) continue + const time = src.substr(0, 10) === 'data:image' ? 0 : resources[src] + if (time === undefined) continue + const rect = element.getBoundingClientRect() + const top = Math.max(rect.top, 0) + const left = Math.max(rect.left, 0) const bottom = Math.min( rect.bottom, window.innerHeight || (document.documentElement && document.documentElement.clientHeight) || 0, - ); + ) const right = Math.min( rect.right, window.innerWidth || (document.documentElement && document.documentElement.clientWidth) || 0, - ); - if (bottom <= top || right <= left) continue; - const area = (bottom - top) * (right - left); - paintBlocks.push({ time, area }); + ) + if (bottom <= top || right <= left) continue + const area = (bottom - top) * (right - left) + paintBlocks.push({ time, area }) } - return paintBlocks; + return paintBlocks } function calculateSpeedIndex(firstContentfulPaint: number, paintBlocks: Array): number { @@ -69,20 +69,20 @@ function calculateSpeedIndex(firstContentfulPaint: number, paintBlocks: Array firstContentfulPaint ? time : firstContentfulPaint); + const { time, area } = paintBlocks[i] + a += area + s += area * (time > firstContentfulPaint ? time : firstContentfulPaint) } - return a === 0 ? 0 : s / a; + return a === 0 ? 0 : s / a } export interface Options { - captureResourceTimings: boolean; - capturePageLoadTimings: boolean; - capturePageRenderTimings: boolean; + captureResourceTimings: boolean + capturePageLoadTimings: boolean + capturePageRenderTimings: boolean } export default function (app: App, opts: Partial): void { @@ -93,20 +93,20 @@ export default function (app: App, opts: Partial): void { capturePageRenderTimings: true, }, opts, - ); + ) if (!('PerformanceObserver' in window)) { - options.captureResourceTimings = false; + options.captureResourceTimings = false } if (!options.captureResourceTimings) { - return; + return } // Resources are necessary for all timings - let resources: ResourcesTimeMap | null = {}; + let resources: ResourcesTimeMap | null = {} function resourceTiming(entry: PerformanceResourceTiming): void { - if (entry.duration < 0 || !isURL(entry.name) || app.isServiceURL(entry.name)) return; + if (entry.duration < 0 || !isURL(entry.name) || app.isServiceURL(entry.name)) return if (resources !== null) { - resources[entry.name] = entry.startTime + entry.duration; + resources[entry.name] = entry.startTime + entry.duration } app.send( ResourceTiming( @@ -119,51 +119,51 @@ export default function (app: App, opts: Partial): void { entry.name, entry.initiatorType, ), - ); + ) } const observer: PerformanceObserver = new PerformanceObserver((list) => list.getEntries().forEach(resourceTiming), - ); + ) - let prevSessionID: string | undefined; + let prevSessionID: string | undefined app.attachStartCallback(function ({ sessionID }) { if (sessionID !== prevSessionID) { // Send past page resources on a newly started session - performance.getEntriesByType('resource').forEach(resourceTiming); - prevSessionID = sessionID; + performance.getEntriesByType('resource').forEach(resourceTiming) + prevSessionID = sessionID } - observer.observe({ entryTypes: ['resource'] }); - }); + observer.observe({ entryTypes: ['resource'] }) + }) app.attachStopCallback(function () { - observer.disconnect(); - }); + observer.disconnect() + }) let firstPaint = 0, - firstContentfulPaint = 0; + firstContentfulPaint = 0 if (options.capturePageLoadTimings) { - let pageLoadTimingSent = false; + let pageLoadTimingSent = false app.ticker.attach(() => { if (pageLoadTimingSent) { - return; + return } if (firstPaint === 0 || firstContentfulPaint === 0) { performance.getEntriesByType('paint').forEach((entry: PerformanceEntry) => { - const { name, startTime } = entry; + const { name, startTime } = entry switch (name) { case 'first-paint': - firstPaint = startTime; - break; + firstPaint = startTime + break case 'first-contentful-paint': - firstContentfulPaint = startTime; - break; + firstContentfulPaint = startTime + break } - }); + }) } if (performance.timing.loadEventEnd || performance.now() > 30000) { - pageLoadTimingSent = true; + pageLoadTimingSent = true const { navigationStart, requestStart, @@ -173,7 +173,7 @@ export default function (app: App, opts: Partial): void { domContentLoadedEventEnd, loadEventStart, loadEventEnd, - } = performance.timing; + } = performance.timing app.send( PageLoadTiming( requestStart - navigationStart || 0, @@ -186,46 +186,46 @@ export default function (app: App, opts: Partial): void { firstPaint, firstContentfulPaint, ), - ); + ) } - }, 30); + }, 30) } if (options.capturePageRenderTimings) { let visuallyComplete = 0, interactiveWindowStartTime = 0, interactiveWindowTickTime: number | null = 0, - paintBlocks: Array | null = null; + paintBlocks: Array | null = null - let pageRenderTimingSent = false; + let pageRenderTimingSent = false app.ticker.attach(() => { if (pageRenderTimingSent) { - return; + return } - const time = performance.now(); + const time = performance.now() if (resources !== null) { visuallyComplete = Math.max.apply( null, Object.keys(resources).map((k) => (resources as any)[k]), - ); + ) if (time - visuallyComplete > 1000) { - paintBlocks = getPaintBlocks(resources); - resources = null; + paintBlocks = getPaintBlocks(resources) + resources = null } } if (interactiveWindowTickTime !== null) { if (time - interactiveWindowTickTime > 50) { - interactiveWindowStartTime = time; + interactiveWindowStartTime = time } - interactiveWindowTickTime = time - interactiveWindowStartTime > 5000 ? null : time; + interactiveWindowTickTime = time - interactiveWindowStartTime > 5000 ? null : time } if ((paintBlocks !== null && interactiveWindowTickTime === null) || time > 30000) { - pageRenderTimingSent = true; - resources = null; + pageRenderTimingSent = true + resources = null const speedIndex = paintBlocks === null ? 0 - : calculateSpeedIndex(firstContentfulPaint || firstPaint, paintBlocks); + : calculateSpeedIndex(firstContentfulPaint || firstPaint, paintBlocks) const timeToInteractive = interactiveWindowTickTime === null ? Math.max( @@ -234,15 +234,15 @@ export default function (app: App, opts: Partial): void { performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart || 0, ) - : 0; + : 0 app.send( PageRenderTiming( speedIndex, firstContentfulPaint > visuallyComplete ? firstContentfulPaint : visuallyComplete, timeToInteractive, ), - ); + ) } - }); + }) } } diff --git a/tracker/tracker/src/main/modules/viewport.ts b/tracker/tracker/src/main/modules/viewport.ts index eac4d5985..e17a70359 100644 --- a/tracker/tracker/src/main/modules/viewport.ts +++ b/tracker/tracker/src/main/modules/viewport.ts @@ -1,40 +1,40 @@ -import type App from '../app/index.js'; -import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../app/messages.gen.js'; +import type App from '../app/index.js' +import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../app/messages.gen.js' export default function (app: App): void { - let url: string, width: number, height: number; - let navigationStart = performance.timing.navigationStart; + let url: string, width: number, height: number + let navigationStart = performance.timing.navigationStart const sendSetPageLocation = app.safe(() => { - const { URL } = document; + const { URL } = document if (URL !== url) { - url = URL; - app.send(SetPageLocation(url, document.referrer, navigationStart)); - navigationStart = 0; + url = URL + app.send(SetPageLocation(url, document.referrer, navigationStart)) + navigationStart = 0 } - }); + }) const sendSetViewportSize = app.safe(() => { - const { innerWidth, innerHeight } = window; + const { innerWidth, innerHeight } = window if (innerWidth !== width || innerHeight !== height) { - width = innerWidth; - height = innerHeight; - app.send(SetViewportSize(width, height)); + width = innerWidth + height = innerHeight + app.send(SetViewportSize(width, height)) } - }); + }) const sendSetPageVisibility = document.hidden === undefined ? Function.prototype - : app.safe(() => app.send(SetPageVisibility(document.hidden))); + : app.safe(() => app.send(SetPageVisibility(document.hidden))) app.attachStartCallback(() => { - url = ''; - width = height = -1; - sendSetPageLocation(); - sendSetViewportSize(); - sendSetPageVisibility(); - }); + url = '' + width = height = -1 + sendSetPageLocation() + sendSetViewportSize() + sendSetPageVisibility() + }) if (document.hidden !== undefined) { app.attachEventListener( @@ -43,9 +43,9 @@ export default function (app: App): void { sendSetPageVisibility as EventListener, false, false, - ); + ) } - app.ticker.attach(sendSetPageLocation, 1, false); - app.ticker.attach(sendSetViewportSize, 5, false); + app.ticker.attach(sendSetPageLocation, 1, false) + app.ticker.attach(sendSetViewportSize, 5, false) } diff --git a/tracker/tracker/src/main/utils.ts b/tracker/tracker/src/main/utils.ts index 67aa68a94..ca4a5ddd9 100644 --- a/tracker/tracker/src/main/utils.ts +++ b/tracker/tracker/src/main/utils.ts @@ -1,65 +1,65 @@ export function timestamp(): number { - return Math.round(performance.now()) + performance.timing.navigationStart; + return Math.round(performance.now()) + performance.timing.navigationStart } export const stars: (str: string) => string = 'repeat' in String.prototype ? (str: string): string => '*'.repeat(str.length) - : (str: string): string => str.replace(/./g, '*'); + : (str: string): string => str.replace(/./g, '*') export function normSpaces(str: string): string { - return str.trim().replace(/\s+/g, ' '); + return str.trim().replace(/\s+/g, ' ') } // isAbsoluteUrl regexp: /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url) export function isURL(s: string): boolean { - return s.startsWith('https://') || s.startsWith('http://'); + return s.startsWith('https://') || s.startsWith('http://') } -export const IN_BROWSER = !(typeof window === 'undefined'); +export const IN_BROWSER = !(typeof window === 'undefined') // TODO: JOIN IT WITH LOGGER somehow (use logging decorators?); Don't forget about index.js loggin when there is no logger instance. -export const DOCS_HOST = 'https://docs.openreplay.com'; +export const DOCS_HOST = 'https://docs.openreplay.com' -const warnedFeatures: { [key: string]: boolean } = {}; +const warnedFeatures: { [key: string]: boolean } = {} export function deprecationWarn(nameOfFeature: string, useInstead: string, docsPath = '/'): void { if (warnedFeatures[nameOfFeature]) { - return; + return } console.warn( `OpenReplay: ${nameOfFeature} is deprecated. ${ useInstead ? `Please, use ${useInstead} instead.` : '' } Visit ${DOCS_HOST}${docsPath} for more information.`, - ); - warnedFeatures[nameOfFeature] = true; + ) + warnedFeatures[nameOfFeature] = true } export function getLabelAttribute(e: Element): string | null { - let value = e.getAttribute('data-openreplay-label'); + let value = e.getAttribute('data-openreplay-label') if (value !== null) { - return value; + return value } - value = e.getAttribute('data-asayer-label'); + value = e.getAttribute('data-asayer-label') if (value !== null) { - deprecationWarn('"data-asayer-label" attribute', '"data-openreplay-label" attribute', '/'); + deprecationWarn('"data-asayer-label" attribute', '"data-openreplay-label" attribute', '/') } - return value; + return value } export function hasOpenreplayAttribute(e: Element, name: string): boolean { - const newName = `data-openreplay-${name}`; + const newName = `data-openreplay-${name}` if (e.hasAttribute(newName)) { - return true; + return true } - const oldName = `data-asayer-${name}`; + const oldName = `data-asayer-${name}` if (e.hasAttribute(oldName)) { deprecationWarn( `"${oldName}" attribute`, `"${newName}" attribute`, '/installation/sanitize-data', - ); - return true; + ) + return true } - return false; + return false } diff --git a/tracker/tracker/src/main/vendors/finder/finder.ts b/tracker/tracker/src/main/vendors/finder/finder.ts index 547ed2b08..431956595 100644 --- a/tracker/tracker/src/main/vendors/finder/finder.ts +++ b/tracker/tracker/src/main/vendors/finder/finder.ts @@ -1,10 +1,10 @@ type Node = { - name: string; - penalty: number; - level?: number; -}; + name: string + penalty: number + level?: number +} -type Path = Node[]; +type Path = Node[] enum Limit { All, @@ -13,27 +13,27 @@ enum Limit { } export type Options = { - root: Element; - idName: (name: string) => boolean; - className: (name: string) => boolean; - tagName: (name: string) => boolean; - attr: (name: string, value: string) => boolean; - seedMinLength: number; - optimizedMinLength: number; - threshold: number; - maxNumberOfTries: number; -}; + root: Element + idName: (name: string) => boolean + className: (name: string) => boolean + tagName: (name: string) => boolean + attr: (name: string, value: string) => boolean + seedMinLength: number + optimizedMinLength: number + threshold: number + maxNumberOfTries: number +} -let config: Options; +let config: Options -let rootDocument: Document | Element; +let rootDocument: Document | Element export function finder(input: Element, options?: Partial) { if (input.nodeType !== Node.ELEMENT_NODE) { - throw new Error("Can't generate CSS selector for non-element node type."); + throw new Error("Can't generate CSS selector for non-element node type.") } if ('html' === input.tagName.toLowerCase()) { - return 'html'; + return 'html' } const defaults: Options = { @@ -46,264 +46,264 @@ export function finder(input: Element, options?: Partial) { optimizedMinLength: 2, threshold: 1000, maxNumberOfTries: 10000, - }; + } - config = { ...defaults, ...options }; + config = { ...defaults, ...options } - rootDocument = findRootDocument(config.root, defaults); + rootDocument = findRootDocument(config.root, defaults) let path = bottomUpSearch(input, Limit.All, () => bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)), - ); + ) if (path) { - const optimized = sort(optimize(path, input)); + const optimized = sort(optimize(path, input)) if (optimized.length > 0) { - path = optimized[0]; + path = optimized[0] } - return selector(path); + return selector(path) } else { - throw new Error('Selector was not found.'); + throw new Error('Selector was not found.') } } function findRootDocument(rootNode: Element | Document, defaults: Options) { if (rootNode.nodeType === Node.DOCUMENT_NODE) { - return rootNode; + return rootNode } if (rootNode === defaults.root) { - return rootNode.ownerDocument; + return rootNode.ownerDocument } - return rootNode; + return rootNode } function bottomUpSearch(input: Element, limit: Limit, fallback?: () => Path | null): Path | null { - let path: Path | null = null; - const stack: Node[][] = []; - let current: Element | null = input; - let i = 0; + let path: Path | null = null + const stack: Node[][] = [] + let current: Element | null = input + let i = 0 while (current && current !== config.root.parentElement) { let level: Node[] = maybe(id(current)) || maybe(...attr(current)) || maybe(...classNames(current)) || - maybe(tagName(current)) || [any()]; + maybe(tagName(current)) || [any()] - const nth = index(current); + const nth = index(current) if (limit === Limit.All) { if (nth) { - level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth))); + level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth))) } } else if (limit === Limit.Two) { - level = level.slice(0, 1); + level = level.slice(0, 1) if (nth) { - level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth))); + level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth))) } } else if (limit === Limit.One) { - const [node] = (level = level.slice(0, 1)); + const [node] = (level = level.slice(0, 1)) if (nth && dispensableNth(node)) { - level = [nthChild(node, nth)]; + level = [nthChild(node, nth)] } } for (const node of level) { - node.level = i; + node.level = i } - stack.push(level); + stack.push(level) if (stack.length >= config.seedMinLength) { - path = findUniquePath(stack, fallback); + path = findUniquePath(stack, fallback) if (path) { - break; + break } } - current = current.parentElement; - i++; + current = current.parentElement + i++ } if (!path) { - path = findUniquePath(stack, fallback); + path = findUniquePath(stack, fallback) } - return path; + return path } function findUniquePath(stack: Node[][], fallback?: () => Path | null): Path | null { - const paths = sort(combinations(stack)); + const paths = sort(combinations(stack)) if (paths.length > config.threshold) { - return fallback ? fallback() : null; + return fallback ? fallback() : null } for (const candidate of paths) { if (unique(candidate)) { - return candidate; + return candidate } } - return null; + return null } function selector(path: Path): string { - let node = path[0]; - let query = node.name; + let node = path[0] + let query = node.name for (let i = 1; i < path.length; i++) { - const level = path[i].level || 0; + const level = path[i].level || 0 if (node.level === level - 1) { - query = `${path[i].name} > ${query}`; + query = `${path[i].name} > ${query}` } else { - query = `${path[i].name} ${query}`; + query = `${path[i].name} ${query}` } - node = path[i]; + node = path[i] } - return query; + return query } function penalty(path: Path): number { - return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0); + return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0) } function unique(path: Path) { switch (rootDocument.querySelectorAll(selector(path)).length) { case 0: - throw new Error(`Can't select any node with this selector: ${selector(path)}`); + throw new Error(`Can't select any node with this selector: ${selector(path)}`) case 1: - return true; + return true default: - return false; + return false } } function id(input: Element): Node | null { - const elementId = input.getAttribute('id'); + const elementId = input.getAttribute('id') if (elementId && config.idName(elementId)) { return { name: '#' + cssesc(elementId, { isIdentifier: true }), penalty: 0, - }; + } } - return null; + return null } function attr(input: Element): Node[] { - const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value)); + const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value)) return attrs.map( (attr): Node => ({ name: '[' + cssesc(attr.name, { isIdentifier: true }) + '="' + cssesc(attr.value) + '"]', penalty: 0.5, }), - ); + ) } function classNames(input: Element): Node[] { - const names = Array.from(input.classList).filter(config.className); + const names = Array.from(input.classList).filter(config.className) return names.map( (name): Node => ({ name: '.' + cssesc(name, { isIdentifier: true }), penalty: 1, }), - ); + ) } function tagName(input: Element): Node | null { - const name = input.tagName.toLowerCase(); + const name = input.tagName.toLowerCase() if (config.tagName(name)) { return { name, penalty: 2, - }; + } } - return null; + return null } function any(): Node { return { name: '*', penalty: 3, - }; + } } function index(input: Element): number | null { - const parent = input.parentNode; + const parent = input.parentNode if (!parent) { - return null; + return null } - let child = parent.firstChild; + let child = parent.firstChild if (!child) { - return null; + return null } - let i = 0; + let i = 0 while (child) { if (child.nodeType === Node.ELEMENT_NODE) { - i++; + i++ } if (child === input) { - break; + break } - child = child.nextSibling; + child = child.nextSibling } - return i; + return i } function nthChild(node: Node, i: number): Node { return { name: node.name + `:nth-child(${i})`, penalty: node.penalty + 1, - }; + } } function dispensableNth(node: Node) { - return node.name !== 'html' && !node.name.startsWith('#'); + return node.name !== 'html' && !node.name.startsWith('#') } function maybe(...level: (Node | null)[]): Node[] | null { - const list = level.filter(notEmpty); + const list = level.filter(notEmpty) if (list.length > 0) { - return list; + return list } - return null; + return null } function notEmpty(value: T | null | undefined): value is T { - return value !== null && value !== undefined; + return value !== null && value !== undefined } function combinations(stack: Node[][], path: Node[] = []): Node[][] { - const paths: Node[][] = []; + const paths: Node[][] = [] if (stack.length > 0) { for (const node of stack[0]) { - paths.push(...combinations(stack.slice(1, stack.length), path.concat(node))); + paths.push(...combinations(stack.slice(1, stack.length), path.concat(node))) } } else { - paths.push(path); + paths.push(path) } - return paths; + return paths } function sort(paths: Iterable): Path[] { - return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)); + return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)) } type Scope = { - counter: number; - visited: Map; -}; + counter: number + visited: Map +} function optimize( path: Path, @@ -313,103 +313,103 @@ function optimize( visited: new Map(), }, ): Node[][] { - const paths: Node[][] = []; + const paths: Node[][] = [] if (path.length > 2 && path.length > config.optimizedMinLength) { for (let i = 1; i < path.length - 1; i++) { if (scope.counter > config.maxNumberOfTries) { - return paths; // Okay At least I tried! + return paths // Okay At least I tried! } - scope.counter += 1; - const newPath = [...path]; - newPath.splice(i, 1); - const newPathKey = selector(newPath); + scope.counter += 1 + const newPath = [...path] + newPath.splice(i, 1) + const newPathKey = selector(newPath) if (scope.visited.has(newPathKey)) { - return paths; + return paths } if (unique(newPath) && same(newPath, input)) { - paths.push(newPath); - scope.visited.set(newPathKey, true); - paths.push(...optimize(newPath, input, scope)); + paths.push(newPath) + scope.visited.set(newPathKey, true) + paths.push(...optimize(newPath, input, scope)) } } } - return paths; + return paths } function same(path: Path, input: Element) { - return rootDocument.querySelector(selector(path)) === input; + return rootDocument.querySelector(selector(path)) === input } -const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/; -const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/; -const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g; +const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/ +const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/ +const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g const defaultOptions = { escapeEverything: false, isIdentifier: false, quotes: 'single', wrap: false, -}; +} function cssesc(string: string, opt: Partial = {}) { - const options = { ...defaultOptions, ...opt }; + const options = { ...defaultOptions, ...opt } if (options.quotes != 'single' && options.quotes != 'double') { - options.quotes = 'single'; + options.quotes = 'single' } - const quote = options.quotes == 'double' ? '"' : "'"; - const isIdentifier = options.isIdentifier; + const quote = options.quotes == 'double' ? '"' : "'" + const isIdentifier = options.isIdentifier - const firstChar = string.charAt(0); - let output = ''; - let counter = 0; - const length = string.length; + const firstChar = string.charAt(0) + let output = '' + let counter = 0 + const length = string.length while (counter < length) { - const character = string.charAt(counter++); - let codePoint = character.charCodeAt(0); - let value: string | undefined = void 0; + const character = string.charAt(counter++) + let codePoint = character.charCodeAt(0) + let value: string | undefined = void 0 // If it’s not a printable ASCII character… if (codePoint < 0x20 || codePoint > 0x7e) { if (codePoint >= 0xd800 && codePoint <= 0xdbff && counter < length) { // It’s a high surrogate, and there is a next character. - const extra = string.charCodeAt(counter++); + const extra = string.charCodeAt(counter++) if ((extra & 0xfc00) == 0xdc00) { // next character is low surrogate - codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; + codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000 } else { // It’s an unmatched surrogate; only append this code unit, in case // the next code unit is the high surrogate of a surrogate pair. - counter--; + counter-- } } - value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + value = '\\' + codePoint.toString(16).toUpperCase() + ' ' } else { if (options.escapeEverything) { if (regexAnySingleEscape.test(character)) { - value = '\\' + character; + value = '\\' + character } else { - value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + value = '\\' + codePoint.toString(16).toUpperCase() + ' ' } } else if (/[\t\n\f\r\x0B]/.test(character)) { - value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + value = '\\' + codePoint.toString(16).toUpperCase() + ' ' } else if ( character == '\\' || (!isIdentifier && ((character == '"' && quote == character) || (character == "'" && quote == character))) || (isIdentifier && regexSingleEscape.test(character)) ) { - value = '\\' + character; + value = '\\' + character } else { - value = character; + value = character } } - output += value; + output += value } if (isIdentifier) { if (/^-[-\d]/.test(output)) { - output = '\\-' + output.slice(1); + output = '\\-' + output.slice(1) } else if (/\d/.test(firstChar)) { - output = '\\3' + firstChar + ' ' + output.slice(1); + output = '\\3' + firstChar + ' ' + output.slice(1) } } @@ -419,14 +419,14 @@ function cssesc(string: string, opt: Partial = {}) { output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) { if ($1 && $1.length % 2) { // It’s not safe to remove the space, so don’t. - return $0; + return $0 } // Strip the space. - return ($1 || '') + $2; - }); + return ($1 || '') + $2 + }) if (!isIdentifier && options.wrap) { - return quote + output + quote; + return quote + output + quote } - return output; + return output } diff --git a/tracker/tracker/src/webworker/BatchWriter.ts b/tracker/tracker/src/webworker/BatchWriter.ts index 705d77b00..b34adf78f 100644 --- a/tracker/tracker/src/webworker/BatchWriter.ts +++ b/tracker/tracker/src/webworker/BatchWriter.ts @@ -1,17 +1,17 @@ -import type Message from '../common/messages.gen.js'; -import * as Messages from '../common/messages.gen.js'; -import MessageEncoder from './MessageEncoder.gen.js'; -import PrimitiveEncoder from './PrimitiveEncoder.js'; +import type Message from '../common/messages.gen.js' +import * as Messages from '../common/messages.gen.js' +import MessageEncoder from './MessageEncoder.gen.js' +import PrimitiveEncoder from './PrimitiveEncoder.js' -const SIZE_BYTES = 2; -const MAX_M_SIZE = (1 << (SIZE_BYTES * 8)) - 1; +const SIZE_BYTES = 2 +const MAX_M_SIZE = (1 << (SIZE_BYTES * 8)) - 1 export default class BatchWriter { - private nextIndex = 0; - private beaconSize = 2 * 1e5; // Default 200kB - private encoder = new MessageEncoder(this.beaconSize); - private readonly sizeEncoder = new PrimitiveEncoder(SIZE_BYTES); - private isEmpty = true; + private nextIndex = 0 + private beaconSize = 2 * 1e5 // Default 200kB + private encoder = new MessageEncoder(this.beaconSize) + private readonly sizeEncoder = new PrimitiveEncoder(SIZE_BYTES) + private isEmpty = true constructor( private readonly pageNo: number, @@ -19,12 +19,12 @@ export default class BatchWriter { private url: string, private readonly onBatch: (batch: Uint8Array) => void, ) { - this.prepare(); + this.prepare() } private prepare(): void { if (!this.encoder.isEmpty()) { - return; + return } // MBTODO: move service-messages creation to webworker const batchMetadata: Messages.BatchMetadata = [ @@ -34,76 +34,76 @@ export default class BatchWriter { this.nextIndex, this.timestamp, this.url, - ]; - this.encoder.encode(batchMetadata); - this.isEmpty = true; + ] + this.encoder.encode(batchMetadata) + this.isEmpty = true } private write(message: Message): boolean { - const e = this.encoder; + const e = this.encoder if (!e.uint(message[0]) || !e.skip(SIZE_BYTES)) { // TODO: app.debug.log - return false; + return false } - const startOffset = e.getCurrentOffset(); - const wasWritten = e.encode(message); + const startOffset = e.getCurrentOffset() + const wasWritten = e.encode(message) if (wasWritten) { - const endOffset = e.getCurrentOffset(); - const size = endOffset - startOffset; + const endOffset = e.getCurrentOffset() + const size = endOffset - startOffset if (size > MAX_M_SIZE || !this.sizeEncoder.uint(size)) { - console.warn('OpenReplay: max message size overflow.'); - return false; + console.warn('OpenReplay: max message size overflow.') + return false } - this.sizeEncoder.checkpoint(); // TODO: separate checkpoint logic to an Encoder-inherit class - e.set(this.sizeEncoder.flush(), startOffset - SIZE_BYTES); + this.sizeEncoder.checkpoint() // TODO: separate checkpoint logic to an Encoder-inherit class + e.set(this.sizeEncoder.flush(), startOffset - SIZE_BYTES) - e.checkpoint(); - this.isEmpty = false; - this.nextIndex++; + e.checkpoint() + this.isEmpty = false + this.nextIndex++ } // app.debug.log - return wasWritten; + return wasWritten } - private beaconSizeLimit = 1e6; + private beaconSizeLimit = 1e6 setBeaconSizeLimit(limit: number) { - this.beaconSizeLimit = limit; + this.beaconSizeLimit = limit } writeMessage(message: Message) { if (message[0] === Messages.Type.Timestamp) { - this.timestamp = message[1]; // .timestamp + this.timestamp = message[1] // .timestamp } if (message[0] === Messages.Type.SetPageLocation) { - this.url = message[1]; // .url + this.url = message[1] // .url } if (this.write(message)) { - return; + return } - this.finaliseBatch(); + this.finaliseBatch() while (!this.write(message)) { if (this.beaconSize === this.beaconSizeLimit) { - console.warn('OpenReplay: beacon size overflow. Skipping large message.', message); - this.encoder.reset(); - this.prepare(); - return; + console.warn('OpenReplay: beacon size overflow. Skipping large message.', message) + this.encoder.reset() + this.prepare() + return } // MBTODO: tempWriter for one message? - this.beaconSize = Math.min(this.beaconSize * 2, this.beaconSizeLimit); - this.encoder = new MessageEncoder(this.beaconSize); - this.prepare(); + this.beaconSize = Math.min(this.beaconSize * 2, this.beaconSizeLimit) + this.encoder = new MessageEncoder(this.beaconSize) + this.prepare() } } finaliseBatch() { if (this.isEmpty) { - return; + return } - this.onBatch(this.encoder.flush()); - this.prepare(); + this.onBatch(this.encoder.flush()) + this.prepare() } clean() { - this.encoder.reset(); + this.encoder.reset() } } diff --git a/tracker/tracker/src/webworker/PrimitiveEncoder.ts b/tracker/tracker/src/webworker/PrimitiveEncoder.ts index 2d591a6ed..34cd3f055 100644 --- a/tracker/tracker/src/webworker/PrimitiveEncoder.ts +++ b/tracker/tracker/src/webworker/PrimitiveEncoder.ts @@ -1,4 +1,4 @@ -declare const TextEncoder: any; +declare const TextEncoder: any const textEncoder: { encode(str: string): Uint8Array } = typeof TextEncoder === 'function' ? new TextEncoder() @@ -6,112 +6,112 @@ const textEncoder: { encode(str: string): Uint8Array } = // Based on https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder encode(str): Uint8Array { const Len = str.length, - resArr = new Uint8Array(Len * 3); - let resPos = -1; + resArr = new Uint8Array(Len * 3) + let resPos = -1 for (let point = 0, nextcode = 0, i = 0; i !== Len; ) { - (point = str.charCodeAt(i)), (i += 1); + ;(point = str.charCodeAt(i)), (i += 1) if (point >= 0xd800 && point <= 0xdbff) { if (i === Len) { - resArr[(resPos += 1)] = 0xef; /*0b11101111*/ - resArr[(resPos += 1)] = 0xbf; /*0b10111111*/ - resArr[(resPos += 1)] = 0xbd; /*0b10111101*/ - break; + resArr[(resPos += 1)] = 0xef /*0b11101111*/ + resArr[(resPos += 1)] = 0xbf /*0b10111111*/ + resArr[(resPos += 1)] = 0xbd /*0b10111101*/ + break } // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae - nextcode = str.charCodeAt(i); + nextcode = str.charCodeAt(i) if (nextcode >= 0xdc00 && nextcode <= 0xdfff) { - point = (point - 0xd800) * 0x400 + nextcode - 0xdc00 + 0x10000; - i += 1; + point = (point - 0xd800) * 0x400 + nextcode - 0xdc00 + 0x10000 + i += 1 if (point > 0xffff) { - resArr[(resPos += 1)] = (0x1e /*0b11110*/ << 3) | (point >>> 18); + resArr[(resPos += 1)] = (0x1e /*0b11110*/ << 3) | (point >>> 18) resArr[(resPos += 1)] = - (0x2 /*0b10*/ << 6) | ((point >>> 12) & 0x3f); /*0b00111111*/ + (0x2 /*0b10*/ << 6) | ((point >>> 12) & 0x3f) /*0b00111111*/ resArr[(resPos += 1)] = - (0x2 /*0b10*/ << 6) | ((point >>> 6) & 0x3f); /*0b00111111*/ - resArr[(resPos += 1)] = (0x2 /*0b10*/ << 6) | (point & 0x3f); /*0b00111111*/ - continue; + (0x2 /*0b10*/ << 6) | ((point >>> 6) & 0x3f) /*0b00111111*/ + resArr[(resPos += 1)] = (0x2 /*0b10*/ << 6) | (point & 0x3f) /*0b00111111*/ + continue } } else { - resArr[(resPos += 1)] = 0xef; /*0b11101111*/ - resArr[(resPos += 1)] = 0xbf; /*0b10111111*/ - resArr[(resPos += 1)] = 0xbd; /*0b10111101*/ - continue; + resArr[(resPos += 1)] = 0xef /*0b11101111*/ + resArr[(resPos += 1)] = 0xbf /*0b10111111*/ + resArr[(resPos += 1)] = 0xbd /*0b10111101*/ + continue } } if (point <= 0x007f) { - resArr[(resPos += 1)] = (0x0 /*0b0*/ << 7) | point; + resArr[(resPos += 1)] = (0x0 /*0b0*/ << 7) | point } else if (point <= 0x07ff) { - resArr[(resPos += 1)] = (0x6 /*0b110*/ << 5) | (point >>> 6); - resArr[(resPos += 1)] = (0x2 /*0b10*/ << 6) | (point & 0x3f); /*0b00111111*/ + resArr[(resPos += 1)] = (0x6 /*0b110*/ << 5) | (point >>> 6) + resArr[(resPos += 1)] = (0x2 /*0b10*/ << 6) | (point & 0x3f) /*0b00111111*/ } else { - resArr[(resPos += 1)] = (0xe /*0b1110*/ << 4) | (point >>> 12); - resArr[(resPos += 1)] = (0x2 /*0b10*/ << 6) | ((point >>> 6) & 0x3f); /*0b00111111*/ - resArr[(resPos += 1)] = (0x2 /*0b10*/ << 6) | (point & 0x3f); /*0b00111111*/ + resArr[(resPos += 1)] = (0xe /*0b1110*/ << 4) | (point >>> 12) + resArr[(resPos += 1)] = (0x2 /*0b10*/ << 6) | ((point >>> 6) & 0x3f) /*0b00111111*/ + resArr[(resPos += 1)] = (0x2 /*0b10*/ << 6) | (point & 0x3f) /*0b00111111*/ } } - return resArr.subarray(0, resPos + 1); + return resArr.subarray(0, resPos + 1) }, - }; + } export default class PrimitiveEncoder { - private offset = 0; - private checkpointOffset = 0; - private readonly data: Uint8Array; + private offset = 0 + private checkpointOffset = 0 + private readonly data: Uint8Array constructor(private readonly size: number) { - this.data = new Uint8Array(size); + this.data = new Uint8Array(size) } getCurrentOffset(): number { - return this.offset; + return this.offset } checkpoint() { - this.checkpointOffset = this.offset; + this.checkpointOffset = this.offset } isEmpty(): boolean { - return this.offset === 0; + return this.offset === 0 } skip(n: number): boolean { - this.offset += n; - return this.offset <= this.size; + this.offset += n + return this.offset <= this.size } set(bytes: Uint8Array, offset: number) { - this.data.set(bytes, offset); + this.data.set(bytes, offset) } boolean(value: boolean): boolean { - this.data[this.offset++] = +value; - return this.offset <= this.size; + this.data[this.offset++] = +value + return this.offset <= this.size } uint(value: number): boolean { if (value < 0 || value > Number.MAX_SAFE_INTEGER) { - value = 0; + value = 0 } while (value >= 0x80) { - this.data[this.offset++] = value % 0x100 | 0x80; - value = Math.floor(value / 128); + this.data[this.offset++] = value % 0x100 | 0x80 + value = Math.floor(value / 128) } - this.data[this.offset++] = value; - return this.offset <= this.size; + this.data[this.offset++] = value + return this.offset <= this.size } int(value: number): boolean { - value = Math.round(value); - return this.uint(value >= 0 ? value * 2 : value * -2 - 1); + value = Math.round(value) + return this.uint(value >= 0 ? value * 2 : value * -2 - 1) } string(value: string): boolean { - const encoded = textEncoder.encode(value); - const length = encoded.byteLength; + const encoded = textEncoder.encode(value) + const length = encoded.byteLength if (!this.uint(length) || this.offset + length > this.size) { - return false; + return false } - this.data.set(encoded, this.offset); - this.offset += length; - return true; + this.data.set(encoded, this.offset) + this.offset += length + return true } reset(): void { - this.offset = 0; - this.checkpointOffset = 0; + this.offset = 0 + this.checkpointOffset = 0 } flush(): Uint8Array { - const data = this.data.slice(0, this.checkpointOffset); - this.reset(); - return data; + const data = this.data.slice(0, this.checkpointOffset) + this.reset() + return data } } diff --git a/tracker/tracker/src/webworker/QueueSender.ts b/tracker/tracker/src/webworker/QueueSender.ts index b6aa3ed98..6082e222b 100644 --- a/tracker/tracker/src/webworker/QueueSender.ts +++ b/tracker/tracker/src/webworker/QueueSender.ts @@ -1,6 +1,6 @@ -const INGEST_PATH = '/v1/web/i'; +const INGEST_PATH = '/v1/web/i' -const KEEPALIVE_SIZE_LIMIT = 64 << 10; // 64 kB +const KEEPALIVE_SIZE_LIMIT = 64 << 10 // 64 kB // function sendXHR(url: string, token: string, batch: Uint8Array): Promise { // const req = new XMLHttpRequest() @@ -21,11 +21,11 @@ const KEEPALIVE_SIZE_LIMIT = 64 << 10; // 64 kB // } export default class QueueSender { - private attemptsCount = 0; - private busy = false; - private readonly queue: Array = []; - private readonly ingestURL; - private token: string | null = null; + private attemptsCount = 0 + private busy = false + private readonly queue: Array = [] + private readonly ingestURL + private token: string | null = null constructor( ingestBaseURL: string, private readonly onUnauthorised: () => any, @@ -33,33 +33,33 @@ export default class QueueSender { private readonly MAX_ATTEMPTS_COUNT = 10, private readonly ATTEMPT_TIMEOUT = 1000, ) { - this.ingestURL = ingestBaseURL + INGEST_PATH; + this.ingestURL = ingestBaseURL + INGEST_PATH } authorise(token: string): void { - this.token = token; + this.token = token } push(batch: Uint8Array): void { if (this.busy || !this.token) { - this.queue.push(batch); + this.queue.push(batch) } else { - this.sendBatch(batch); + this.sendBatch(batch) } } private retry(batch: Uint8Array): void { if (this.attemptsCount >= this.MAX_ATTEMPTS_COUNT) { - this.onFailure(); - return; + this.onFailure() + return } - this.attemptsCount++; - setTimeout(() => this.sendBatch(batch), this.ATTEMPT_TIMEOUT * this.attemptsCount); + this.attemptsCount++ + setTimeout(() => this.sendBatch(batch), this.ATTEMPT_TIMEOUT * this.attemptsCount) } // would be nice to use Beacon API, but it is not available in WebWorker private sendBatch(batch: Uint8Array): void { - this.busy = true; + this.busy = true fetch(this.ingestURL, { body: batch, @@ -73,30 +73,30 @@ export default class QueueSender { .then((r) => { if (r.status === 401) { // TODO: continuous session ? - this.busy = false; - this.onUnauthorised(); - return; + this.busy = false + this.onUnauthorised() + return } else if (r.status >= 400) { - this.retry(batch); - return; + this.retry(batch) + return } // Success - this.attemptsCount = 0; - const nextBatch = this.queue.shift(); + this.attemptsCount = 0 + const nextBatch = this.queue.shift() if (nextBatch) { - this.sendBatch(nextBatch); + this.sendBatch(nextBatch) } else { - this.busy = false; + this.busy = false } }) .catch((e) => { - console.warn('OpenReplay:', e); - this.retry(batch); - }); + console.warn('OpenReplay:', e) + this.retry(batch) + }) } clean() { - this.queue.length = 0; + this.queue.length = 0 } } diff --git a/tracker/tracker/src/webworker/index.ts b/tracker/tracker/src/webworker/index.ts index 687d94154..6373d4cc9 100644 --- a/tracker/tracker/src/webworker/index.ts +++ b/tracker/tracker/src/webworker/index.ts @@ -1,9 +1,9 @@ -import type Message from '../common/messages.gen.js'; -import { Type as MType } from '../common/messages.gen.js'; -import { WorkerMessageData } from '../common/interaction.js'; +import type Message from '../common/messages.gen.js' +import { Type as MType } from '../common/messages.gen.js' +import { WorkerMessageData } from '../common/interaction.js' -import QueueSender from './QueueSender.js'; -import BatchWriter from './BatchWriter.js'; +import QueueSender from './QueueSender.js' +import BatchWriter from './BatchWriter.js' enum WorkerStatus { NotActive, @@ -12,112 +12,112 @@ enum WorkerStatus { Active, } -const AUTO_SEND_INTERVAL = 10 * 1000; +const AUTO_SEND_INTERVAL = 10 * 1000 -let sender: QueueSender | null = null; -let writer: BatchWriter | null = null; -let workerStatus: WorkerStatus = WorkerStatus.NotActive; +let sender: QueueSender | null = null +let writer: BatchWriter | null = null +let workerStatus: WorkerStatus = WorkerStatus.NotActive function send(): void { if (!writer) { - return; + return } - writer.finaliseBatch(); + writer.finaliseBatch() } function reset(): void { - workerStatus = WorkerStatus.Stopping; + workerStatus = WorkerStatus.Stopping if (sendIntervalID !== null) { - clearInterval(sendIntervalID); - sendIntervalID = null; + clearInterval(sendIntervalID) + sendIntervalID = null } if (writer) { - writer.clean(); - writer = null; + writer.clean() + writer = null } - workerStatus = WorkerStatus.NotActive; + workerStatus = WorkerStatus.NotActive } function resetCleanQueue(): void { if (sender) { - sender.clean(); - sender = null; + sender.clean() + sender = null } - reset(); + reset() } -let sendIntervalID: ReturnType | null = null; -let restartTimeoutID: ReturnType; +let sendIntervalID: ReturnType | null = null +let restartTimeoutID: ReturnType self.onmessage = ({ data }: MessageEvent): any => { if (data == null) { - send(); // TODO: sendAll? - return; + send() // TODO: sendAll? + return } if (data === 'stop') { - send(); - reset(); - return; + send() + reset() + return } if (Array.isArray(data)) { // Message[] if (!writer) { - throw new Error('WebWorker: writer not initialised. Service Should be Started.'); + throw new Error('WebWorker: writer not initialised. Service Should be Started.') } - const w = writer; + const w = writer data.forEach((message) => { if (message[0] === MType.SetPageVisibility) { if (message[1]) { // .hidden - restartTimeoutID = setTimeout(() => self.postMessage('restart'), 30 * 60 * 1000); + restartTimeoutID = setTimeout(() => self.postMessage('restart'), 30 * 60 * 1000) } else { - clearTimeout(restartTimeoutID); + clearTimeout(restartTimeoutID) } } - w.writeMessage(message); - }); - return; + w.writeMessage(message) + }) + return } if (data.type === 'start') { - workerStatus = WorkerStatus.Starting; + workerStatus = WorkerStatus.Starting sender = new QueueSender( data.ingestPoint, () => { // onUnauthorised - self.postMessage('restart'); + self.postMessage('restart') }, () => { // onFailure - resetCleanQueue(); - self.postMessage('failed'); + resetCleanQueue() + self.postMessage('failed') }, data.connAttemptCount, data.connAttemptGap, - ); + ) writer = new BatchWriter( data.pageNo, data.timestamp, data.url, // onBatch (batch) => sender && sender.push(batch), - ); + ) if (sendIntervalID === null) { - sendIntervalID = setInterval(send, AUTO_SEND_INTERVAL); + sendIntervalID = setInterval(send, AUTO_SEND_INTERVAL) } - return (workerStatus = WorkerStatus.Active); + return (workerStatus = WorkerStatus.Active) } if (data.type === 'auth') { if (!sender) { - throw new Error('WebWorker: sender not initialised. Received auth.'); + throw new Error('WebWorker: sender not initialised. Received auth.') } if (!writer) { - throw new Error('WebWorker: writer not initialised. Received auth.'); + throw new Error('WebWorker: writer not initialised. Received auth.') } - sender.authorise(data.token); - data.beaconSizeLimit && writer.setBeaconSizeLimit(data.beaconSizeLimit); - return; + sender.authorise(data.token) + data.beaconSizeLimit && writer.setBeaconSizeLimit(data.beaconSizeLimit) + return } -}; +}