From c2541786316d3d1bcc0273479dfb4778ce43f885 Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 25 Jul 2022 12:21:06 +0200 Subject: [PATCH] feat(tracker): linting hook for tracker --- frontend/app/date.ts | 4 +- tracker/.husky/pre-commit | 25 ++ tracker/tracker-assist/.eslintignore | 8 + tracker/tracker-assist/.eslintrc.cjs | 49 +++ tracker/tracker-assist/package.json | 25 +- .../tracker-assist/src/AnnotationCanvas.ts | 22 +- tracker/tracker-assist/src/Assist.ts | 165 ++++---- tracker/tracker-assist/src/CallWindow.ts | 112 +++--- .../src/ConfirmWindow/ConfirmWindow.ts | 139 +++---- .../src/ConfirmWindow/defaults.ts | 20 +- tracker/tracker-assist/src/LocalStream.ts | 24 +- tracker/tracker-assist/src/Mouse.ts | 50 +-- tracker/tracker-assist/src/RemoteControl.ts | 12 +- tracker/tracker-assist/src/_slim.ts | 2 +- tracker/tracker-assist/src/dnd.ts | 18 +- tracker/tracker-assist/src/icons.ts | 4 +- tracker/tracker-assist/src/index.ts | 16 +- tracker/tracker/.eslintignore | 8 + tracker/tracker/.eslintrc.cjs | 21 +- tracker/tracker/.prettierrc.json | 1 + tracker/tracker/package.json | 35 +- tracker/tracker/scripts/compile.cjs | 2 +- tracker/tracker/src/common/webworker.ts | 24 +- tracker/tracker/src/main/app/guards.ts | 37 +- tracker/tracker/src/main/app/index.ts | 369 +++++++++--------- tracker/tracker/src/main/app/logger.ts | 61 +-- tracker/tracker/src/main/app/nodes.ts | 10 +- .../src/main/app/observer/iframe_observer.ts | 15 +- .../tracker/src/main/app/observer/observer.ts | 112 +++--- .../main/app/observer/shadow_root_observer.ts | 15 +- .../src/main/app/observer/top_observer.ts | 131 ++++--- tracker/tracker/src/main/app/sanitizer.ts | 66 ++-- tracker/tracker/src/main/app/session.ts | 58 ++- tracker/tracker/src/main/app/ticker.ts | 2 +- tracker/tracker/src/main/index.ts | 167 ++++---- .../tracker/src/main/modules/connection.ts | 14 +- tracker/tracker/src/main/modules/console.ts | 42 +- tracker/tracker/src/main/modules/cssrules.ts | 50 ++- tracker/tracker/src/main/modules/exception.ts | 66 ++-- tracker/tracker/src/main/modules/img.ts | 61 +-- tracker/tracker/src/main/modules/input.ts | 63 ++- tracker/tracker/src/main/modules/longtasks.ts | 29 +- tracker/tracker/src/main/modules/mouse.ts | 106 +++-- .../tracker/src/main/modules/performance.ts | 35 +- tracker/tracker/src/main/modules/scroll.ts | 12 +- tracker/tracker/src/main/modules/timing.ts | 131 +++---- tracker/tracker/src/main/modules/viewport.ts | 8 +- tracker/tracker/src/main/utils.ts | 67 ++-- .../tracker/src/main/vendors/finder/finder.ts | 362 ++++++++--------- tracker/tracker/src/webworker/BatchWriter.ts | 76 ++-- .../tracker/src/webworker/PrimitiveWriter.ts | 44 +-- tracker/tracker/src/webworker/QueueSender.ts | 101 +++-- tracker/tracker/src/webworker/index.ts | 125 +++--- tracker/tracker/src/webworker/tsconfig.json | 4 +- 54 files changed, 1688 insertions(+), 1537 deletions(-) create mode 100755 tracker/.husky/pre-commit create mode 100644 tracker/tracker-assist/.eslintignore create mode 100644 tracker/tracker-assist/.eslintrc.cjs create mode 100644 tracker/tracker/.eslintignore diff --git a/frontend/app/date.ts b/frontend/app/date.ts index e043f33fd..897c4db90 100644 --- a/frontend/app/date.ts +++ b/frontend/app/date.ts @@ -13,7 +13,7 @@ export const durationFormatted = (duration: Duration):string => { duration = duration.toFormat('h\'h\'m\'m'); } else if (duration.as('months') < 1) { // show in days and hours duration = duration.toFormat('d\'d\'h\'h'); - } else { // + } else { duration = duration.toFormat('m\'m\'s\'s\''); } @@ -133,4 +133,4 @@ export const countDaysFrom = (timestamp: number): number => { const date = DateTime.fromMillis(timestamp); const d = new Date(); return Math.round(Math.abs(d.getTime() - date.toJSDate().getTime()) / (1000 * 3600 * 24)); -} \ No newline at end of file +} diff --git a/tracker/.husky/pre-commit b/tracker/.husky/pre-commit new file mode 100755 index 000000000..9471928e2 --- /dev/null +++ b/tracker/.husky/pre-commit @@ -0,0 +1,25 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +if git diff --cached --name-only | grep --quiet '^tracker/tracker/' +then + echo "tracker" + pwd + cd tracker/tracker + + npm run lint-front + + cd ../../ +fi + +if git diff --cached --name-only | grep --quiet '^tracker/tracker-assist/' +then + echo "tracker-assist" + cd tracker/tracker-assist + + npm run lint-front + + cd ../../ +fi + +exit 0 diff --git a/tracker/tracker-assist/.eslintignore b/tracker/tracker-assist/.eslintignore new file mode 100644 index 000000000..94b2f339c --- /dev/null +++ b/tracker/tracker-assist/.eslintignore @@ -0,0 +1,8 @@ +node_modules +npm-debug.log +lib +cjs +build +.cache +.eslintrc.cjs +src/common/messages.ts diff --git a/tracker/tracker-assist/.eslintrc.cjs b/tracker/tracker-assist/.eslintrc.cjs new file mode 100644 index 000000000..01d5c5bc0 --- /dev/null +++ b/tracker/tracker-assist/.eslintrc.cjs @@ -0,0 +1,49 @@ +/* eslint-disable */ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname, + }, + plugins: ['@typescript-eslint', 'prettier'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'prettier', + ], + rules: { + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, + }, + ], + '@typescript-eslint/camelcase': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/unbound-method': 'off', + '@typescript-eslint/explicit-function-return-type': 'warn', + '@typescript-eslint/prefer-readonly': 'warn', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/restrict-plus-operands': 'warn', + '@typescript-eslint/no-unsafe-return': 'warn', + 'no-useless-escape': 'warn', + 'no-control-regex': 'warn', + '@typescript-eslint/restrict-template-expressions': 'warn', + '@typescript-eslint/no-useless-constructor': 'warn', + '@typescript-eslint/no-this-alias': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': 'warn', + '@typescript-eslint/no-useless-constructor': 'warn', + 'semi': ["error", "never"], + 'quotes': ["error", "single"], + 'comma-dangle': ["error", "always"] + }, +}; diff --git a/tracker/tracker-assist/package.json b/tracker/tracker-assist/package.json index 3d363cffd..3d3e13f93 100644 --- a/tracker/tracker-assist/package.json +++ b/tracker/tracker-assist/package.json @@ -13,7 +13,7 @@ "type": "module", "main": "./lib/index.js", "scripts": { - "lint": "prettier --write 'src/**/*.ts' README.md && tsc --noEmit", + "lint": "eslint src --ext .ts,.js --fix --quiet", "build": "npm run build-es && npm run build-cjs", "build-es": "rm -Rf lib && tsc && npm run replace-versions", "build-cjs": "rm -Rf cjs && tsc --project tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > cjs/package.json && npm run replace-paths && npm run replace-versions", @@ -21,7 +21,9 @@ "replace-versions": "npm run replace-pkg-version && npm run replace-req-version", "replace-pkg-version": "replace-in-files lib/* cjs/* --string='PACKAGE_VERSION' --replacement=$npm_package_version", "replace-req-version": "replace-in-files lib/* cjs/* --string='REQUIRED_TRACKER_VERSION' --replacement='3.5.14'", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "prepare": "cd ../../ && husky install tracker/.husky/", + "lint-front": "lint-staged" }, "dependencies": { "csstype": "^3.0.10", @@ -32,9 +34,26 @@ "@openreplay/tracker": "^3.5.3" }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.30.0", + "@typescript-eslint/parser": "^5.30.0", + "eslint": "^7.8.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "husky": "^8.0.1", + "lint-staged": "^13.0.3", + "prettier": "^2.7.1", "@openreplay/tracker": "file:../tracker", - "prettier": "^2.7.0", "replace-in-files-cli": "^1.0.0", "typescript": "^4.6.0-dev.20211126" + }, + "husky": { + "hooks": { + "pre-commit": "sh lint.sh" + } + }, + "lint-staged": { + "*.{js,mjs,cjs,jsx,ts,tsx}": [ + "eslint --fix --quiet" + ] } } diff --git a/tracker/tracker-assist/src/AnnotationCanvas.ts b/tracker/tracker-assist/src/AnnotationCanvas.ts index 11a06a781..1341045f6 100644 --- a/tracker/tracker-assist/src/AnnotationCanvas.ts +++ b/tracker/tracker-assist/src/AnnotationCanvas.ts @@ -1,14 +1,14 @@ export default class AnnotationCanvas { private canvas: HTMLCanvasElement private ctx: CanvasRenderingContext2D | null = null - private painting: boolean = false + private painting = false constructor() { this.canvas = document.createElement('canvas') Object.assign(this.canvas.style, { - position: "fixed", + position: 'fixed', left: 0, top: 0, - pointerEvents: "none", + pointerEvents: 'none', zIndex: 2147483647 - 2, }) } @@ -18,7 +18,7 @@ export default class AnnotationCanvas { this.canvas.height = window.innerHeight } - private lastPosition: [number, number] = [0,0] + private lastPosition: [number, number] = [0,0,] start = (p: [number, number]) => { this.painting = true this.clrTmID && clearTimeout(this.clrTmID) @@ -38,9 +38,9 @@ export default class AnnotationCanvas { this.ctx.moveTo(this.lastPosition[0], this.lastPosition[1]) this.ctx.lineTo(p[0], p[1]) this.ctx.lineWidth = 8 - this.ctx.lineCap = "round" - this.ctx.lineJoin = "round" - this.ctx.strokeStyle = "red" + this.ctx.lineCap = 'round' + this.ctx.lineJoin = 'round' + this.ctx.strokeStyle = 'red' this.ctx.stroke() this.lastPosition = p } @@ -51,7 +51,7 @@ export default class AnnotationCanvas { const fadeStep = () => { if (!this.ctx || this.painting ) { return } this.ctx.globalCompositeOperation = 'destination-out' - this.ctx.fillStyle = "rgba(255, 255, 255, 0.1)" + this.ctx.fillStyle = 'rgba(255, 255, 255, 0.1)' this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height) this.ctx.globalCompositeOperation = 'source-over' timeoutID = setTimeout(fadeStep,100) @@ -67,8 +67,8 @@ export default class AnnotationCanvas { mount() { document.body.appendChild(this.canvas) - this.ctx = this.canvas.getContext("2d") - window.addEventListener("resize", this.resizeCanvas) + this.ctx = this.canvas.getContext('2d') + window.addEventListener('resize', this.resizeCanvas) this.resizeCanvas() } @@ -76,6 +76,6 @@ export default class AnnotationCanvas { if (this.canvas.parentNode){ this.canvas.parentNode.removeChild(this.canvas) } - window.removeEventListener("resize", this.resizeCanvas) + window.removeEventListener('resize', this.resizeCanvas) } } \ No newline at end of file diff --git a/tracker/tracker-assist/src/Assist.ts b/tracker/tracker-assist/src/Assist.ts index a36c307db..eec9a66d7 100644 --- a/tracker/tracker-assist/src/Assist.ts +++ b/tracker/tracker-assist/src/Assist.ts @@ -1,20 +1,21 @@ -import type { Socket } from 'socket.io-client'; -import { connect } from 'socket.io-client'; -import Peer from 'peerjs'; -import type { Properties } from 'csstype'; -import { App } from '@openreplay/tracker'; +/* eslint-disable @typescript-eslint/no-empty-function */ +import type { Socket, } from 'socket.io-client' +import { connect, } from 'socket.io-client' +import Peer from 'peerjs' +import type { Properties, } from 'csstype' +import { App, } from '@openreplay/tracker' -import RequestLocalStream from './LocalStream.js'; -import RemoteControl from './RemoteControl.js'; -import CallWindow from './CallWindow.js'; -import AnnotationCanvas from './AnnotationCanvas.js'; -import ConfirmWindow from './ConfirmWindow/ConfirmWindow.js'; -import { callConfirmDefault } from './ConfirmWindow/defaults.js'; -import type { Options as ConfirmOptions } from './ConfirmWindow/defaults.js'; +import RequestLocalStream from './LocalStream.js' +import RemoteControl from './RemoteControl.js' +import CallWindow from './CallWindow.js' +import AnnotationCanvas from './AnnotationCanvas.js' +import ConfirmWindow from './ConfirmWindow/ConfirmWindow.js' +import { callConfirmDefault, } from './ConfirmWindow/defaults.js' +import type { Options as ConfirmOptions, } from './ConfirmWindow/defaults.js' // TODO: fully specified strict check (everywhere) -type StartEndCallback = () => ((()=>{}) | void) +type StartEndCallback = () => ((()=>Record) | void) export interface Options { onAgentConnect: StartEndCallback, @@ -40,7 +41,7 @@ enum CallingState { // TODO typing???? -type OptionalCallback = (()=>{}) | void +type OptionalCallback = (()=>Record) | void type Agent = { onDisconnect?: OptionalCallback, onControlReleased?: OptionalCallback, @@ -49,11 +50,11 @@ type Agent = { } export default class Assist { - readonly version = "PACKAGE_VERSION" + readonly version = 'PACKAGE_VERSION' private socket: Socket | null = null private peer: Peer | null = null - private assistDemandedRestart: boolean = false + private assistDemandedRestart = false private callingState: CallingState = CallingState.False private agents: Record = {} @@ -64,8 +65,8 @@ export default class Assist { private readonly noSecureMode: boolean = false, ) { this.options = Object.assign({ - session_calling_peer_key: "__openreplay_calling_peer", - session_control_peer_key: "__openreplay_control_peer", + session_calling_peer_key: '__openreplay_calling_peer', + session_control_peer_key: '__openreplay_control_peer', config: null, onCallStart: ()=>{}, onAgentConnect: ()=>{}, @@ -74,10 +75,10 @@ export default class Assist { controlConfirm: {}, // TODO: clear options passing/merging/overriting }, options, - ); + ) if (document.hidden !== undefined) { - const sendActivityState = () => this.emit("UPDATE_SESSION", { active: !document.hidden }) + const sendActivityState = () => this.emit('UPDATE_SESSION', { active: !document.hidden, }) app.attachEventListener( document, 'visibilitychange', @@ -88,15 +89,15 @@ export default class Assist { } const titleNode = document.querySelector('title') const observer = titleNode && new MutationObserver(() => { - this.emit("UPDATE_SESSION", { pageTitle: document.title }) + this.emit('UPDATE_SESSION', { pageTitle: document.title, }) }) app.attachStartCallback(() => { - if (this.assistDemandedRestart) { return; } + if (this.assistDemandedRestart) { return } this.onStart() - observer && observer.observe(titleNode, { subtree: true, characterData: true, childList: true }) + observer && observer.observe(titleNode, { subtree: true, characterData: true, childList: true, }) }) app.attachStopCallback(() => { - if (this.assistDemandedRestart) { return; } + if (this.assistDemandedRestart) { return } this.clean() observer && observer.disconnect() }) @@ -104,10 +105,10 @@ export default class Assist { if (this.agentsConnected) { // @ts-ignore No need in statistics messages. TODO proper filter if (messages.length === 2 && messages[0]._id === 0 && messages[1]._id === 49) { return } - this.emit("messages", messages) + this.emit('messages', messages) } }) - app.session.attachUpdateCallback(sessInfo => this.emit("UPDATE_SESSION", sessInfo)) + app.session.attachUpdateCallback(sessInfo => this.emit('UPDATE_SESSION', sessInfo)) } private emit(ev: string, ...args) { @@ -119,7 +120,7 @@ export default class Assist { } private notifyCallEnd() { - this.emit("call_end"); + this.emit('call_end') } private onRemoteCallEnd = () => {} @@ -131,17 +132,17 @@ export default class Assist { const socket = this.socket = connect(app.getHost(), { path: '/ws-assist/socket', query: { - "peerId": peerID, - "identity": "session", - "sessionInfo": JSON.stringify({ + 'peerId': peerID, + 'identity': 'session', + 'sessionInfo': JSON.stringify({ pageTitle: document.title, active: true, - ...this.app.getSessionInfo() + ...this.app.getSessionInfo(), }), }, - transports: ["websocket"], + transports: ['websocket',], }) - socket.onAny((...args) => app.debug.log("Socket:", ...args)) + socket.onAny((...args) => app.debug.log('Socket:', ...args)) @@ -149,15 +150,15 @@ export default class Assist { this.options, id => { this.agents[id].onControlReleased = this.options.onRemoteControlStart() - this.emit("control_granted", id) + this.emit('control_granted', id) annot = new AnnotationCanvas() annot.mount() }, id => { const cb = this.agents[id].onControlReleased delete this.agents[id].onControlReleased - typeof cb === "function" && cb() - this.emit("control_rejected", id) + typeof cb === 'function' && cb() + this.emit('control_rejected', id) if (annot != null) { annot.remove() annot = null @@ -166,41 +167,41 @@ export default class Assist { ) // TODO: check incoming args - socket.on("request_control", remoteControl.requestControl) - socket.on("release_control", remoteControl.releaseControl) - socket.on("scroll", remoteControl.scroll) - socket.on("click", remoteControl.click) - socket.on("move", remoteControl.move) - socket.on("focus", (clientID, nodeID) => { + socket.on('request_control', remoteControl.requestControl) + socket.on('release_control', remoteControl.releaseControl) + socket.on('scroll', remoteControl.scroll) + socket.on('click', remoteControl.click) + socket.on('move', remoteControl.move) + socket.on('focus', (clientID, nodeID) => { const el = app.nodes.getNode(nodeID) if (el instanceof HTMLElement) { remoteControl.focus(clientID, el) } }) - socket.on("input", remoteControl.input) + socket.on('input', remoteControl.input) let annot: AnnotationCanvas | null = null - socket.on("moveAnnotation", (_, p) => annot && annot.move(p)) // TODO: restrict by id - socket.on("startAnnotation", (_, p) => annot && annot.start(p)) - socket.on("stopAnnotation", () => annot && annot.stop()) + socket.on('moveAnnotation', (_, p) => annot && annot.move(p)) // TODO: restrict by id + socket.on('startAnnotation', (_, p) => annot && annot.start(p)) + socket.on('stopAnnotation', () => annot && annot.stop()) - socket.on("NEW_AGENT", (id: string, info) => { + socket.on('NEW_AGENT', (id: string, info) => { this.agents[id] = { onDisconnect: this.options.onAgentConnect && this.options.onAgentConnect(), ...info, // TODO } this.assistDemandedRestart = true - this.app.stop(); + this.app.stop() this.app.start().then(() => { this.assistDemandedRestart = false }) }) - socket.on("AGENTS_CONNECTED", (ids: string[]) => { + socket.on('AGENTS_CONNECTED', (ids: string[]) => { ids.forEach(id =>{ this.agents[id] = { onDisconnect: this.options.onAgentConnect && this.options.onAgentConnect(), } }) this.assistDemandedRestart = true - this.app.stop(); + this.app.stop() this.app.start().then(() => { this.assistDemandedRestart = false }) remoteControl.reconnect(ids) @@ -208,7 +209,7 @@ export default class Assist { let confirmCall:ConfirmWindow | null = null - socket.on("AGENT_DISCONNECTED", (id) => { + socket.on('AGENT_DISCONNECTED', (id) => { remoteControl.releaseControl(id) // close the call also @@ -221,15 +222,15 @@ export default class Assist { this.agents[id] && this.agents[id].onDisconnect != null && this.agents[id].onDisconnect() delete this.agents[id] }) - socket.on("NO_AGENT", () => { + socket.on('NO_AGENT', () => { this.agents = {} }) - socket.on("call_end", () => this.onRemoteCallEnd()) // TODO: check if agent calling id + socket.on('call_end', () => this.onRemoteCallEnd()) // TODO: check if agent calling id // TODO: fix the code - let agentName = "" - let callingAgent = "" - socket.on("_agent_name",(id, name) => { agentName = name; callingAgent = id }) + let agentName = '' + let callingAgent = '' + socket.on('_agent_name',(id, name) => { agentName = name; callingAgent = id }) // PeerJS call (todo: use native WebRTC) @@ -242,27 +243,27 @@ export default class Assist { if (this.options.config) { peerOptions['config'] = this.options.config } - const peer = this.peer = new Peer(peerID, peerOptions); + const peer = this.peer = new Peer(peerID, peerOptions) // app.debug.log('Peer created: ', peer) // @ts-ignore - peer.on('error', e => app.debug.warn("Peer error: ", e.type, e)) + peer.on('error', e => app.debug.warn('Peer error: ', e.type, e)) peer.on('disconnected', () => peer.reconnect()) peer.on('call', (call) => { - app.debug.log("Call: ", call) + app.debug.log('Call: ', call) if (this.callingState !== CallingState.False) { call.close() //this.notifyCallEnd() // TODO: strictly connect calling peer with agent socket.id - app.debug.warn("Call closed instantly bacause line is busy. CallingState: ", this.callingState) - return; + app.debug.warn('Call closed instantly bacause line is busy. CallingState: ', this.callingState) + return } const setCallingState = (newState: CallingState) => { if (newState === CallingState.True) { - sessionStorage.setItem(this.options.session_calling_peer_key, call.peer); + sessionStorage.setItem(this.options.session_calling_peer_key, call.peer) } else if (newState === CallingState.False) { - sessionStorage.removeItem(this.options.session_calling_peer_key); + sessionStorage.removeItem(this.options.session_calling_peer_key) } - this.callingState = newState; + this.callingState = newState } let confirmAnswer: Promise @@ -278,7 +279,7 @@ export default class Assist { confirmAnswer = confirmCall.mount() this.playNotificationSound() this.onRemoteCallEnd = () => { // if call cancelled by a caller before confirmation - app.debug.log("Received call_end during confirm window opened") + app.debug.log('Received call_end during confirm window opened') confirmCall?.remove() setCallingState(CallingState.False) call.close() @@ -307,7 +308,7 @@ export default class Assist { const onCallEnd = this.options.onCallStart() const handleCallEnd = () => { - app.debug.log("Handle Call End") + app.debug.log('Handle Call End') call.close() callUI.remove() annot && annot.remove() @@ -322,27 +323,27 @@ export default class Assist { this.onRemoteCallEnd = handleCallEnd call.on('error', e => { - app.debug.warn("Call error:", e) + app.debug.warn('Call error:', e) initiateCallEnd() - }); + }) RequestLocalStream().then(lStream => { call.on('stream', function(rStream) { - callUI.setRemoteStream(rStream); + callUI.setRemoteStream(rStream) const onInteraction = () => { // only if hidden? callUI.playRemote() - document.removeEventListener("click", onInteraction) + document.removeEventListener('click', onInteraction) } - document.addEventListener("click", onInteraction) - }); + document.addEventListener('click', onInteraction) + }) lStream.onVideoTrack(vTrack => { - const sender = call.peerConnection.getSenders().find(s => s.track?.kind === "video") + const sender = call.peerConnection.getSenders().find(s => s.track?.kind === 'video') if (!sender) { - app.debug.warn("No video sender found") + app.debug.warn('No video sender found') return } - app.debug.log("sender found:", sender) + app.debug.log('sender found:', sender) sender.replaceTrack(vTrack) }) @@ -352,16 +353,16 @@ export default class Assist { setCallingState(CallingState.True) }) .catch(e => { - app.debug.warn("Audio mediadevice request error:", e) + app.debug.warn('Audio mediadevice request error:', e) initiateCallEnd() - }); - }).catch(); // in case of Confirm.remove() without any confirmation/decline - }); + }) + }).catch() // in case of Confirm.remove() without any confirmation/decline + }) } private playNotificationSound() { if ('Audio' in window) { - new Audio("https://static.openreplay.com/tracker-assist/notification.mp3") + new Audio('https://static.openreplay.com/tracker-assist/notification.mp3') .play() .catch(e => { this.app.debug.warn(e) @@ -372,11 +373,11 @@ export default class Assist { private clean() { if (this.peer) { this.peer.destroy() - this.app.debug.log("Peer destroyed") + this.app.debug.log('Peer destroyed') } if (this.socket) { this.socket.disconnect() - this.app.debug.log("Socket disconnected") + this.app.debug.log('Socket disconnected') } } } diff --git a/tracker/tracker-assist/src/CallWindow.ts b/tracker/tracker-assist/src/CallWindow.ts index ae2bdd3fa..8804ffa3e 100644 --- a/tracker/tracker-assist/src/CallWindow.ts +++ b/tracker/tracker-assist/src/CallWindow.ts @@ -1,7 +1,7 @@ -import type { LocalStream } from './LocalStream.js'; -import attachDND from './dnd.js'; +import type { LocalStream, } from './LocalStream.js' +import attachDND from './dnd.js' -const SS_START_TS_KEY = "__openreplay_assist_call_start_ts" +const SS_START_TS_KEY = '__openreplay_assist_call_start_ts' export default class CallWindow { private iframe: HTMLIFrameElement @@ -21,66 +21,66 @@ export default class CallWindow { constructor() { const iframe = this.iframe = document.createElement('iframe') Object.assign(iframe.style, { - position: "fixed", + position: 'fixed', zIndex: 2147483647 - 1, - border: "none", - bottom: "10px", - right: "10px", - height: "200px", - width: "200px", + border: 'none', + bottom: '10px', + right: '10px', + height: '200px', + width: '200px', }) // TODO: find the best attribute name for the ignoring iframes - iframe.setAttribute("data-openreplay-obscured", "") - iframe.setAttribute("data-openreplay-hidden", "") - iframe.setAttribute("data-openreplay-ignore", "") + iframe.setAttribute('data-openreplay-obscured', '') + iframe.setAttribute('data-openreplay-hidden', '') + iframe.setAttribute('data-openreplay-ignore', '') document.body.appendChild(iframe) - const doc = iframe.contentDocument; + const doc = iframe.contentDocument if (!doc) { - console.error("OpenReplay: CallWindow iframe document is not reachable.") - return; + console.error('OpenReplay: CallWindow iframe document is not reachable.') + return } //const baseHref = "https://static.openreplay.com/tracker-assist/test" - const baseHref = "https://static.openreplay.com/tracker-assist/3.4.4" - this.load = fetch(baseHref + "/index.html") + const baseHref = 'https://static.openreplay.com/tracker-assist/3.4.4' + this.load = fetch(baseHref + '/index.html') .then(r => r.text()) .then((text) => { iframe.onload = () => { - const assistSection = doc.getElementById("or-assist") - assistSection?.classList.remove("status-connecting") + const assistSection = doc.getElementById('or-assist') + assistSection?.classList.remove('status-connecting') //iframe.style.height = doc.body.scrollHeight + 'px'; //iframe.style.width = doc.body.scrollWidth + 'px'; this.adjustIframeSize() - iframe.onload = null; + iframe.onload = null } // ? text = text.replace(/href="css/g, `href="${baseHref}/css`) - doc.open(); - doc.write(text); - doc.close(); + doc.open() + doc.write(text) + doc.close() - this.vLocal = doc.getElementById("video-local") as (HTMLVideoElement | null); - this.vRemote = doc.getElementById("video-remote") as (HTMLVideoElement | null); - this.videoContainer = doc.getElementById("video-container"); + this.vLocal = doc.getElementById('video-local') as (HTMLVideoElement | null) + this.vRemote = doc.getElementById('video-remote') as (HTMLVideoElement | null) + this.videoContainer = doc.getElementById('video-container') - this.audioBtn = doc.getElementById("audio-btn"); + this.audioBtn = doc.getElementById('audio-btn') if (this.audioBtn) { - this.audioBtn.onclick = () => this.toggleAudio(); + this.audioBtn.onclick = () => this.toggleAudio() } - this.videoBtn = doc.getElementById("video-btn"); + this.videoBtn = doc.getElementById('video-btn') if (this.videoBtn) { - this.videoBtn.onclick = () => this.toggleVideo(); + this.videoBtn.onclick = () => this.toggleVideo() } - this.endCallBtn = doc.getElementById("end-call-btn"); + this.endCallBtn = doc.getElementById('end-call-btn') - this.agentNameElem = doc.getElementById("agent-name"); - this.vPlaceholder = doc.querySelector("#remote-stream p") + this.agentNameElem = doc.getElementById('agent-name') + this.vPlaceholder = doc.querySelector('#remote-stream p') - const tsElem = doc.getElementById("duration"); + const tsElem = doc.getElementById('duration') if (tsElem) { const startTs = Number(sessionStorage.getItem(SS_START_TS_KEY)) || Date.now() sessionStorage.setItem(SS_START_TS_KEY, startTs.toString()) @@ -90,15 +90,15 @@ export default class CallWindow { const mins = ~~(secsFull / 60) const secs = secsFull - mins * 60 tsElem.innerText = `${mins}:${secs < 10 ? 0 : ''}${secs}` - }, 500); + }, 500) } - const dragArea = doc.querySelector(".drag-area") + const dragArea = doc.querySelector('.drag-area') if (dragArea) { // TODO: save coordinates on the new page attachDND(iframe, dragArea, doc.documentElement) } - }); + }) //this.toggleVideoUI(false) //this.toggleRemoteVideoUI(false) @@ -107,8 +107,8 @@ export default class CallWindow { private adjustIframeSize() { const doc = this.iframe.contentDocument if (!doc) { return } - this.iframe.style.height = doc.body.scrollHeight + 'px'; - this.iframe.style.width = doc.body.scrollWidth + 'px'; + this.iframe.style.height = doc.body.scrollHeight + 'px' + this.iframe.style.width = doc.body.scrollWidth + 'px' } setCallEndAction(endCall: () => void) { @@ -124,16 +124,16 @@ export default class CallWindow { setRemoteStream(rStream: MediaStream) { this.load.then(() => { if (this.vRemote && !this.vRemote.srcObject) { - this.vRemote.srcObject = rStream; + this.vRemote.srcObject = rStream if (this.vPlaceholder) { - this.vPlaceholder.innerText = "Video has been paused. Click anywhere to resume."; + this.vPlaceholder.innerText = 'Video has been paused. Click anywhere to resume.' } // Hack for audio. Doesen't work inside the iframe because of some magical reasons (check if it is connected to autoplay?) - this.aRemote = document.createElement("audio"); - this.aRemote.autoplay = true; - this.aRemote.style.display = "none" - this.aRemote.srcObject = rStream; + this.aRemote = document.createElement('audio') + this.aRemote.autoplay = true + this.aRemote.style.display = 'none' + this.aRemote.srcObject = rStream document.body.appendChild(this.aRemote) } @@ -156,9 +156,9 @@ export default class CallWindow { this.load.then(() => { if (this.videoContainer) { if (enable) { - this.videoContainer.classList.add("remote") + this.videoContainer.classList.add('remote') } else { - this.videoContainer.classList.remove("remote") + this.videoContainer.classList.remove('remote') } this.adjustIframeSize() } @@ -186,11 +186,11 @@ export default class CallWindow { private toggleAudioUI(enabled: boolean) { - if (!this.audioBtn) { return; } + if (!this.audioBtn) { return } if (enabled) { - this.audioBtn.classList.remove("muted") + this.audioBtn.classList.remove('muted') } else { - this.audioBtn.classList.add("muted") + this.audioBtn.classList.add('muted') } } @@ -200,18 +200,18 @@ export default class CallWindow { } private toggleVideoUI(enabled: boolean) { - if (!this.videoBtn || !this.videoContainer) { return; } + if (!this.videoBtn || !this.videoContainer) { return } if (enabled) { - this.videoContainer.classList.add("local") - this.videoBtn.classList.remove("off"); + this.videoContainer.classList.add('local') + this.videoBtn.classList.remove('off') } else { - this.videoContainer.classList.remove("local") - this.videoBtn.classList.add("off"); + this.videoContainer.classList.remove('local') + this.videoBtn.classList.add('off') } this.adjustIframeSize() } - private videoRequested: boolean = false + private videoRequested = false private toggleVideo() { this.localStream?.toggleVideo() .then(enabled => { diff --git a/tracker/tracker-assist/src/ConfirmWindow/ConfirmWindow.ts b/tracker/tracker-assist/src/ConfirmWindow/ConfirmWindow.ts index 02d9dd9c6..fd7209689 100644 --- a/tracker/tracker-assist/src/ConfirmWindow/ConfirmWindow.ts +++ b/tracker/tracker-assist/src/ConfirmWindow/ConfirmWindow.ts @@ -1,4 +1,5 @@ -import type { Properties } from 'csstype'; +/* eslint-disable @typescript-eslint/no-empty-function */ +import type { Properties, } from 'csstype' export type ButtonOptions = | HTMLButtonElement @@ -19,45 +20,45 @@ export interface ConfirmWindowOptions { function makeButton(options: ButtonOptions, defaultStyle?: Properties): HTMLButtonElement { if (options instanceof HTMLButtonElement) { - return options; + return options } - const btn = document.createElement("button"); + const btn = document.createElement('button') Object.assign(btn.style, { - padding: "10px 14px", - fontSize: "14px", - borderRadius: "3px", - border: "none", - cursor: "pointer", - display: "flex", - alignItems: "center", - textTransform: "uppercase", - marginRight: "10px" - }, defaultStyle); - if (typeof options === "string") { - btn.innerHTML = options; + padding: '10px 14px', + fontSize: '14px', + borderRadius: '3px', + border: 'none', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + textTransform: 'uppercase', + marginRight: '10px', + }, defaultStyle) + if (typeof options === 'string') { + btn.innerHTML = options } else { - btn.innerHTML = options.innerHTML; - Object.assign(btn.style, options.style); + btn.innerHTML = options.innerHTML + Object.assign(btn.style, options.style) } - return btn; + return btn } export default class ConfirmWindow { private wrapper: HTMLDivElement; constructor(options: ConfirmWindowOptions) { - const wrapper = document.createElement("div"); - const popup = document.createElement("div"); - const p = document.createElement("p"); - p.innerText = options.text; - const buttons = document.createElement("div"); + const wrapper = document.createElement('div') + const popup = document.createElement('div') + const p = document.createElement('p') + p.innerText = options.text + const buttons = document.createElement('div') const confirmBtn = makeButton(options.confirmBtn, { - background: "rgba(0, 167, 47, 1)", - color: "white" + background: 'rgba(0, 167, 47, 1)', + color: 'white', }) const declineBtn = makeButton(options.declineBtn, { - background: "#FFE9E9", - color: "#CC0000" + background: '#FFE9E9', + color: '#CC0000', }) buttons.appendChild(confirmBtn) buttons.appendChild(declineBtn) @@ -66,78 +67,78 @@ export default class ConfirmWindow { Object.assign(buttons.style, { - marginTop: "10px", - display: "flex", - alignItems: "center", + marginTop: '10px', + display: 'flex', + alignItems: 'center', // justifyContent: "space-evenly", - backgroundColor: "white", - padding: "10px", - boxShadow: "0px 0px 3.99778px 1.99889px rgba(0, 0, 0, 0.1)", - borderRadius: "6px" - }); + backgroundColor: 'white', + padding: '10px', + boxShadow: '0px 0px 3.99778px 1.99889px rgba(0, 0, 0, 0.1)', + borderRadius: '6px', + }) Object.assign( popup.style, { - font: "14px 'Roboto', sans-serif", - position: "relative", - pointerEvents: "auto", - margin: "4em auto", - width: "90%", - maxWidth: "fit-content", - padding: "20px", - background: "#F3F3F3", + font: '14px \'Roboto\', sans-serif', + position: 'relative', + pointerEvents: 'auto', + margin: '4em auto', + width: '90%', + maxWidth: 'fit-content', + padding: '20px', + background: '#F3F3F3', //opacity: ".75", - color: "black", - borderRadius: "3px", - boxShadow: "0px 0px 3.99778px 1.99889px rgba(0, 0, 0, 0.1)" + color: 'black', + borderRadius: '3px', + boxShadow: '0px 0px 3.99778px 1.99889px rgba(0, 0, 0, 0.1)', }, options.style - ); + ) Object.assign(wrapper.style, { - position: "fixed", + position: 'fixed', left: 0, top: 0, - height: "100%", - width: "100%", - pointerEvents: "none", - zIndex: 2147483647 - 1 - }); + height: '100%', + width: '100%', + pointerEvents: 'none', + zIndex: 2147483647 - 1, + }) - wrapper.appendChild(popup); - this.wrapper = wrapper; + wrapper.appendChild(popup) + this.wrapper = wrapper confirmBtn.onclick = () => { - this._remove(); - this.resolve(true); - }; + this._remove() + this.resolve(true) + } declineBtn.onclick = () => { - this._remove(); - this.resolve(false); - }; + this._remove() + this.resolve(false) + } } private resolve: (result: boolean) => void = () => {}; private reject: () => void = () => {}; mount(): Promise { - document.body.appendChild(this.wrapper); + document.body.appendChild(this.wrapper) return new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); + this.resolve = resolve + this.reject = reject + }) } private _remove() { if (!this.wrapper.parentElement) { - return; + return } - document.body.removeChild(this.wrapper); + document.body.removeChild(this.wrapper) } remove() { - this._remove(); - this.reject(); + this._remove() + this.reject() } } diff --git a/tracker/tracker-assist/src/ConfirmWindow/defaults.ts b/tracker/tracker-assist/src/ConfirmWindow/defaults.ts index 8f84cbe89..d6d5430c4 100644 --- a/tracker/tracker-assist/src/ConfirmWindow/defaults.ts +++ b/tracker/tracker-assist/src/ConfirmWindow/defaults.ts @@ -1,10 +1,10 @@ -import { declineCall, acceptCall, cross, remoteControl } from '../icons.js' -import type { ButtonOptions, ConfirmWindowOptions } from './ConfirmWindow.js' +import { declineCall, acceptCall, cross, remoteControl, } from '../icons.js' +import type { ButtonOptions, ConfirmWindowOptions, } from './ConfirmWindow.js' -const TEXT_GRANT_REMORTE_ACCESS = "Grant Remote Control"; -const TEXT_REJECT = "Reject"; -const TEXT_ANSWER_CALL = `${acceptCall}   Answer`; +const TEXT_GRANT_REMORTE_ACCESS = 'Grant Remote Control' +const TEXT_REJECT = 'Reject' +const TEXT_ANSWER_CALL = `${acceptCall}   Answer` export type Options = string | Partial; @@ -14,15 +14,15 @@ function confirmDefault( declineBtn: ButtonOptions, text: string ): ConfirmWindowOptions { - const isStr = typeof opts === "string"; + const isStr = typeof opts === 'string' return Object.assign( { text: isStr ? opts : text, confirmBtn, - declineBtn + declineBtn, }, isStr ? undefined : opts - ); + ) } export const callConfirmDefault = (opts: Options) => @@ -30,7 +30,7 @@ export const callConfirmDefault = (opts: Options) => opts, TEXT_ANSWER_CALL, TEXT_REJECT, - "You have an incoming call. Do you want to answer?" + 'You have an incoming call. Do you want to answer?' ) export const controlConfirmDefault = (opts: Options) => @@ -38,5 +38,5 @@ export const controlConfirmDefault = (opts: Options) => opts, TEXT_GRANT_REMORTE_ACCESS, TEXT_REJECT, - "Agent requested remote control. Allow?" + 'Agent requested remote control. Allow?' ) diff --git a/tracker/tracker-assist/src/LocalStream.ts b/tracker/tracker-assist/src/LocalStream.ts index 63f01ad58..7f233108a 100644 --- a/tracker/tracker-assist/src/LocalStream.ts +++ b/tracker/tracker-assist/src/LocalStream.ts @@ -5,44 +5,44 @@ declare global { } function dummyTrack(): MediaStreamTrack { - const canvas = document.createElement("canvas")//, { width: 0, height: 0}) + const canvas = document.createElement('canvas')//, { width: 0, height: 0}) canvas.width=canvas.height=2 // Doesn't work when 1 (?!) - const ctx = canvas.getContext('2d'); - ctx?.fillRect(0, 0, canvas.width, canvas.height); + const ctx = canvas.getContext('2d') + ctx?.fillRect(0, 0, canvas.width, canvas.height) requestAnimationFrame(function draw(){ ctx?.fillRect(0,0, canvas.width, canvas.height) - requestAnimationFrame(draw); - }); + requestAnimationFrame(draw) + }) // Also works. Probably it should be done once connected. //setTimeout(() => { ctx?.fillRect(0,0, canvas.width, canvas.height) }, 4000) - return canvas.captureStream(60).getTracks()[0]; + return canvas.captureStream(60).getTracks()[0] } export default function RequestLocalStream(): Promise { - return navigator.mediaDevices.getUserMedia({ audio:true }) + return navigator.mediaDevices.getUserMedia({ audio:true, }) .then(aStream => { const aTrack = aStream.getAudioTracks()[0] - if (!aTrack) { throw new Error("No audio tracks provided") } + if (!aTrack) { throw new Error('No audio tracks provided') } return new _LocalStream(aTrack) }) } class _LocalStream { - private mediaRequested: boolean = false + private mediaRequested = false readonly stream: MediaStream private readonly vdTrack: MediaStreamTrack constructor(aTrack: MediaStreamTrack) { this.vdTrack = dummyTrack() - this.stream = new MediaStream([ aTrack, this.vdTrack ]) + this.stream = new MediaStream([ aTrack, this.vdTrack, ]) } toggleVideo(): Promise { if (!this.mediaRequested) { - return navigator.mediaDevices.getUserMedia({video:true}) + return navigator.mediaDevices.getUserMedia({video:true,}) .then(vStream => { const vTrack = vStream.getVideoTracks()[0] if (!vTrack) { - throw new Error("No video track provided") + throw new Error('No video track provided') } this.stream.addTrack(vTrack) this.stream.removeTrack(this.vdTrack) diff --git a/tracker/tracker-assist/src/Mouse.ts b/tracker/tracker-assist/src/Mouse.ts index 52858ccf6..a6164e153 100644 --- a/tracker/tracker-assist/src/Mouse.ts +++ b/tracker/tracker-assist/src/Mouse.ts @@ -3,24 +3,24 @@ type XY = [number, number] export default class Mouse { private mouse: HTMLDivElement - private position: [number,number] = [0,0] + private position: [number,number] = [0,0,] constructor() { - this.mouse = document.createElement('div'); + this.mouse = document.createElement('div') Object.assign(this.mouse.style, { - width: "20px", - height: "20px", - opacity: ".4", - borderRadius: "50%", - position: "absolute", - zIndex: "999998", - background: "radial-gradient(red, transparent)", - }); + width: '20px', + height: '20px', + opacity: '.4', + borderRadius: '50%', + position: 'absolute', + zIndex: '999998', + background: 'radial-gradient(red, transparent)', + }) } mount() { document.body.appendChild(this.mouse) - window.addEventListener("scroll", this.handleWScroll) - window.addEventListener("resize", this.resetLastScrEl) + window.addEventListener('scroll', this.handleWScroll) + window.addEventListener('resize', this.resetLastScrEl) } move(pos: XY) { @@ -28,16 +28,16 @@ export default class Mouse { this.resetLastScrEl() } - this.position = pos; + this.position = pos Object.assign(this.mouse.style, { left: `${pos[0] || 0}px`, - top: `${pos[1] || 0}px` + top: `${pos[1] || 0}px`, }) } getPosition(): XY { - return this.position; + return this.position } click(pos: XY) { @@ -51,18 +51,18 @@ export default class Mouse { } private readonly pScrEl = document.scrollingElement || document.documentElement // Is it always correct - private lastScrEl: Element | "window" | null = null + private lastScrEl: Element | 'window' | null = null private resetLastScrEl = () => { this.lastScrEl = null } private handleWScroll = e => { if (e.target !== this.lastScrEl && - this.lastScrEl !== "window") { + this.lastScrEl !== 'window') { this.resetLastScrEl() } } scroll(delta: XY) { // what would be the browser-like logic? - const [mouseX, mouseY] = this.position - const [dX, dY] = delta + const [mouseX, mouseY,] = this.position + const [dX, dY,] = delta let el = this.lastScrEl @@ -72,7 +72,7 @@ export default class Mouse { el.scrollTop += dY return // TODO: if not scrolled } - if (el === "window") { + if (el === 'window') { window.scroll(this.pScrEl.scrollLeft + dX, this.pScrEl.scrollTop + dY) return } @@ -85,7 +85,7 @@ export default class Mouse { // el.scrollTopMax > 0 // available in firefox if (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth) { const styles = getComputedStyle(el) - if (styles.overflow.indexOf("scroll") >= 0 || styles.overflow.indexOf("auto") >= 0) { // returns true for body in habr.com but it's not scrollable + if (styles.overflow.indexOf('scroll') >= 0 || styles.overflow.indexOf('auto') >= 0) { // returns true for body in habr.com but it's not scrollable const esl = el.scrollLeft const est = el.scrollTop el.scrollLeft += dX @@ -101,14 +101,14 @@ export default class Mouse { // If not scrolled window.scroll(this.pScrEl.scrollLeft + dX, this.pScrEl.scrollTop + dY) - this.lastScrEl = "window" + this.lastScrEl = 'window' } remove() { if (this.mouse.parentElement) { - document.body.removeChild(this.mouse); + document.body.removeChild(this.mouse) } - window.removeEventListener("scroll", this.handleWScroll) - window.removeEventListener("resize", this.resetLastScrEl) + window.removeEventListener('scroll', this.handleWScroll) + window.removeEventListener('resize', this.resetLastScrEl) } } \ No newline at end of file diff --git a/tracker/tracker-assist/src/RemoteControl.ts b/tracker/tracker-assist/src/RemoteControl.ts index 50cb717c8..4cbc785f3 100644 --- a/tracker/tracker-assist/src/RemoteControl.ts +++ b/tracker/tracker-assist/src/RemoteControl.ts @@ -1,7 +1,7 @@ -import Mouse from './Mouse.js'; -import ConfirmWindow from './ConfirmWindow/ConfirmWindow.js'; -import { controlConfirmDefault } from './ConfirmWindow/defaults.js'; -import type { Options as AssistOptions } from './Assist'; +import Mouse from './Mouse.js' +import ConfirmWindow from './ConfirmWindow/ConfirmWindow.js' +import { controlConfirmDefault, } from './ConfirmWindow/defaults.js' +import type { Options as AssistOptions, } from './Assist' enum RCStatus { Disabled, @@ -11,7 +11,7 @@ enum RCStatus { let setInputValue = function(this: HTMLInputElement | HTMLTextAreaElement, value: string) { this.value = value } -const nativeInputValueDescriptor = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value") +const nativeInputValueDescriptor = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value') if (nativeInputValueDescriptor && nativeInputValueDescriptor.set) { setInputValue = nativeInputValueDescriptor.set } @@ -93,7 +93,7 @@ export default class RemoteControl { if (this.focused instanceof HTMLTextAreaElement || this.focused instanceof HTMLInputElement) { setInputValue.call(this.focused, value) - const ev = new Event('input', { bubbles: true}) + const ev = new Event('input', { bubbles: true,}) this.focused.dispatchEvent(ev) } else if (this.focused.isContentEditable) { this.focused.innerText = value diff --git a/tracker/tracker-assist/src/_slim.ts b/tracker/tracker-assist/src/_slim.ts index ce86863be..760a9b8d2 100644 --- a/tracker/tracker-assist/src/_slim.ts +++ b/tracker/tracker-assist/src/_slim.ts @@ -5,4 +5,4 @@ */ // @ts-ignore -typeof window !== "undefined" && (window.parcelRequire = window.parcelRequire || undefined); +typeof window !== 'undefined' && (window.parcelRequire = window.parcelRequire || undefined) diff --git a/tracker/tracker-assist/src/dnd.ts b/tracker/tracker-assist/src/dnd.ts index 818bb0b89..622d86b94 100644 --- a/tracker/tracker-assist/src/dnd.ts +++ b/tracker/tracker-assist/src/dnd.ts @@ -9,7 +9,7 @@ export default function attachDND( dropArea: Element, ) { - dragArea.addEventListener('pointerdown', userPressed, { passive: true }) + dragArea.addEventListener('pointerdown', userPressed, { passive: true, }) let bbox, startX, startY, @@ -20,9 +20,9 @@ export default function attachDND( startX = event.clientX startY = event.clientY bbox = movingEl.getBoundingClientRect() - dropArea.addEventListener('pointermove', userMoved, { passive: true }) - dropArea.addEventListener('pointerup', userReleased, { passive: true }) - dropArea.addEventListener('pointercancel', userReleased, { passive: true }) + dropArea.addEventListener('pointermove', userMoved, { passive: true, }) + dropArea.addEventListener('pointerup', userReleased, { passive: true, }) + dropArea.addEventListener('pointercancel', userReleased, { passive: true, }) }; /* @@ -46,8 +46,8 @@ export default function attachDND( } function userMovedRaf() { - movingEl.style.transform = "translate3d("+deltaX+"px,"+deltaY+"px, 0px)"; - raf = null; + movingEl.style.transform = 'translate3d('+deltaX+'px,'+deltaY+'px, 0px)' + raf = null } function userReleased() { @@ -58,9 +58,9 @@ export default function attachDND( cancelAnimationFrame(raf) raf = null } - movingEl.style.left = bbox.left + deltaX + "px" - movingEl.style.top = bbox.top + deltaY + "px" - movingEl.style.transform = "translate3d(0px,0px,0px)" + movingEl.style.left = bbox.left + deltaX + 'px' + movingEl.style.top = bbox.top + deltaY + 'px' + movingEl.style.transform = 'translate3d(0px,0px,0px)' deltaX = deltaY = 0 } } \ No newline at end of file diff --git a/tracker/tracker-assist/src/icons.ts b/tracker/tracker-assist/src/icons.ts index 763b015b9..c844812f4 100644 --- a/tracker/tracker-assist/src/icons.ts +++ b/tracker/tracker-assist/src/icons.ts @@ -4,7 +4,7 @@ export const declineCall = ` -`; +` export const acceptCall = declineCall.replace('fill="#ef5261"', 'fill="green"') @@ -13,4 +13,4 @@ export const cross = ` ` -export const remoteControl = `` +export const remoteControl = '' diff --git a/tracker/tracker-assist/src/index.ts b/tracker/tracker-assist/src/index.ts index 2a3161089..32af38d3f 100644 --- a/tracker/tracker-assist/src/index.ts +++ b/tracker/tracker-assist/src/index.ts @@ -1,7 +1,7 @@ -import './_slim.js'; +import './_slim.js' -import type { App } from '@openreplay/tracker'; -import type { Options } from './Assist.js' +import type { App, } from '@openreplay/tracker' +import type { Options, } from './Assist.js' import Assist from './Assist.js' @@ -9,13 +9,13 @@ export default function(opts?: Partial) { return function(app: App | null, appOptions: { __DISABLE_SECURE_MODE?: boolean } = {}) { // @ts-ignore if (app === null || !navigator?.mediaDevices?.getUserMedia) { // 93.04% browsers - return; - } - if (!app.checkRequiredVersion || !app.checkRequiredVersion("REQUIRED_TRACKER_VERSION")) { - console.warn("OpenReplay Assist: couldn't load. The minimum required version of @openreplay/tracker@REQUIRED_TRACKER_VERSION is not met") return } - app.notify.log("OpenReplay Assist initializing.") + if (!app.checkRequiredVersion || !app.checkRequiredVersion('REQUIRED_TRACKER_VERSION')) { + console.warn('OpenReplay Assist: couldn\'t load. The minimum required version of @openreplay/tracker@REQUIRED_TRACKER_VERSION is not met') + return + } + app.notify.log('OpenReplay Assist initializing.') const assist = new Assist(app, opts, appOptions.__DISABLE_SECURE_MODE) app.debug.log(assist) return assist diff --git a/tracker/tracker/.eslintignore b/tracker/tracker/.eslintignore new file mode 100644 index 000000000..94b2f339c --- /dev/null +++ b/tracker/tracker/.eslintignore @@ -0,0 +1,8 @@ +node_modules +npm-debug.log +lib +cjs +build +.cache +.eslintrc.cjs +src/common/messages.ts diff --git a/tracker/tracker/.eslintrc.cjs b/tracker/tracker/.eslintrc.cjs index b0c6b42eb..1e2170eee 100644 --- a/tracker/tracker/.eslintrc.cjs +++ b/tracker/tracker/.eslintrc.cjs @@ -1,8 +1,10 @@ +/* eslint-disable */ module.exports = { root: true, parser: '@typescript-eslint/parser', parserOptions: { - project: ['./tsconfig.json'], + project: ['./tsconfig-base.json', './src/main/tsconfig-cjs.json'], + tsconfigRootDir: __dirname, }, plugins: ['prettier', '@typescript-eslint'], extends: [ @@ -11,7 +13,7 @@ module.exports = { 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', - 'prettier/@typescript-eslint', + 'prettier', ], rules: { 'prettier/prettier': ['error', require('./.prettierrc.json')], @@ -27,8 +29,21 @@ module.exports = { '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/explicit-function-return-type': 'warn', '@typescript-eslint/prefer-readonly': 'warn', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/restrict-plus-operands': 'warn', + '@typescript-eslint/no-unsafe-return': 'warn', + 'no-useless-escape': 'warn', + 'no-control-regex': 'warn', + '@typescript-eslint/restrict-template-expressions': 'warn', + '@typescript-eslint/no-useless-constructor': 'warn', + '@typescript-eslint/no-this-alias': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'warn', '@typescript-eslint/no-useless-constructor': 'warn', }, -}; +}; diff --git a/tracker/tracker/.prettierrc.json b/tracker/tracker/.prettierrc.json index a20502b7f..5e2863a11 100644 --- a/tracker/tracker/.prettierrc.json +++ b/tracker/tracker/.prettierrc.json @@ -1,4 +1,5 @@ { + "printWidth": 100, "singleQuote": true, "trailingComma": "all" } diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index 183006c05..797569e33 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -14,33 +14,50 @@ "type": "module", "main": "./lib/index.js", "scripts": { - "lint": "eslint src --ext .ts,.js --fix && tsc --noEmit", + "lint": "eslint src --ext .ts,.js --fix --quiet", "clean": "rm -Rf build && rm -Rf lib && rm -Rf cjs", - "tsc": "tsc -b src/main && tsc -b src/webworker && tsc --project src/main/tsconfig-cjs.json", + "tscRun": "tsc -b src/main && tsc -b src/webworker && tsc --project src/main/tsconfig-cjs.json", "rollup": "rollup --config rollup.config.js", "compile": "node --experimental-modules --experimental-json-modules scripts/compile.cjs", - "build": "npm run clean && npm run tsc && npm run rollup && npm run compile", - "prepare": "node scripts/checkver.cjs && npm run build" + "build": "npm run clean && npm run tscRun && npm run rollup && npm run compile", + "prepare": "cd ../../ && husky install tracker/.husky/", + "lint-front": "lint-staged" }, "devDependencies": { "@babel/core": "^7.10.2", "@rollup/plugin-babel": "^5.0.3", "@rollup/plugin-node-resolve": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", + "@typescript-eslint/eslint-plugin": "^5.30.0", + "@typescript-eslint/parser": "^5.30.0", "eslint": "^7.8.0", - "eslint-plugin-prettier": "^4.1.4", - "prettier": "^2.0.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "husky": "^8.0.1", + "lint-staged": "^13.0.3", + "prettier": "^2.7.1", "replace-in-files": "^2.0.3", "rollup": "^2.17.0", "rollup-plugin-terser": "^6.1.0", "semver": "^6.3.0", - "typescript": "^4.6.0-dev.20211126" + "typescript": "4.6.0-dev.20211126" }, "dependencies": { "error-stack-parser": "^2.0.6" }, "engines": { "node": ">=14.0" + }, + "husky": { + "hooks": { + "pre-commit": "sh lint.sh" + } + }, + "lint-staged": { + "*.{js,mjs,jsx,ts,tsx}": [ + "eslint --fix --quiet" + ], + "*.{json,md,html,js,jsx,ts,tsx}": [ + "prettier --write" + ] } } diff --git a/tracker/tracker/scripts/compile.cjs b/tracker/tracker/scripts/compile.cjs index ec30ac21e..998ef3eca 100644 --- a/tracker/tracker/scripts/compile.cjs +++ b/tracker/tracker/scripts/compile.cjs @@ -12,7 +12,7 @@ async function main() { await replaceInFiles({ files: 'build/**/*', from: 'WEBWORKER_BODY', - to: webworker.replace(/'/g, "\\'"), + to: webworker.replace(/'/g, "\\'").replace(/\n/g, ""), }); await fs.rename('build/main', 'lib'); await fs.rename('build/common', 'lib/common'); diff --git a/tracker/tracker/src/common/webworker.ts b/tracker/tracker/src/common/webworker.ts index 3f6de9c35..5abf9b8fc 100644 --- a/tracker/tracker/src/common/webworker.ts +++ b/tracker/tracker/src/common/webworker.ts @@ -1,19 +1,19 @@ export interface Options { - connAttemptCount?: number - connAttemptGap?: number + connAttemptCount?: number; + connAttemptGap?: number; } type Start = { - type: "start", - ingestPoint: string - pageNo: number - timestamp: number -} & Options + type: 'start'; + ingestPoint: string; + pageNo: number; + timestamp: number; +} & Options; type Auth = { - type: "auth" - token: string - beaconSizeLimit?: number -} + type: 'auth'; + token: string; + beaconSizeLimit?: number; +}; -export type WorkerMessageData = null | "stop" | Start | Auth | Array<{ _id: number }> +export type WorkerMessageData = null | 'stop' | Start | Auth | Array<{ _id: number }>; diff --git a/tracker/tracker/src/main/app/guards.ts b/tracker/tracker/src/main/app/guards.ts index c3f36d398..5304950de 100644 --- a/tracker/tracker/src/main/app/guards.ts +++ b/tracker/tracker/src/main/app/guards.ts @@ -3,31 +3,32 @@ export function isSVGElement(node: Element): node is SVGElement { } 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 -} -export function hasTag(el: Node, tagName: T): el is TagTypeMap[typeof tagName] { - return el.nodeName === tagName + 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; } diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index be03a968c..b96fab257 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 "../../common/messages.js"; -import { Timestamp, Metadata, UserID } from "../../common/messages.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 '../../common/messages.js'; +import { Timestamp, Metadata, UserID } from '../../common/messages.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/webworker.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/webworker.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, @@ -54,7 +54,7 @@ type AppOptions = { session_reset_key: string; local_uuid_key: string; ingestPoint: string; - resourceBaseHref: string | null, // resourceHref? + resourceBaseHref: string | null; // resourceHref? //resourceURLRewriter: (url: string) => string | boolean, verbose: boolean; __is_snippet: boolean; @@ -67,8 +67,7 @@ type AppOptions = { 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'; @@ -86,19 +85,18 @@ export default class App { private readonly messages: Array = []; private readonly observer: Observer; private readonly startCallbacks: Array = []; - private readonly stopCallbacks: Array = []; + private readonly stopCallbacks: Array<() => any> = []; private readonly commitCallbacks: Array = []; private readonly options: AppOptions; private readonly revID: string; private activityState: ActivityState = ActivityState.NotActive; - private version = 'TRACKER_VERSION'; // TODO: version compatability check inside each plugin. + private readonly version = 'TRACKER_VERSION'; // TODO: version compatability check inside each plugin. private readonly worker?: Worker; constructor( projectKey: string, sessionToken: string | null | undefined, options: Partial, ) { - // if (options.onStart !== undefined) { // deprecationWarn("'onStart' option", "tracker.start().then(/* handle session info */)") // } ?? maybe onStart is good @@ -133,13 +131,14 @@ export default class App { 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(new UserID(userID)) + if (userID != null) { + // TODO: nullable userID + this.send(new UserID(userID)); } if (metadata != null) { - Object.entries(metadata).forEach(([key, value]) => this.send(new Metadata(key, value))) + Object.entries(metadata).forEach(([key, value]) => this.send(new Metadata(key, value))); } - }) + }); this.localStorage = this.options.localStorage; this.sessionStorage = this.options.sessionStorage; @@ -149,57 +148,57 @@ export default class App { try { this.worker = new Worker( - URL.createObjectURL( - new Blob([`WEBWORKER_BODY`], { type: 'text/javascript' }), - ), + URL.createObjectURL(new Blob(['WEBWORKER_BODY'], { type: 'text/javascript' })), ); - this.worker.onerror = e => { - this._debug("webworker_error", e) - } + this.worker.onerror = (e) => { + this._debug('webworker_error', e); + }; this.worker.onmessage = ({ data }: MessageEvent) => { - if (data === "failed") { + if (data === 'failed') { this.stop(); - this._debug("worker_failed", {}) // add context (from worker) - } else if (data === "restart") { + this._debug('worker_failed', {}); // add context (from worker) + } else if (data === 'restart') { this.stop(); this.start({ forceNew: true }); } - } + }; const alertWorker = () => { if (this.worker) { this.worker.postMessage(null); } - } + }; // keep better tactics, discard others? 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); - } catch (e) { - this._debug("worker_start", e); + } catch (e) { + this._debug('worker_start', e); } } private _debug(context: string, e: any) { - if(this.options.__debug_report_edp !== null) { + if (this.options.__debug_report_edp !== null) { fetch(this.options.__debug_report_edp, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context, - error: `${e}` - }) + 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 } + if (this.activityState === ActivityState.NotActive) { + return; + } this.messages.push(message); - // TODO: commit on start if there were `urgent` sends; + // 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 + // 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(); @@ -209,7 +208,7 @@ export default class App { if (this.worker && this.messages.length) { this.messages.unshift(new Timestamp(timestamp())); this.worker.postMessage(this.messages); - this.commitCallbacks.forEach(cb => cb(this.messages)); + this.commitCallbacks.forEach((cb) => cb(this.messages)); this.messages.length = 0; } } @@ -220,22 +219,22 @@ export default class App { try { 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); } - attachStopCallback(cb: Function): void { + attachStopCallback(cb: () => any): void { this.stopCallbacks.push(cb); } attachEventListener( @@ -248,24 +247,20 @@ export default class App { if (useSafe) { 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() { @@ -276,13 +271,13 @@ 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() - } + ...this.getStartInfo(), + }; } getSessionToken(): string | undefined { const token = this.sessionStorage.getItem(this.options.session_token_key); @@ -294,38 +289,39 @@ export default class App { 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 + 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) { @@ -337,14 +333,18 @@ export default class App { } 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.")) + 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; - let pageNo: number = 0; + let pageNo = 0; const pageNoStr = this.sessionStorage.getItem(this.options.session_pageno_key); if (pageNoStr != null) { pageNo = parseInt(pageNoStr); @@ -352,95 +352,104 @@ export default class App { } this.sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()); - const startInfo = this.getStartInfo() + const startInfo = this.getStartInfo(); const startWorkerMsg: WorkerMessageData = { - type: "start", + type: 'start', pageNo, ingestPoint: this.options.ingestPoint, timestamp: startInfo.timestamp, 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. - // "updating" with old metadata in order to trigger session's UpdateCallbacks. + this.session.update({ + // TODO: transparent "session" module logic AND explicit internal api for plugins. + // "updating" with old metadata in order to trigger session's UpdateCallbacks. // (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); - return window.fetch(this.options.ingestPoint + '/v1/web/start', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - ...startInfo, - userID: this.session.getInfo().userID, - token: this.sessionStorage.getItem(this.options.session_token_key), - deviceMemory, - jsHeapSizeLimit, - reset: startOpts.forceNew || sReset !== null, - }), - }) - .then(r => { - if (r.status === 200) { - return r.json() - } else { - return r.text().then(text => 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)"); - } - const { token, userUUID, sessionID, beaconSizeLimit } = r; - if (typeof token !== 'string' || + return window + .fetch(this.options.ingestPoint + '/v1/web/start', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...startInfo, + userID: this.session.getInfo().userID, + token: this.sessionStorage.getItem(this.options.session_token_key), + deviceMemory, + jsHeapSizeLimit, + reset: startOpts.forceNew || sReset !== null, + }), + }) + .then((r) => { + if (r.status === 200) { + return r.json(); + } else { + return r + .text() + .then((text) => + 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)'); + } + 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) }`); - } - 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) + (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined') + ) { + 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' + const startWorkerMsg: WorkerMessageData = { + type: 'auth', + token, + beaconSizeLimit, + }; + this.worker.postMessage(startWorkerMsg); - this.activityState = ActivityState.Active - - const onStartInfo = { sessionToken: token, userUUID, sessionID }; + this.activityState = ActivityState.Active; - this.startCallbacks.forEach((cb) => cb(onStartInfo)); // TODO: start as early as possible (before receiving the token) - this.observer.observe(); - this.ticker.start(); + const onStartInfo = { sessionToken: token, userUUID, sessionID }; - this.notify.log("OpenReplay tracking started."); - // get rid of onStart ? - if (typeof this.options.onStart === 'function') { - this.options.onStart(onStartInfo) - } - return SuccessfulStart(onStartInfo) - }) - .catch(reason => { - this.sessionStorage.removeItem(this.options.session_token_key) - this.stop() - if (reason === CANCELED) { return UnsuccessfulStart(CANCELED) } + 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 was unable to start. ", reason) - this._debug("session_start", reason) - return UnsuccessfulStart(START_ERROR) - }) + this.notify.log('OpenReplay tracking started.'); + // get rid of onStart ? + if (typeof this.options.onStart === 'function') { + this.options.onStart(onStartInfo); + } + return SuccessfulStart(onStartInfo); + }) + .catch((reason) => { + this.sessionStorage.removeItem(this.options.session_token_key); + this.stop(); + if (reason === CANCELED) { + return UnsuccessfulStart(CANCELED); + } + + this.notify.log('OpenReplay was unable to start. ', reason); + this._debug('session_start', reason); + return UnsuccessfulStart(START_ERROR); + }); } start(options: StartOptions = {}): Promise { @@ -450,31 +459,31 @@ export default class App { 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): 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) { - this.worker.postMessage("stop") + this.worker.postMessage('stop'); } } finally { - this.activityState = ActivityState.NotActive + this.activityState = ActivityState.NotActive; } } } diff --git a/tracker/tracker/src/main/app/logger.ts b/tracker/tracker/src/main/app/logger.ts index 34d8c12c6..c810cb4ce 100644 --- a/tracker/tracker/src/main/app/logger.ts +++ b/tracker/tracker/src/main/app/logger.ts @@ -1,4 +1,3 @@ - export const LogLevel = { Verbose: 5, Log: 4, @@ -6,54 +5,60 @@ export const LogLevel = { Errors: 2, Silent: 0, } as const; -type LogLevel = typeof LogLevel[keyof typeof LogLevel] - +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; constructor(options: Options = LogLevel.Silent) { - this.options = options === true - ? { level: LogLevel.Verbose } - : typeof options === "number" ? { level: options } : options; + this.options = + options === true + ? { level: LogLevel.Verbose } + : typeof options === 'number' + ? { level: options } + : options; } log(...args: any) { - if (IsCustomLevel(this.options.level) - ? this.options.level.log - : this.options.level >= LogLevel.Log) { - console.log(...args) + if ( + IsCustomLevel(this.options.level) + ? this.options.level.log + : this.options.level >= LogLevel.Log + ) { + console.log(...args); } } warn(...args: any) { - if (IsCustomLevel(this.options.level) - ? this.options.level.warn - : this.options.level >= LogLevel.Warnings) { - console.warn(...args) + if ( + IsCustomLevel(this.options.level) + ? this.options.level.warn + : this.options.level >= LogLevel.Warnings + ) { + console.warn(...args); } } error(...args: any) { - if (IsCustomLevel(this.options.level) - ? this.options.level.error - : this.options.level >= LogLevel.Errors) { - console.error(...args) + if ( + IsCustomLevel(this.options.level) + ? this.options.level.error + : this.options.level >= LogLevel.Errors + ) { + console.error(...args); } } } diff --git a/tracker/tracker/src/main/app/nodes.ts b/tracker/tracker/src/main/app/nodes.ts index a1c87b6e2..4bdab642a 100644 --- a/tracker/tracker/src/main/app/nodes.ts +++ b/tracker/tracker/src/main/app/nodes.ts @@ -13,11 +13,7 @@ export default class Nodes { attachNodeCallback(nodeCallback: NodeCallback): void { this.nodeCallbacks.push(nodeCallback); } - attachElementListener( - type: string, - node: Element, - elementListener: EventListener, - ): void { + attachElementListener(type: string, node: Element, elementListener: EventListener): void { const id = this.getID(node); if (id === undefined) { return; @@ -50,9 +46,7 @@ export default class Nodes { const listeners = this.elementListeners.get(id); if (listeners !== undefined) { this.elementListeners.delete(id); - listeners.forEach((listener) => - node.removeEventListener(listener[0], listener[1]), - ); + listeners.forEach((listener) => node.removeEventListener(listener[0], listener[1])); } } return id; diff --git a/tracker/tracker/src/main/app/observer/iframe_observer.ts b/tracker/tracker/src/main/app/observer/iframe_observer.ts index 1f50e588a..faea6a5e6 100644 --- a/tracker/tracker/src/main/app/observer/iframe_observer.ts +++ b/tracker/tracker/src/main/app/observer/iframe_observer.ts @@ -1,19 +1,20 @@ -import Observer from "./observer.js"; -import { CreateIFrameDocument } from "../../../common/messages.js"; +import Observer from './observer.js'; +import { CreateIFrameDocument } from '../../../common/messages.js'; export default class IFrameObserver extends Observer { observe(iframe: HTMLIFrameElement) { const doc = iframe.contentDocument; const hostID = this.app.nodes.getID(iframe); - if (!doc || hostID === undefined) { return } //log TODO common app.logger + if (!doc || hostID === undefined) { + 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") + console.log('OpenReplay: Iframe document not bound'); return; } - this.app.send(CreateIFrameDocument(hostID, docID)); + this.app.send(CreateIFrameDocument(hostID, docID)); }); } - -} \ No newline at end of file +} diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts index 940a7eefb..8f2e60177 100644 --- a/tracker/tracker/src/main/app/observer/observer.ts +++ b/tracker/tracker/src/main/app/observer/observer.ts @@ -8,40 +8,35 @@ import { CreateElementNode, MoveNode, RemoveNode, -} from "../../../common/messages.js"; -import App from "../index.js"; -import { - isRootNode, - isTextNode, - isElementNode, - isSVGElement, - hasTag, -} from "../guards.js"; +} from '../../../common/messages.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 } - if (!isElementNode(node)) { return true } + if (isTextNode(node)) { + return false; + } + if (!isElementNode(node)) { + return true; + } 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"); + return !(rel?.includes('stylesheet') || as === 'style' || as === 'font'); } return ( - tag === 'SCRIPT' || - tag === 'NOSCRIPT' || - tag === 'META' || - tag === 'TITLE' || - tag === 'BASE' + tag === 'SCRIPT' || tag === 'NOSCRIPT' || tag === 'META' || tag === 'TITLE' || tag === 'BASE' ); } function isObservable(node: Node): boolean { - if (isRootNode(node)) { return true } - return !isIgnored(node) + if (isRootNode(node)) { + return true; + } + return !isIgnored(node); } - /* TODO: - fix unbinding logic + send all removals first (ensure sequence is correct) @@ -57,14 +52,15 @@ enum RecentsType { export default abstract class Observer { private readonly observer: MutationObserver; private readonly commited: Array = []; - private readonly recents: Map = new Map() + 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 + for (const mutation of mutations) { + // mutations order is sequential const target = mutation.target; const type = mutation.type; @@ -85,16 +81,16 @@ export default abstract class Observer { 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; if (name === null) { 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; @@ -110,18 +106,13 @@ export default abstract class Observer { } private clear(): void { this.commited.length = 0; - this.recents.clear() + 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 { + 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); @@ -150,7 +141,7 @@ export default abstract class Observer { } if ( name === 'value' && - hasTag(node, "INPUT") && + hasTag(node, 'INPUT') && node.type !== 'button' && node.type !== 'reset' && node.type !== 'submit' @@ -161,7 +152,7 @@ export default abstract class Observer { this.app.send(new RemoveNodeAttribute(id, name)); return; } - if (name === 'style' || name === 'href' && hasTag(node, "LINK")) { + if (name === 'style' || (name === 'href' && hasTag(node, 'LINK'))) { this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())); return; } @@ -172,26 +163,27 @@ export default abstract class Observer { } private sendNodeData(id: number, parentElement: Element, data: string): void { - if (hasTag(parentElement, "STYLE") || hasTag(parentElement, "style")) { + if (hasTag(parentElement, 'STYLE') || hasTag(parentElement, 'style')) { this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref())); return; } - data = this.app.sanitizer.sanitize(id, data) + data = this.app.sanitizer.sanitize(id, data); this.app.send(new SetNodeData(id, data)); } private bindNode(node: Node): void { - const [ id, isNew ]= this.app.nodes.registerNode(node); - if (isNew){ - 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) + const [id, isNew] = this.app.nodes.registerNode(node); + if (isNew) { + 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); } } private bindTree(node: Node): void { if (!isObservable(node)) { - return + return; } this.bindNode(node); const walker = document.createTreeWalker( @@ -229,7 +221,7 @@ export default abstract class Observer { // 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) // TODO: Clean the logic (though now it workd fine) - if (!hasTag(node, "HTML") || !this.isTopContext) { + if (!hasTag(node, 'HTML') || !this.isTopContext) { 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? ) @@ -264,15 +256,15 @@ export default abstract class Observer { if (sibling === null) { 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'; } 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; @@ -282,15 +274,7 @@ export default abstract class Observer { (el as HTMLElement | SVGElement).style.height = height + 'px'; } - this.app.send(new - CreateElementNode( - id, - parentID, - index, - el.tagName, - isSVGElement(node), - ), - ); + this.app.send(new CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node))); } for (let i = 0; i < el.attributes.length; i++) { const attr = el.attributes[i]; @@ -335,19 +319,23 @@ export default abstract class Observer { } return (this.commited[id] = this._commitNode(id, node)); } - private commitNodes(isStart: boolean = false): void { + private commitNodes(isStart = false): void { let node; this.recents.forEach((type, 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(); } // ISSSUE - protected observeRoot(node: Node, beforeCommit: (id?: number) => unknown, nodeToBind: Node = node) { + protected observeRoot( + node: Node, + beforeCommit: (id?: number) => unknown, + nodeToBind: Node = node, + ) { this.observer.observe(node, { childList: true, attributes: true, @@ -357,8 +345,8 @@ export default abstract class Observer { characterDataOldValue: false, }); this.bindTree(nodeToBind); - beforeCommit(this.app.nodes.getID(node)) - this.commitNodes(true) + beforeCommit(this.app.nodes.getID(node)); + this.commitNodes(true); } disconnect(): void { 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 ea37bb30f..f6059e2f5 100644 --- a/tracker/tracker/src/main/app/observer/shadow_root_observer.ts +++ b/tracker/tracker/src/main/app/observer/shadow_root_observer.ts @@ -1,18 +1,19 @@ -import Observer from "./observer.js"; -import { CreateIFrameDocument } from "../../../common/messages.js"; +import Observer from './observer.js'; +import { CreateIFrameDocument } from '../../../common/messages.js'; export default class ShadowRootObserver extends Observer { observe(el: Element) { const shRoot = el.shadowRoot; const hostID = this.app.nodes.getID(el); - if (!shRoot || hostID === undefined) { return } // log + if (!shRoot || hostID === undefined) { + return; + } // log this.observeRoot(shRoot, (rootID) => { if (rootID === undefined) { - console.log("OpenReplay: Shadow Root was not bound") + console.log('OpenReplay: Shadow Root was not bound'); return; } - this.app.send(CreateIFrameDocument(hostID,rootID)); + this.app.send(CreateIFrameDocument(hostID, rootID)); }); } - -} \ No newline at end of file +} diff --git a/tracker/tracker/src/main/app/observer/top_observer.ts b/tracker/tracker/src/main/app/observer/top_observer.ts index 469ad2388..cc921967f 100644 --- a/tracker/tracker/src/main/app/observer/top_observer.ts +++ b/tracker/tracker/src/main/app/observer/top_observer.ts @@ -1,101 +1,112 @@ -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 "../../../common/messages.js"; -import App from "../index.js"; -import { IN_BROWSER, hasOpenreplayAttribute } from '../../utils.js' +import { CreateDocument } from '../../../common/messages.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 { +export default class TopObserver extends Observer { private readonly options: Options; constructor(app: App, options: Partial) { super(app, true); - this.options = Object.assign({ - captureIFrames: true - }, options); + this.options = Object.assign( + { + captureIFrames: true, + }, + options, + ); // IFrames - this.app.nodes.attachNodeCallback(node => { - if (hasTag(node, "IFRAME") && - ((this.options.captureIFrames && !hasOpenreplayAttribute(node, "obscured")) - || hasOpenreplayAttribute(node, "capture")) + this.app.nodes.attachNodeCallback((node) => { + if ( + hasTag(node, 'IFRAME') && + ((this.options.captureIFrames && !hasOpenreplayAttribute(node, 'obscured')) || + hasOpenreplayAttribute(node, 'capture')) ) { - this.handleIframe(node) + this.handleIframe(node); } - }) + }); // ShadowDOM - this.app.nodes.attachNodeCallback(node => { + this.app.nodes.attachNodeCallback((node) => { if (isElementNode(node) && node.shadowRoot !== null) { - this.handleShadowRoot(node.shadowRoot) + this.handleShadowRoot(node.shadowRoot); } - }) + }); } - 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) - if (id === undefined) { return } //log - if (iframe.contentDocument === doc) { return } // How frequently can it happen? - doc = iframe.contentDocument - if (!doc || !iframe.contentWindow) { return } - const observer = new IFrameObserver(this.app) + const id = this.app.nodes.getID(iframe); + if (id === undefined) { + return; + } //log + if (iframe.contentDocument === doc) { + return; + } // How frequently can it happen? + doc = iframe.contentDocument; + if (!doc || !iframe.contentWindow) { + return; + } + 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; - Element.prototype.attachShadow = function() { - const shadow = attachShadowNativeFn.apply(this, arguments) - observer.handleShadowRoot(shadow) - return shadow - } + Element.prototype.attachShadow = function () { + // eslint-disable-next-line + 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 - // In this case context.document have to be observed, but this will cause - // the change in the re-player behaviour caused by CreateDocument message: + // In this case context.document have to be observed, but this will cause + // the change in the re-player behaviour caused by CreateDocument message: // the 0-node ("fRoot") will become #document rather than documentElement as it is now. // Alternatively - observe(#document) then bindNode(documentElement) - this.observeRoot(window.document, () => { - this.app.send(new CreateDocument()) - }, window.document.documentElement); + this.observeRoot( + window.document, + () => { + this.app.send(new 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(); } - -} \ No newline at end of file +} diff --git a/tracker/tracker/src/main/app/sanitizer.ts b/tracker/tracker/src/main/app/sanitizer.ts index d870cfaca..358913f93 100644 --- a/tracker/tracker/src/main/app/sanitizer.ts +++ b/tracker/tracker/src/main/app/sanitizer.ts @@ -1,6 +1,6 @@ -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; @@ -13,36 +13,39 @@ export default class Sanitizer { private readonly options: Options; constructor(private readonly app: App, options: Partial) { - this.options = Object.assign({ - obscureTextEmails: true, - obscureTextNumbers: false, - }, options); + this.options = Object.assign( + { + obscureTextEmails: true, + obscureTextNumbers: false, + }, + options, + ); } handleNode(id: number, parentID: number, node: Node) { if ( - this.masked.has(parentID) || - (isElementNode(node) && - hasOpenreplayAttribute(node, 'masked')) - ) { - this.masked.add(id); - } - if ( - this.maskedContainers.has(parentID) || - (isElementNode(node) && - hasOpenreplayAttribute(node, 'htmlmasked')) - ) { - this.maskedContainers.add(id); - } + this.masked.has(parentID) || + (isElementNode(node) && hasOpenreplayAttribute(node, 'masked')) + ) { + this.masked.add(id); + } + if ( + this.maskedContainers.has(parentID) || + (isElementNode(node) && hasOpenreplayAttribute(node, 'htmlmasked')) + ) { + this.maskedContainers.add(id); + } } sanitize(id: number, data: string): string { if (this.masked.has(id)) { // 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, - '█', - ); + return data + .trim() + .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'); @@ -50,11 +53,10 @@ export default class Sanitizer { if (this.options.obscureTextEmails) { data = data.replace( /([^\s]+)@([^\s]+)\.([^\s]+)/g, - (...f: Array) => - stars(f[1]) + '@' + stars(f[2]) + '.' + stars(f[3]), + (...f: Array) => stars(f[1]) + '@' + stars(f[2]) + '.' + stars(f[3]), ); } - return data + return data; } isMasked(id: number): boolean { @@ -65,15 +67,15 @@ export default class Sanitizer { } getInnerTextSecure(el: HTMLElement): string { - const id = this.app.nodes.getID(el) - if (!id) { return '' } - return this.sanitize(id, el.innerText) - + const id = this.app.nodes.getID(el); + if (!id) { + return ''; + } + return this.sanitize(id, el.innerText); } clear(): void { 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 d7815309a..938883359 100644 --- a/tracker/tracker/src/main/app/session.ts +++ b/tracker/tracker/src/main/app/session.ts @@ -1,54 +1,52 @@ -import { UserID, UserAnonymousID, Metadata } from "../../common/messages.js"; - +import { UserID, UserAnonymousID, Metadata } from '../../common/messages.js'; 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 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 + if (newInfo.userID !== undefined) { + // TODO clear nullable/undefinable types + 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 { @@ -56,12 +54,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; } -} \ No newline at end of file +} diff --git a/tracker/tracker/src/main/app/ticker.ts b/tracker/tracker/src/main/app/ticker.ts index 2a4ee52f7..81bf7b6a8 100644 --- a/tracker/tracker/src/main/app/ticker.ts +++ b/tracker/tracker/src/main/app/ticker.ts @@ -1,4 +1,4 @@ -import App from "./index.js"; +import App from './index.js'; type Callback = () => void; function wrap(callback: Callback, n: number): Callback { diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index b5b991bf7..310545dd9 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -1,30 +1,39 @@ -import App, { DEFAULT_INGEST_POINT } from "./app/index.js"; +import App, { DEFAULT_INGEST_POINT } from './app/index.js'; export { default as App } from './app/index.js'; -import { UserID, UserAnonymousID, Metadata, RawCustomEvent, CustomIssue } from "../common/messages.js"; -import * as _Messages from "../common/messages.js"; +import { + UserID, + UserAnonymousID, + Metadata, + RawCustomEvent, + CustomIssue, +} from '../common/messages.js'; +import * as _Messages from '../common/messages.js'; export const Messages = _Messages; -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"; +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'; -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'; @@ -44,25 +53,32 @@ 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}`); + console.error( + `OpenReplay: invalid options argument type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`, + ); return false; } if (typeof obj.projectKey !== 'string') { if (typeof obj.projectKey !== 'number') { - if (typeof obj.projectID !== 'number') { // 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 + if (typeof obj.projectID !== 'number') { + // 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; } else { obj.projectKey = obj.projectID.toString(); - deprecationWarn("`projectID` option", "`projectKey` option", DOCS_SETUP) + 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}`) + console.warn( + `OpenReplay: invalid options argument type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`, + ); } return true; } @@ -74,18 +90,22 @@ export default class API { return; } if ((window as any).__OPENREPLAY__) { - 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.") + console.error('OpenReplay: one tracker instance has been initialised already'); return; } - const doNotTrack = options.respectDoNotTrack && - (navigator.doNotTrack == '1' + 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; + } + const doNotTrack = + options.respectDoNotTrack && + (navigator.doNotTrack == '1' || // @ts-ignore - || window.doNotTrack == '1'); - const app = this.app = doNotTrack || + window.doNotTrack == '1'); + const app = (this.app = + doNotTrack || !('Map' in window) || !('Set' in window) || !('MutationObserver' in window) || @@ -95,7 +115,7 @@ 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); @@ -114,29 +134,33 @@ export default class API { const wOpen = window.open; app.attachStartCallback(() => { // @ts-ignore ? - window.open = function(...args) { - app.resetNextPageSession(true) - wOpen.call(window, ...args) - app.resetNextPageSession(false) - } - }) + window.open = function (...args) { + app.resetNextPageSession(true); + wOpen.call(window, ...args); + app.resetNextPageSession(false); + }; + }); app.attachStopCallback(() => { window.open = wOpen; - }) + }); } } else { - console.log("OpenReplay: browser doesn't support API required for tracking or doNotTrack is set to 1.") + 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"); + 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(JSON.stringify({ - trackerVersion: 'TRACKER_VERSION', - projectKey: options.projectKey, - doNotTrack, - // TODO: add precise reason (an exact API missing) - })); + req.send( + JSON.stringify({ + trackerVersion: 'TRACKER_VERSION', + projectKey: options.projectKey, + doNotTrack, + // TODO: add precise reason (an exact API missing) + }), + ); } } @@ -151,10 +175,12 @@ export default class API { return this.app.active(); } - start(startOpts?: Partial) : Promise { + 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."); + 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.'); } if (this.app === null) { return Promise.reject("Browser doesn't support required api, or doNotTrack is active."); @@ -182,7 +208,7 @@ export default class API { return this.app.getSessionID(); } sessionID(): string | null | undefined { - deprecationWarn("'sessionID' method", "'getSessionID' method", "/"); + deprecationWarn("'sessionID' method", "'getSessionID' method", '/'); return this.getSessionID(); } @@ -192,7 +218,7 @@ export default class API { } } userID(id: string): void { - deprecationWarn("'userID' method", "'setUserID' method", "/"); + deprecationWarn("'userID' method", "'setUserID' method", '/'); this.setUserID(id); } @@ -202,25 +228,21 @@ export default class API { } } userAnonymousID(id: string): void { - deprecationWarn("'userAnonymousID' method", "'setUserAnonymousID' method", "/") + deprecationWarn("'userAnonymousID' method", "'setUserAnonymousID' method", '/'); this.setUserAnonymousID(id); } setMetadata(key: string, value: string): void { - if ( - typeof key === 'string' && - typeof value === 'string' && - this.app !== null - ) { + if (typeof key === 'string' && typeof value === 'string' && this.app !== null) { this.app.session.setMetadata(key, value); } } metadata(key: string, value: string): void { - deprecationWarn("'metadata' method", "'setMetadata' method", "/") + deprecationWarn("'metadata' method", "'setMetadata' method", '/'); this.setMetadata(key, value); } - event(key: string, payload: any, issue: boolean = false): void { + event(key: string, payload: any, issue = false): void { if (typeof key === 'string' && this.app !== null) { if (issue) { return this.issue(key, payload); @@ -247,10 +269,13 @@ export default class API { } handleError = (e: Error | ErrorEvent | PromiseRejectionEvent) => { - if (this.app === null) { return; } + if (this.app === null) { + return; + } if (e instanceof Error) { this.app.send(getExceptionMessage(e, [])); - } else if (e instanceof ErrorEvent || + } else if ( + e instanceof ErrorEvent || ('PromiseRejectionEvent' in window && e instanceof PromiseRejectionEvent) ) { const msg = getExceptionMessageFromEvent(e); @@ -258,5 +283,5 @@ export default class API { this.app.send(msg); } } - } + }; } diff --git a/tracker/tracker/src/main/modules/connection.ts b/tracker/tracker/src/main/modules/connection.ts index 72ef972f7..ecf9e9fe9 100644 --- a/tracker/tracker/src/main/modules/connection.ts +++ b/tracker/tracker/src/main/modules/connection.ts @@ -1,13 +1,13 @@ -import App from "../app/index.js"; -import { ConnectionInformation } from "../../common/messages.js"; +import App from '../app/index.js'; +import { ConnectionInformation } from '../../common/messages.js'; -export default function(app: App): void { +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 || diff --git a/tracker/tracker/src/main/modules/console.ts b/tracker/tracker/src/main/modules/console.ts index 40925c83c..33ad34957 100644 --- a/tracker/tracker/src/main/modules/console.ts +++ b/tracker/tracker/src/main/modules/console.ts @@ -1,7 +1,7 @@ -import type App from "../app/index.js"; -import { hasTag } from "../app/guards.js"; -import { IN_BROWSER } from "../utils.js"; -import { ConsoleLog } from "../../common/messages.js"; +import type App from '../app/index.js'; +import { hasTag } from '../app/guards.js'; +import { IN_BROWSER } from '../utils.js'; +import { ConsoleLog } from '../../common/messages.js'; const printError: (e: Error) => string = IN_BROWSER && 'InstallTrigger' in window // detect Firefox @@ -104,10 +104,7 @@ export default function (app: App, opts: Partial): void { }, opts, ); - if ( - !Array.isArray(options.consoleMethods) || - options.consoleMethods.length === 0 - ) { + if (!Array.isArray(options.consoleMethods) || options.consoleMethods.length === 0) { return; } @@ -139,18 +136,21 @@ export default function (app: App, opts: Partial): void { }); patchConsole(window.console); - app.nodes.attachNodeCallback(app.safe(node => { - if (hasTag(node, "IFRAME")) { // TODO: newContextCallback - let context = node.contentWindow - if (context) { - 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) + app.nodes.attachNodeCallback( + app.safe((node) => { + if (hasTag(node, 'IFRAME')) { + // TODO: newContextCallback + let context = node.contentWindow; + if (context) { + 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); + } + }); + } + }), + ); } diff --git a/tracker/tracker/src/main/modules/cssrules.ts b/tracker/tracker/src/main/modules/cssrules.ts index 5007bbd26..53c805722 100644 --- a/tracker/tracker/src/main/modules/cssrules.ts +++ b/tracker/tracker/src/main/modules/cssrules.ts @@ -1,57 +1,53 @@ -import type App from "../app/index.js"; -import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from "../../common/messages.js"; -import { hasTag } from "../app/guards.js"; +import type App from '../app/index.js'; +import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../../common/messages.js'; +import { hasTag } from '../app/guards.js'; - -export default function(app: App | null) { +export default function (app: App | null) { if (app === null) { return; } if (!window.CSSStyleSheet) { - app.send(new TechnicalInfo("no_stylesheet_prototype_in_window", "")) + app.send(new TechnicalInfo('no_stylesheet_prototype_in_window', '')); return; } - const processOperation = app.safe( - (stylesheet: CSSStyleSheet, index: number, rule?: string) => { - const sendMessage = typeof rule === 'string' - ? (nodeID: number) => app.send(new CSSInsertRuleURLBased(nodeID, rule, index, app.getBaseHref())) + const processOperation = app.safe((stylesheet: CSSStyleSheet, index: number, rule?: string) => { + const sendMessage = + typeof rule === 'string' + ? (nodeID: number) => + app.send(new CSSInsertRuleURLBased(nodeID, rule, index, app.getBaseHref())) : (nodeID: number) => app.send(new 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"); - } - const nodeID = app.nodes.getID(stylesheet.ownerNode); - if (nodeID !== undefined) { - sendMessage(nodeID); - } // else error? + // TODO: Extend messages to maintain nested rules (CSSGroupingRule prototype, as well as CSSKeyframesRule) + if (stylesheet.ownerNode == null) { + throw new Error('Owner Node not found'); } - ) + const nodeID = app.nodes.getID(stylesheet.ownerNode); + if (nodeID !== undefined) { + sendMessage(nodeID); + } // else error? + }); const { insertRule, deleteRule } = CSSStyleSheet.prototype; - CSSStyleSheet.prototype.insertRule = function( - rule: string, - index: number = 0, - ) { + CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) { processOperation(this, index, rule); return insertRule.call(this, rule, index); }; - CSSStyleSheet.prototype.deleteRule = function(index: number) { + CSSStyleSheet.prototype.deleteRule = function (index: number) { processOperation(this, index); return deleteRule.call(this, index); }; app.nodes.attachNodeCallback((node: Node): void => { - if (!hasTag(node, "STYLE") || !node.sheet) { + if (!hasTag(node, 'STYLE') || !node.sheet) { return; } if (node.textContent !== null && node.textContent.trim().length > 0) { return; // Only fully virtual sheets maintained so far - } + } 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 03dc84d72..2cb66885e 100644 --- a/tracker/tracker/src/main/modules/exception.ts +++ b/tracker/tracker/src/main/modules/exception.ts @@ -1,6 +1,6 @@ -import type App from "../app/index.js"; -import type Message from "../../common/messages.js"; -import { JSException } from "../../common/messages.js"; +import type App from '../app/index.js'; +import type Message from '../../common/messages.js'; +import { JSException } from '../../common/messages.js'; import ErrorStackParser from 'error-stack-parser'; export interface Options { @@ -8,53 +8,56 @@ export interface Options { } 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 { - return [{ - columnNumber: e.colno, - lineNumber: e.lineno, - fileName: e.filename, - functionName: "", - source: "", - }]; + return [ + { + columnNumber: e.colno, + lineNumber: e.lineno, + fileName: e.filename, + functionName: '', + source: '', + }, + ]; } export function getExceptionMessage(error: Error, fallbackStack: Array): Message { let stack = fallbackStack; try { stack = ErrorStackParser.parse(error); - } catch (e) { - } + } catch (e) {} return new JSException(error.name, error.message, JSON.stringify(stack)); } -export function getExceptionMessageFromEvent(e: ErrorEvent | PromiseRejectionEvent): Message | null { +export function getExceptionMessageFromEvent( + e: ErrorEvent | PromiseRejectionEvent, +): 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(':'); if (!message) { name = 'Error'; - message = e.message + message = e.message; } return new 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; try { - message = JSON.stringify(e.reason) - } catch(_) { - message = String(e.reason) + message = JSON.stringify(e.reason); + } catch (_) { + message = String(e.reason); } return new JSException('Unhandled Promise Rejection', message, '[]'); } @@ -62,7 +65,6 @@ export function getExceptionMessageFromEvent(e: ErrorEvent | PromiseRejectionEve return null; } - export default function (app: App, opts: Partial): void { const options: Options = Object.assign( { @@ -76,17 +78,11 @@ export default function (app: App, opts: Partial): void { if (msg != null) { app.send(msg); } - } + }; - app.attachEventListener( - window, - 'unhandledrejection', - (e: PromiseRejectionEvent): void => handler(e), - ); - app.attachEventListener( - window, - 'error', - (e: ErrorEvent): void => handler(e), + app.attachEventListener(window, 'unhandledrejection', (e: PromiseRejectionEvent): 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 b462f7705..f34675074 100644 --- a/tracker/tracker/src/main/modules/img.ts +++ b/tracker/tracker/src/main/modules/img.ts @@ -1,34 +1,38 @@ -import type App from "../app/index.js"; -import { timestamp, isURL } from "../utils.js"; -import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from "../../common/messages.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 '../../common/messages.js'; +import { hasTag } from '../app/guards.js'; function resolveURL(url: string, location: Location = document.location) { - url = url.trim() - if (url.startsWith('/')) { - return location.origin + url + url = url.trim(); + if (url.startsWith('/')) { + 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(new SetNodeAttribute(id, "src", PLACEHOLDER_SRC)) + app.send(new SetNodeAttribute(id, 'src', PLACEHOLDER_SRC)); const { width, height } = node.getBoundingClientRect(); - if (!node.hasAttribute("width")){ - app.send(new SetNodeAttribute(id, "width", String(width))) + if (!node.hasAttribute('width')) { + app.send(new SetNodeAttribute(id, 'width', String(width))); } - if (!node.hasAttribute("height")){ - app.send(new SetNodeAttribute(id, "height", String(height))) + if (!node.hasAttribute('height')) { + app.send(new SetNodeAttribute(id, 'height', String(height))); } } @@ -41,35 +45,38 @@ export default function (app: App): void { if (!complete) { 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(new 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(new SetNodeAttribute(id, 'src', resolvedSrc)) + app.send(new SetNodeAttribute(id, 'src', resolvedSrc)); if (srcset) { - const resolvedSrcset = srcset.split(',').map(str => resolveURL(str)).join(',') - app.send(new SetNodeAttribute(id, 'srcset', resolvedSrcset)) + const resolvedSrcset = srcset + .split(',') + .map((str) => resolveURL(str)) + .join(','); + app.send(new SetNodeAttribute(id, 'srcset', resolvedSrcset)); } } }); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { - if (mutation.type === "attributes") { - const target = (mutation.target as HTMLImageElement); + if (mutation.type === 'attributes') { + const target = mutation.target as HTMLImageElement; const id = app.nodes.getID(target); if (id === undefined) { return; } - if (mutation.attributeName === "src") { + if (mutation.attributeName === 'src') { const src = target.src; app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref())); } - if (mutation.attributeName === "srcset") { + if (mutation.attributeName === 'srcset') { const srcset = target.srcset; app.send(new SetNodeAttribute(id, 'srcset', srcset)); } @@ -78,12 +85,12 @@ export default function (app: App): void { }); app.nodes.attachNodeCallback((node: Node): void => { - if (!hasTag(node, "IMG")) { + if (!hasTag(node, 'IMG')) { return; } app.nodes.attachElementListener('error', node, sendImgSrc); app.nodes.attachElementListener('load', node, sendImgSrc); sendImgSrc.call(node); - observer.observe(node, { attributes: true, attributeFilter: [ "src", "srcset" ] }); + 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 496d642a2..4e7989ac3 100644 --- a/tracker/tracker/src/main/modules/input.ts +++ b/tracker/tracker/src/main/modules/input.ts @@ -1,45 +1,38 @@ -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 "../../common/messages.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 '../../common/messages.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")) { + if (hasTag(node, 'TEXTAREA')) { return true; } - if (!hasTag(node, "INPUT")) { + if (!hasTag(node, 'INPUT')) { 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")) { + if (!hasTag(node, 'INPUT')) { return false; } const type = node.type; return type === 'checkbox' || type === 'radio'; } -const labelElementFor: ( - element: TextEditableElement, -) => HTMLLabelElement | undefined = +const labelElementFor: (element: TextEditableElement) => HTMLLabelElement | undefined = IN_BROWSER && 'labels' in HTMLInputElement.prototype ? (node) => { let p: Node | null = node; while ((p = p.parentNode) !== null) { - if (hasTag(p, "LABEL")) { - return p + if (hasTag(p, 'LABEL')) { + return p; } } const labels = node.labels; @@ -50,8 +43,8 @@ const labelElementFor: ( : (node) => { let p: Node | null = node; while ((p = p.parentNode) !== null) { - if (hasTag(p, "LABEL")) { - return p as HTMLLabelElement; + if (hasTag(p, 'LABEL')) { + return p; } } const id = node.id; @@ -67,12 +60,13 @@ export function getInputLabel(node: TextEditableElement): string { let label = getLabelAttribute(node); if (label === null) { const labelElement = labelElementFor(node); - label = (labelElement && labelElement.innerText) - || node.placeholder - || node.name - || node.id - || node.className - || node.type + label = + (labelElement && labelElement.innerText) || + node.placeholder || + node.name || + node.id || + node.className || + node.type; } return normSpaces(label).slice(0, 100); } @@ -116,8 +110,7 @@ export default function (app: App, opts: Partial): void { (inputMode === InputMode.Plain && ((options.obscureInputNumbers && node.type !== 'date' && /\d\d\d\d/.test(value)) || (options.obscureInputDates && node.type === 'date') || - (options.obscureInputEmails && - (node.type === 'email' || !!~value.indexOf('@'))))) + (options.obscureInputEmails && (node.type === 'email' || !!~value.indexOf('@'))))) ) { inputMode = InputMode.Obscured; } @@ -183,11 +176,11 @@ export default function (app: App, opts: Partial): void { return; } // TODO: support multiple select (?): use selectedOptions; Need send target? - if (hasTag(node, "SELECT")) { - sendInputValue(id, node) - app.attachEventListener(node, "change", () => { - sendInputValue(id, node) - }) + if (hasTag(node, 'SELECT')) { + sendInputValue(id, node); + app.attachEventListener(node, 'change', () => { + sendInputValue(id, node); + }); } if (isTextEditable(node)) { inputValues.set(id, node.value); diff --git a/tracker/tracker/src/main/modules/longtasks.ts b/tracker/tracker/src/main/modules/longtasks.ts index 959d748f7..8ac5f789a 100644 --- a/tracker/tracker/src/main/modules/longtasks.ts +++ b/tracker/tracker/src/main/modules/longtasks.ts @@ -1,5 +1,5 @@ -import type App from "../app/index.js"; -import { LongTask } from "../../common/messages.js"; +import type App from '../app/index.js'; +import { LongTask } from '../../common/messages.js'; // https://w3c.github.io/performance-timeline/#the-performanceentry-interface interface TaskAttributionTiming extends PerformanceEntry { @@ -7,22 +7,35 @@ interface TaskAttributionTiming extends PerformanceEntry { readonly containerSrc: string; readonly containerId: string; readonly containerName: string; -}; +} // https://www.w3.org/TR/longtasks/#performancelongtasktiming interface PerformanceLongTaskTiming extends PerformanceEntry { readonly attribution: ReadonlyArray; -}; +} export default function (app: App): void { if (!('PerformanceObserver' in window) || !('PerformanceLongTaskTiming' in window)) { return; } - const contexts: string[] = [ "unknown", "self", "same-origin-ancestor", "same-origin-descendant", "same-origin", "cross-origin-ancestor", "cross-origin-descendant", "cross-origin-unreachable", "multiple-contexts" ]; - const containerTypes: string[] = [ "window", "iframe", "embed", "object" ]; + const contexts: string[] = [ + 'unknown', + 'self', + 'same-origin-ancestor', + 'same-origin-descendant', + 'same-origin', + 'cross-origin-ancestor', + 'cross-origin-descendant', + 'cross-origin-unreachable', + 'multiple-contexts', + ]; + const containerTypes: string[] = ['window', 'iframe', 'embed', 'object']; function longTask(entry: PerformanceLongTaskTiming): void { - let type: string = "", src: string = "", id: string = "", name: string = ""; + let type = '', + src = '', + id = '', + name = ''; const container = entry.attribution[0]; if (container != null) { type = container.containerType; @@ -48,4 +61,4 @@ export default function (app: App): void { list.getEntries().forEach(longTask), ); observer.observe({ entryTypes: ['longtask'] }); -} \ No newline at end of file +} diff --git a/tracker/tracker/src/main/modules/mouse.ts b/tracker/tracker/src/main/modules/mouse.ts index 09a06e0ca..c46ed84ea 100644 --- a/tracker/tracker/src/main/modules/mouse.ts +++ b/tracker/tracker/src/main/modules/mouse.ts @@ -1,44 +1,43 @@ -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 "../../common/messages.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 '../../common/messages.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.split(' ') - .map(cn => cn.trim()) - .filter(cn => cn !== '') - .reduce((sel, cn) => `${sel}.${cn}`, el.tagName.toLowerCase()) + - (selector ? ` > ${selector}` : ''); - if (el === document.body) { - return selector - } - el = el.parentElement - } while (el !== document.body && el !== null) - return selector + el.className + .split(' ') + .map((cn) => cn.trim()) + .filter((cn) => cn !== '') + .reduce((sel, cn) => `${sel}.${cn}`, el.tagName.toLowerCase()) + + (selector ? ` > ${selector}` : ''); + if (el === document.body) { + return selector; + } + el = el.parentElement; + } while (el !== document.body && el !== null); + return selector; } function isClickable(element: Element): boolean { - const tag = element.tagName.toUpperCase() - return tag === 'BUTTON' || + const tag = element.tagName.toUpperCase(); + return ( + tag === 'BUTTON' || tag === 'A' || tag === 'LI' || tag === 'SELECT' || (element as HTMLElement).onclick != null || - element.getAttribute('role') === 'button' - //|| element.className.includes("btn") - // MBTODO: intersect addEventListener + element.getAttribute('role') === 'button' + ); + //|| element.className.includes("btn") + // MBTODO: intersect addEventListener } //TODO: fix (typescript doesn't allow work when the guard is inside the function) @@ -73,9 +72,7 @@ function _getTarget(target: Element): Element | null { if (tag === 'INPUT') { return element; } - if (isClickable(element) || - getLabelAttribute(element) !== null - ) { + if (isClickable(element) || getLabelAttribute(element) !== null) { return element; } element = element.parentElement; @@ -84,22 +81,21 @@ function _getTarget(target: Element): Element | null { } export default function (app: App): void { - function getTargetLabel(target: Element): string { const dl = getLabelAttribute(target); if (dl !== null) { return dl; } - if (hasTag(target, "INPUT")) { - return getInputLabel(target) + if (hasTag(target, 'INPUT')) { + return getInputLabel(target); } if (isClickable(target)) { - let label = '' - if (target instanceof HTMLElement) { - label = app.sanitizer.getInnerTextSecure(target) - } - label = label || target.id || target.className - return normSpaces(label).slice(0, 100) + let label = ''; + if (target instanceof HTMLElement) { + label = app.sanitizer.getInnerTextSecure(target); + } + label = label || target.id || target.className; + return normSpaces(label).slice(0, 100); } return ''; } @@ -124,22 +120,18 @@ export default function (app: App): void { } }; - 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); - if (target !== mouseTarget) { - mouseTarget = target; - mouseTargetTime = performance.now(); - } - }, - ); + app.attachEventListener(document.documentElement, 'mouseover', (e: MouseEvent): void => { + const target = getTarget(e.target); + if (target !== mouseTarget) { + mouseTarget = target; + mouseTargetTime = performance.now(); + } + }); app.attachEventListener( document, 'mousemove', @@ -158,12 +150,10 @@ export default function (app: App): void { const id = app.nodes.getID(target); if (id !== undefined) { sendMouseMove(); - app.send(new - MouseClick( + app.send( + new MouseClick( id, - mouseTarget === target - ? Math.round(performance.now() - mouseTargetTime) - : 0, + mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0, getTargetLabel(target), getSelector(id, target), ), diff --git a/tracker/tracker/src/main/modules/performance.ts b/tracker/tracker/src/main/modules/performance.ts index c69293225..c921e45eb 100644 --- a/tracker/tracker/src/main/modules/performance.ts +++ b/tracker/tracker/src/main/modules/performance.ts @@ -1,20 +1,19 @@ -import type App from "../app/index.js"; -import { IN_BROWSER } from "../utils.js"; -import { PerformanceTrack } from "../../common/messages.js"; - +import type App from '../app/index.js'; +import { IN_BROWSER } from '../utils.js'; +import { PerformanceTrack } from '../../common/messages.js'; type Perf = { - memory: { - 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: { + 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: {} }; export const deviceMemory = IN_BROWSER ? ((navigator as any).deviceMemory || 0) * 1024 : 0; export const jsHeapSizeLimit = perf.memory.jsHeapSizeLimit || 0; @@ -30,7 +29,9 @@ export default function (app: App, opts: Partial): void { }, opts, ); - if (!options.capturePerformance) { return; } + if (!options.capturePerformance) { + return; + } let frames: number | undefined; let ticks: number | undefined; @@ -58,8 +59,8 @@ export default function (app: App, opts: Partial): void { if (frames === undefined || ticks === undefined) { return; } - app.send(new - PerformanceTrack( + app.send( + new PerformanceTrack( frames, ticks, perf.memory.totalJSHeapSize || 0, diff --git a/tracker/tracker/src/main/modules/scroll.ts b/tracker/tracker/src/main/modules/scroll.ts index 3515b58e6..656408824 100644 --- a/tracker/tracker/src/main/modules/scroll.ts +++ b/tracker/tracker/src/main/modules/scroll.ts @@ -1,14 +1,14 @@ -import type App from "../app/index.js"; -import { SetViewportScroll, SetNodeScroll } from "../../common/messages.js"; -import { isElementNode } from "../app/guards.js"; +import type App from '../app/index.js'; +import { SetViewportScroll, SetNodeScroll } from '../../common/messages.js'; +import { isElementNode } from '../app/guards.js'; export default function (app: App): void { let documentScroll = false; const nodeScroll: Map = new Map(); const sendSetViewportScroll = app.safe((): void => - app.send(new - SetViewportScroll( + app.send( + new SetViewportScroll( window.pageXOffset || (document.documentElement && document.documentElement.scrollLeft) || (document.body && document.body.scrollLeft) || @@ -39,7 +39,7 @@ export default function (app: App): void { if (isStart && isElementNode(node) && node.scrollLeft + node.scrollTop > 0) { nodeScroll.set(node, [node.scrollLeft, node.scrollTop]); } - }) + }); app.attachEventListener(window, 'scroll', (e: Event): void => { const target = e.target; diff --git a/tracker/tracker/src/main/modules/timing.ts b/tracker/tracker/src/main/modules/timing.ts index 30183dfa6..1ca670562 100644 --- a/tracker/tracker/src/main/modules/timing.ts +++ b/tracker/tracker/src/main/modules/timing.ts @@ -1,8 +1,7 @@ -import type App from "../app/index.js"; -import { hasTag } from "../app/guards.js"; -import { isURL } from "../utils.js"; -import { ResourceTiming, PageLoadTiming, PageRenderTiming } from "../../common/messages.js"; - +import type App from '../app/index.js'; +import { hasTag } from '../app/guards.js'; +import { isURL } from '../utils.js'; +import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../../common/messages.js'; // Inspired by https://github.com/WPO-Foundation/RUM-SpeedIndex/blob/master/src/rum-speedindex.js @@ -22,13 +21,11 @@ function getPaintBlocks(resources: ResourcesTimeMap): Array { for (let i = 0; i < elements.length; i++) { const element = elements[i]; let src = ''; - if (hasTag(element, "IMG")) { + if (hasTag(element, 'IMG')) { 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); if (matches !== null) { @@ -53,9 +50,7 @@ function getPaintBlocks(resources: ResourcesTimeMap): Array { ); const right = Math.min( rect.right, - window.innerWidth || - (document.documentElement && document.documentElement.clientWidth) || - 0, + window.innerWidth || (document.documentElement && document.documentElement.clientWidth) || 0, ); if (bottom <= top || right <= left) continue; const area = (bottom - top) * (right - left); @@ -64,18 +59,14 @@ function getPaintBlocks(resources: ResourcesTimeMap): Array { return paintBlocks; } -function calculateSpeedIndex( - firstContentfulPaint: number, - paintBlocks: Array, -): number { +function calculateSpeedIndex(firstContentfulPaint: number, paintBlocks: Array): number { let a = (Math.max( (document.documentElement && document.documentElement.clientWidth) || 0, window.innerWidth || 0, ) * Math.max( - (document.documentElement && document.documentElement.clientHeight) || - 0, + (document.documentElement && document.documentElement.clientHeight) || 0, window.innerHeight || 0, )) / 10; @@ -106,25 +97,23 @@ export default function (app: App, opts: Partial): void { if (!('PerformanceObserver' in window)) { options.captureResourceTimings = false; } - if (!options.captureResourceTimings) { return } // Resources are necessary for all timings + if (!options.captureResourceTimings) { + 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 (resources !== null) { resources[entry.name] = entry.startTime + entry.duration; } - app.send(new - ResourceTiming( + app.send( + new ResourceTiming( entry.startTime + performance.timing.navigationStart, entry.duration, - entry.responseStart && entry.startTime - ? entry.responseStart - entry.startTime - : 0, - entry.transferSize > entry.encodedBodySize - ? entry.transferSize - entry.encodedBodySize - : 0, + entry.responseStart && entry.startTime ? entry.responseStart - entry.startTime : 0, + entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0, entry.encodedBodySize || 0, entry.decodedBodySize || 0, entry.name, @@ -133,48 +122,45 @@ export default function (app: App, opts: Partial): void { ); } - const observer: PerformanceObserver = new PerformanceObserver( - (list) => list.getEntries().forEach(resourceTiming), - ) + const observer: PerformanceObserver = new PerformanceObserver((list) => + list.getEntries().forEach(resourceTiming), + ); - 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 + 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; } - observer.observe({ entryTypes: ['resource'] }) - }) - - app.attachStopCallback(function() { - observer.disconnect() - }) + observer.observe({ entryTypes: ['resource'] }); + }); + app.attachStopCallback(function () { + observer.disconnect(); + }); let firstPaint = 0, firstContentfulPaint = 0; if (options.capturePageLoadTimings) { - - let pageLoadTimingSent: boolean = false; + let pageLoadTimingSent = false; app.ticker.attach(() => { if (pageLoadTimingSent) { return; } if (firstPaint === 0 || firstContentfulPaint === 0) { - performance - .getEntriesByType('paint') - .forEach((entry: PerformanceEntry) => { - const { name, startTime } = entry; - switch (name) { - case 'first-paint': - firstPaint = startTime; - break; - case 'first-contentful-paint': - firstContentfulPaint = startTime; - break; - } - }); + performance.getEntriesByType('paint').forEach((entry: PerformanceEntry) => { + const { name, startTime } = entry; + switch (name) { + case 'first-paint': + firstPaint = startTime; + break; + case 'first-contentful-paint': + firstContentfulPaint = startTime; + break; + } + }); } if (performance.timing.loadEventEnd || performance.now() > 30000) { pageLoadTimingSent = true; @@ -188,8 +174,8 @@ export default function (app: App, opts: Partial): void { loadEventStart, loadEventEnd, } = performance.timing; - app.send(new - PageLoadTiming( + app.send( + new PageLoadTiming( requestStart - navigationStart || 0, responseStart - navigationStart || 0, responseEnd - navigationStart || 0, @@ -211,7 +197,7 @@ export default function (app: App, opts: Partial): void { interactiveWindowTickTime: number | null = 0, paintBlocks: Array | null = null; - let pageRenderTimingSent: boolean = false; + let pageRenderTimingSent = false; app.ticker.attach(() => { if (pageRenderTimingSent) { return; @@ -231,37 +217,28 @@ export default function (app: App, opts: Partial): void { if (time - interactiveWindowTickTime > 50) { interactiveWindowStartTime = time; } - interactiveWindowTickTime = - time - interactiveWindowStartTime > 5000 ? null : time; + interactiveWindowTickTime = time - interactiveWindowStartTime > 5000 ? null : time; } - if ( - (paintBlocks !== null && interactiveWindowTickTime === null) || - time > 30000 - ) { + if ((paintBlocks !== null && interactiveWindowTickTime === null) || time > 30000) { pageRenderTimingSent = true; resources = null; const speedIndex = paintBlocks === null ? 0 - : calculateSpeedIndex( - firstContentfulPaint || firstPaint, - paintBlocks, - ); + : calculateSpeedIndex(firstContentfulPaint || firstPaint, paintBlocks); const timeToInteractive = interactiveWindowTickTime === null ? Math.max( interactiveWindowStartTime, firstContentfulPaint, - performance.timing.domContentLoadedEventEnd - - performance.timing.navigationStart || 0, + performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart || + 0, ) : 0; - app.send(new - PageRenderTiming( + app.send( + new PageRenderTiming( speedIndex, - firstContentfulPaint > visuallyComplete - ? firstContentfulPaint - : visuallyComplete, + firstContentfulPaint > visuallyComplete ? firstContentfulPaint : visuallyComplete, timeToInteractive, ), ); diff --git a/tracker/tracker/src/main/modules/viewport.ts b/tracker/tracker/src/main/modules/viewport.ts index 29eac6806..b576b5384 100644 --- a/tracker/tracker/src/main/modules/viewport.ts +++ b/tracker/tracker/src/main/modules/viewport.ts @@ -1,9 +1,5 @@ -import type App from "../app/index.js"; -import { - SetPageLocation, - SetViewportSize, - SetPageVisibility, -} from "../../common/messages.js"; +import type App from '../app/index.js'; +import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../../common/messages.js'; export default function (app: App): void { let url: string, width: number, height: number; diff --git a/tracker/tracker/src/main/utils.ts b/tracker/tracker/src/main/utils.ts index 20dcb3a25..67aa68a94 100644 --- a/tracker/tracker/src/main/utils.ts +++ b/tracker/tracker/src/main/utils.ts @@ -13,48 +13,53 @@ export function normSpaces(str: string): string { // 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'; -const warnedFeatures: { [key: string]: boolean; } = {}; -export function deprecationWarn(nameOfFeature: string, useInstead: string, docsPath: string = "/"): void { - if (warnedFeatures[ nameOfFeature ]) { - return; - } - console.warn(`OpenReplay: ${ nameOfFeature } is deprecated. ${ useInstead ? `Please, use ${ useInstead } instead.` : "" } Visit ${DOCS_HOST}${docsPath} for more information.`) - warnedFeatures[ nameOfFeature ] = true; +const warnedFeatures: { [key: string]: boolean } = {}; +export function deprecationWarn(nameOfFeature: string, useInstead: string, docsPath = '/'): void { + if (warnedFeatures[nameOfFeature]) { + return; + } + console.warn( + `OpenReplay: ${nameOfFeature} is deprecated. ${ + useInstead ? `Please, use ${useInstead} instead.` : '' + } Visit ${DOCS_HOST}${docsPath} for more information.`, + ); + warnedFeatures[nameOfFeature] = true; } export function getLabelAttribute(e: Element): string | null { - let value = e.getAttribute("data-openreplay-label"); - if (value !== null) { - return value; - } - value = e.getAttribute("data-asayer-label"); - if (value !== null) { - deprecationWarn(`"data-asayer-label" attribute`, `"data-openreplay-label" attribute`, "/"); - } - return value; + let value = e.getAttribute('data-openreplay-label'); + if (value !== null) { + return value; + } + value = e.getAttribute('data-asayer-label'); + if (value !== null) { + deprecationWarn('"data-asayer-label" attribute', '"data-openreplay-label" attribute', '/'); + } + return value; } export function hasOpenreplayAttribute(e: Element, name: string): boolean { - const newName = `data-openreplay-${name}`; - if (e.hasAttribute(newName)) { - return true - } - const oldName = `data-asayer-${name}`; - if (e.hasAttribute(oldName)) { - deprecationWarn(`"${oldName}" attribute`, `"${newName}" attribute`, "/installation/sanitize-data"); - return true; - } - return false; + const newName = `data-openreplay-${name}`; + if (e.hasAttribute(newName)) { + return true; + } + const oldName = `data-asayer-${name}`; + if (e.hasAttribute(oldName)) { + deprecationWarn( + `"${oldName}" attribute`, + `"${newName}" attribute`, + '/installation/sanitize-data', + ); + return true; + } + return false; } - - diff --git a/tracker/tracker/src/main/vendors/finder/finder.ts b/tracker/tracker/src/main/vendors/finder/finder.ts index fc9f64af2..547ed2b08 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" + if ('html' === input.tagName.toLowerCase()) { + return 'html'; } const defaults: Options = { @@ -46,356 +46,370 @@ 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))) + 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 as Document + return rootNode.ownerDocument; } - return rootNode + return rootNode; } function bottomUpSearch(input: Element, limit: Limit, fallback?: () => Path | null): Path | null { - let path: Path | null = null - let 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()] + let level: Node[] = maybe(id(current)) || + maybe(...attr(current)) || + maybe(...classNames(current)) || + 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 (let node of level) { - node.level = i + for (const node of level) { + 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 (let candidate of paths) { + 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}), + 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 - })) + 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 - })) + 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 - } + penalty: 2, + }; } - return null + return null; } function any(): Node { return { - name: "*", - penalty: 3 - } + 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 - } + 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 (let node of stack[0]) { - paths.push(...combinations(stack.slice(1, stack.length), path.concat(node))) + for (const node of stack[0]) { + 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, input: Element, scope: Scope = { - counter: 0, - visited: new Map() -}): Node[][] { - const paths: Node[][] = [] +function optimize( + path: Path, + input: Element, + scope: Scope = { + counter: 0, + visited: new Map(), + }, +): 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 -} + escapeEverything: false, + isIdentifier: false, + quotes: 'single', + wrap: false, +}; function cssesc(string: string, opt: Partial = {}) { - const options = {...defaultOptions, ...opt} - if (options.quotes != "single" && options.quotes != "double") { - options.quotes = "single" + const options = { ...defaultOptions, ...opt }; + if (options.quotes != 'single' && options.quotes != 'double') { + 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) { + 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++) - if ((extra & 0xFC00) == 0xDC00) { + 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() + " " - } else if (character == "\\" || !isIdentifier && (character == "\"" && quote == character || character == "'" && quote == character) || isIdentifier && regexSingleEscape.test(character)) { - value = "\\" + character + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } else if ( + character == '\\' || + (!isIdentifier && + ((character == '"' && quote == character) || (character == "'" && quote == character))) || + (isIdentifier && regexSingleEscape.test(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); } } @@ -405,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 181663300..be0aed933 100644 --- a/tracker/tracker/src/webworker/BatchWriter.ts +++ b/tracker/tracker/src/webworker/BatchWriter.ts @@ -1,76 +1,74 @@ -import type Message from "../common/messages.js"; -import PrimitiveWriter from "./PrimitiveWriter.js"; -import { - BatchMeta, - Timestamp, -} from "../common/messages.js"; +import type Message from '../common/messages.js'; +import PrimitiveWriter from './PrimitiveWriter.js'; +import { BatchMeta, Timestamp } from '../common/messages.js'; export default class BatchWriter { - private nextIndex = 0 - private beaconSize = 2 * 1e5 // Default 200kB - private writer = new PrimitiveWriter(this.beaconSize) - private isEmpty = true + private nextIndex = 0; + private beaconSize = 2 * 1e5; // Default 200kB + private writer = new PrimitiveWriter(this.beaconSize); + private isEmpty = true; constructor( - private readonly pageNo: number, + private readonly pageNo: number, private timestamp: number, - private onBatch: (batch: Uint8Array) => void + private readonly onBatch: (batch: Uint8Array) => void, ) { - this.prepare() + this.prepare(); } private prepare(): void { if (!this.writer.isEmpty()) { - return + return; } - new BatchMeta(this.pageNo, this.nextIndex, this.timestamp).encode(this.writer) + new BatchMeta(this.pageNo, this.nextIndex, this.timestamp).encode(this.writer); } private write(message: Message): boolean { - const wasWritten = message.encode(this.writer) + const wasWritten = message.encode(this.writer); if (wasWritten) { - this.isEmpty = false - this.writer.checkpoint() - this.nextIndex++ + this.isEmpty = false; + this.writer.checkpoint(); + this.nextIndex++; } - return wasWritten + return wasWritten; } - private beaconSizeLimit = 1e6 + private beaconSizeLimit = 1e6; setBeaconSizeLimit(limit: number) { - this.beaconSizeLimit = limit + this.beaconSizeLimit = limit; } writeMessage(message: Message) { if (message instanceof Timestamp) { - this.timestamp = (message).timestamp + this.timestamp = (message).timestamp; } while (!this.write(message)) { - this.finaliseBatch() + this.finaliseBatch(); if (this.beaconSize === this.beaconSizeLimit) { - console.warn("OpenReplay: beacon size overflow. Skipping large message."); - this.writer.reset() - this.prepare() - this.isEmpty = true - return + console.warn('OpenReplay: beacon size overflow. Skipping large message.'); + this.writer.reset(); + this.prepare(); + this.isEmpty = true; + return; } // MBTODO: tempWriter for one message? - this.beaconSize = Math.min(this.beaconSize*2, this.beaconSizeLimit) - this.writer = new PrimitiveWriter(this.beaconSize) - this.prepare() - this.isEmpty = true + this.beaconSize = Math.min(this.beaconSize * 2, this.beaconSizeLimit); + this.writer = new PrimitiveWriter(this.beaconSize); + this.prepare(); + this.isEmpty = true; } } finaliseBatch() { - if (this.isEmpty) { return } - this.onBatch(this.writer.flush()) - this.prepare() - this.isEmpty = true + if (this.isEmpty) { + return; + } + this.onBatch(this.writer.flush()); + this.prepare(); + this.isEmpty = true; } clean() { - this.writer.reset() + this.writer.reset(); } - } diff --git a/tracker/tracker/src/webworker/PrimitiveWriter.ts b/tracker/tracker/src/webworker/PrimitiveWriter.ts index 587291bea..87924bf75 100644 --- a/tracker/tracker/src/webworker/PrimitiveWriter.ts +++ b/tracker/tracker/src/webworker/PrimitiveWriter.ts @@ -8,13 +8,13 @@ const textEncoder: { encode(str: string): Uint8Array } = const Len = str.length, resArr = new Uint8Array(Len * 3); let resPos = -1; - for (var point = 0, nextcode = 0, i = 0; i !== Len; ) { + for (let point = 0, nextcode = 0, i = 0; i !== Len; ) { (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*/; + resArr[(resPos += 1)] = 0xef; /*0b11101111*/ + resArr[(resPos += 1)] = 0xbf; /*0b10111111*/ + resArr[(resPos += 1)] = 0xbd; /*0b10111101*/ break; } // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae @@ -23,21 +23,18 @@ const textEncoder: { encode(str: string): Uint8Array } = 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); + (0x2 /*0b10*/ << 6) | ((point >>> 12) & 0x3f); /*0b00111111*/ resArr[(resPos += 1)] = - (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*/; + (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*/; + resArr[(resPos += 1)] = 0xef; /*0b11101111*/ + resArr[(resPos += 1)] = 0xbf; /*0b10111111*/ + resArr[(resPos += 1)] = 0xbd; /*0b10111101*/ continue; } } @@ -45,14 +42,11 @@ const textEncoder: { encode(str: string): Uint8Array } = 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)] = (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)] = (0x2 /*0b10*/ << 6) | ((point >>> 6) & 0x3f); /*0b00111111*/ + resArr[(resPos += 1)] = (0x2 /*0b10*/ << 6) | (point & 0x3f); /*0b00111111*/ } } return resArr.subarray(0, resPos + 1); @@ -60,13 +54,13 @@ const textEncoder: { encode(str: string): Uint8Array } = }; export default class PrimitiveWriter { - private offset: number = 0; - private checkpointOffset: number = 0; + private offset = 0; + private checkpointOffset = 0; private readonly data: Uint8Array; constructor(private readonly size: number) { this.data = new Uint8Array(size); } - checkpoint(): void { + checkpoint() { this.checkpointOffset = this.offset; } isEmpty(): boolean { @@ -78,7 +72,7 @@ export default class PrimitiveWriter { } 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; @@ -95,7 +89,7 @@ export default class PrimitiveWriter { 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; diff --git a/tracker/tracker/src/webworker/QueueSender.ts b/tracker/tracker/src/webworker/QueueSender.ts index d4686539d..b6aa3ed98 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() @@ -20,88 +20,83 @@ 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: Function, - private readonly onFailure: Function, + ingestBaseURL: string, + private readonly onUnauthorised: () => any, + private readonly onFailure: () => any, private readonly MAX_ATTEMPTS_COUNT = 10, private readonly ATTEMPT_TIMEOUT = 1000, ) { - this.ingestURL = ingestBaseURL + INGEST_PATH + this.ingestURL = ingestBaseURL + INGEST_PATH; } - authorise(token: string) { - this.token = token + authorise(token: string): void { + this.token = token; } - push(batch: Uint8Array) { + 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) { + 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 + private sendBatch(batch: Uint8Array): void { + this.busy = true; fetch(this.ingestURL, { body: batch, method: 'POST', headers: { - "Authorization": "Bearer " + this.token, + Authorization: 'Bearer ' + this.token, //"Content-Type": "", }, keepalive: batch.length < KEEPALIVE_SIZE_LIMIT, }) - .then(r => { - if (r.status === 401) { // TODO: continuous session ? - this.busy = false - this.onUnauthorised() - return - } else if (r.status >= 400) { - this.retry(batch) - return - } - - // Success - this.attemptsCount = 0 - const nextBatch = this.queue.shift() - if (nextBatch) { - this.sendBatch(nextBatch) - } else { - this.busy = false - } - }) - .catch(e => { - console.warn("OpenReplay:", e) - this.retry(batch) - }) + .then((r) => { + if (r.status === 401) { + // TODO: continuous session ? + this.busy = false; + this.onUnauthorised(); + return; + } else if (r.status >= 400) { + this.retry(batch); + return; + } + // Success + this.attemptsCount = 0; + const nextBatch = this.queue.shift(); + if (nextBatch) { + this.sendBatch(nextBatch); + } else { + this.busy = false; + } + }) + .catch((e) => { + 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 7d1cb0393..a22e75303 100644 --- a/tracker/tracker/src/webworker/index.ts +++ b/tracker/tracker/src/webworker/index.ts @@ -1,126 +1,125 @@ -import type Message from "../common/messages.js"; -import { WorkerMessageData } from "../common/webworker.js"; +import type Message from '../common/messages.js'; +import { WorkerMessageData } from '../common/webworker.js'; -import { - classes, - SetPageVisibility, - MouseMove, -} from "../common/messages.js"; -import QueueSender from "./QueueSender.js"; -import BatchWriter from "./BatchWriter.js"; +import { classes, SetPageVisibility } from '../common/messages.js'; +import QueueSender from './QueueSender.js'; +import BatchWriter from './BatchWriter.js'; enum WorkerStatus { NotActive, Starting, Stopping, - Active + Active, } -const AUTO_SEND_INTERVAL = 10 * 1000 +const AUTO_SEND_INTERVAL = 10 * 1000; -let sender: QueueSender | null = null -let writer: BatchWriter | null = null +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() { - workerStatus = WorkerStatus.Stopping +function reset(): void { + workerStatus = WorkerStatus.Stopping; if (sendIntervalID !== null) { clearInterval(sendIntervalID); sendIntervalID = null; } if (writer) { - writer.clean() - writer = null + writer.clean(); + writer = null; } - workerStatus = WorkerStatus.NotActive + workerStatus = WorkerStatus.NotActive; } -function resetCleanQueue() { +function resetCleanQueue(): void { if (sender) { - sender.clean() - sender = null - } - reset() + sender.clean(); + sender = null; + } + reset(); } -let sendIntervalID: ReturnType | null = null -let restartTimeoutID: ReturnType +let sendIntervalID: ReturnType | null = null; +let restartTimeoutID: ReturnType; -self.onmessage = ({ data }: MessageEvent) => { +self.onmessage = ({ data }: MessageEvent): any => { if (data == null) { - send() // TODO: sendAll? - return + send(); // TODO: sendAll? + return; } - if (data === "stop") { - send() - reset() - return + if (data === 'stop') { + send(); + reset(); + return; } if (Array.isArray(data)) { 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; // Message[] data.forEach((data) => { - const message: Message = new (classes.get(data._id))(); - Object.assign(message, data) + // @ts-ignore + const message: Message = new (classes.get(data._id))(); + Object.assign(message, data); if (message instanceof SetPageVisibility) { - if ( (message).hidden) { - restartTimeoutID = setTimeout(() => self.postMessage("restart"), 30*60*1000) + // @ts-ignore + if ((message).hidden) { + 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") + () => { + // onUnauthorised + self.postMessage('restart'); }, - () => { // onFailure - resetCleanQueue() - self.postMessage("failed") + () => { + // onFailure + resetCleanQueue(); + self.postMessage('failed'); }, data.connAttemptCount, data.connAttemptGap, - ) + ); writer = new BatchWriter( data.pageNo, data.timestamp, // onBatch - batch => sender && sender.push(batch) - ) + (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 (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; } }; diff --git a/tracker/tracker/src/webworker/tsconfig.json b/tracker/tracker/src/webworker/tsconfig.json index 6794ea55c..52228f9af 100644 --- a/tracker/tracker/src/webworker/tsconfig.json +++ b/tracker/tracker/src/webworker/tsconfig.json @@ -3,7 +3,5 @@ "compilerOptions": { "lib": ["es6", "webworker"] }, - "references": [ - { "path": "../common" } - ] + "references": [{ "path": "../common" }] }