tracker: port 14.0.10 changes to 15.0.0 timeline
tracker 14.0.10, fixes memory leaks, iframe tracking stability, etc change map/set to weakmap/set where possible, check canvas observers on time intervals and destroy peers; run node list maintainer every 30 sec (50ms ticks) tracker: better crossdomain check; angularMode -> forceNgOff toggle potential performance fixes for 14.x.x iframe tracking
This commit is contained in:
parent
62600fdf6a
commit
0002950e3c
40 changed files with 1189 additions and 243 deletions
BIN
tracker/tracker-assist/.yarn/install-state.gz
Normal file
BIN
tracker/tracker-assist/.yarn/install-state.gz
Normal file
Binary file not shown.
1
tracker/tracker-assist/.yarnrc.yml
Normal file
1
tracker/tracker-assist/.yarnrc.yml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
nodeLinker: node-modules
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
## 10.0.0
|
||||||
|
|
||||||
|
- memory handling improvements to prevent possible leaks on sessions with multiple canvas nodes
|
||||||
|
- use new tracker.waitStatus api to wait for restarts
|
||||||
|
|
||||||
## 9.0.0
|
## 9.0.0
|
||||||
|
|
||||||
- support for message compression inside plugin (requires v1.18 frontend)
|
- support for message compression inside plugin (requires v1.18 frontend)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@openreplay/tracker-assist",
|
"name": "@openreplay/tracker-assist",
|
||||||
"description": "Tracker plugin for screen assistance through the WebRTC",
|
"description": "Tracker plugin for screen assistance through the WebRTC",
|
||||||
"version": "9.0.2-beta.2",
|
"version": "10.0.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"WebRTC",
|
"WebRTC",
|
||||||
"assistance",
|
"assistance",
|
||||||
|
|
@ -16,11 +16,11 @@
|
||||||
"tsrun": "tsc",
|
"tsrun": "tsc",
|
||||||
"lint": "eslint src --ext .ts,.js --fix --quiet",
|
"lint": "eslint src --ext .ts,.js --fix --quiet",
|
||||||
"build": "bun run replace-pkg-version && bun run build-es && bun run build-cjs",
|
"build": "bun run replace-pkg-version && bun run build-es && bun run build-cjs",
|
||||||
"build-es": "rm -Rf lib && tsc && bun run replace-req-version",
|
"build-es": "rm -Rf lib && tsc --project tsconfig.json && bun run replace-req-version",
|
||||||
"build-cjs": "rm -Rf cjs && tsc --project tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > cjs/package.json && bun run replace-paths && bun run replace-req-version",
|
"build-cjs": "rm -Rf cjs && tsc --project tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > cjs/package.json && bun run replace-req-version",
|
||||||
"replace-paths": "replace-in-files cjs/* --string='@openreplay/tracker' --replacement='@openreplay/tracker/cjs' && replace-in-files cjs/* --string='/lib/' --replacement='/'",
|
"replace-paths": "replace-in-files cjs/* --string='@openreplay/tracker' --replacement='@openreplay/tracker/cjs' && replace-in-files cjs/* --string='/lib/' --replacement='/'",
|
||||||
"replace-pkg-version": "sh pkgver.sh",
|
"replace-pkg-version": "sh pkgver.sh",
|
||||||
"replace-req-version": "replace-in-files lib/* cjs/* --string='REQUIRED_TRACKER_VERSION' --replacement='13.0.0'",
|
"replace-req-version": "replace-in-files lib/* cjs/* --string='REQUIRED_TRACKER_VERSION' --replacement='14.0.10'",
|
||||||
"prepublishOnly": "bun run test && bun run build",
|
"prepublishOnly": "bun run test && bun run build",
|
||||||
"lint-front": "lint-staged",
|
"lint-front": "lint-staged",
|
||||||
"test": "jest --coverage=false",
|
"test": "jest --coverage=false",
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
"socket.io-client": "^4.7.2"
|
"socket.io-client": "^4.7.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@openreplay/tracker": "^14.0.0 || ^13.0.0"
|
"@openreplay/tracker": "^14.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@openreplay/tracker": "file:../tracker",
|
"@openreplay/tracker": "file:../tracker",
|
||||||
|
|
@ -49,11 +49,12 @@
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"replace-in-files-cli": "^1.0.0",
|
"replace-in-files-cli": "^1.0.0",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"typescript": "^4.6.0-dev.20211126"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,mjs,cjs,jsx,ts,tsx}": [
|
"*.{js,mjs,cjs,jsx,ts,tsx}": [
|
||||||
"eslint --fix --quiet"
|
"eslint --fix --quiet"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@4.5.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ export default class Assist {
|
||||||
private socket: Socket | null = null
|
private socket: Socket | null = null
|
||||||
private peer: Peer | null = null
|
private peer: Peer | null = null
|
||||||
private canvasPeers: Record<number, Peer | null> = {}
|
private canvasPeers: Record<number, Peer | null> = {}
|
||||||
|
private canvasNodeCheckers: Map<number, any> = new Map()
|
||||||
private assistDemandedRestart = false
|
private assistDemandedRestart = false
|
||||||
private callingState: CallingState = CallingState.False
|
private callingState: CallingState = CallingState.False
|
||||||
private remoteControl: RemoteControl | null = null;
|
private remoteControl: RemoteControl | null = null;
|
||||||
|
|
@ -674,6 +675,20 @@ export default class Assist {
|
||||||
app.debug.error,
|
app.debug.error,
|
||||||
)
|
)
|
||||||
this.canvasMap.set(id, canvasHandler)
|
this.canvasMap.set(id, canvasHandler)
|
||||||
|
if (this.canvasNodeCheckers.has(id)) {
|
||||||
|
clearInterval(this.canvasNodeCheckers.get(id))
|
||||||
|
}
|
||||||
|
const int = setInterval(() => {
|
||||||
|
const isPresent = node.ownerDocument.defaultView && node.isConnected
|
||||||
|
if (!isPresent) {
|
||||||
|
canvasHandler.stop()
|
||||||
|
this.canvasMap.delete(id)
|
||||||
|
this.canvasPeers[id]?.destroy()
|
||||||
|
this.canvasPeers[id] = null
|
||||||
|
clearInterval(int)
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
this.canvasNodeCheckers.set(id, int)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -703,6 +718,10 @@ export default class Assist {
|
||||||
this.socket.disconnect()
|
this.socket.disconnect()
|
||||||
this.app.debug.log('Socket disconnected')
|
this.app.debug.log('Socket disconnected')
|
||||||
}
|
}
|
||||||
|
this.canvasMap.clear()
|
||||||
|
this.canvasPeers = []
|
||||||
|
this.canvasNodeCheckers.forEach((int) => clearInterval(int))
|
||||||
|
this.canvasNodeCheckers.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Mouse from './Mouse.js'
|
import Mouse from './Mouse.js'
|
||||||
import ConfirmWindow from './ConfirmWindow/ConfirmWindow.js'
|
import ConfirmWindow from './ConfirmWindow/ConfirmWindow.js'
|
||||||
import { controlConfirmDefault, } from './ConfirmWindow/defaults.js'
|
import { controlConfirmDefault, } from './ConfirmWindow/defaults.js'
|
||||||
import type { Options as AssistOptions, } from './Assist'
|
import type { Options as AssistOptions, } from './Assist.js'
|
||||||
|
|
||||||
export enum RCStatus {
|
export enum RCStatus {
|
||||||
Disabled,
|
Disabled,
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export const pkgVersion = "9.0.2-beta.2";
|
export const pkgVersion = "10.0.0";
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"module": "es6",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "Node",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "./lib",
|
"outDir": "./lib",
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2,253 +2,289 @@
|
||||||
|
|
||||||
- new webvitals messages source
|
- new webvitals messages source
|
||||||
|
|
||||||
# 14.0.10
|
## 14.0.10
|
||||||
|
|
||||||
- adjust timestamps for messages from tracker instances inside child iframes (if they were loaded later)
|
- adjust timestamps for messages from tracker instances inside child iframes (if they were loaded later)
|
||||||
- restart child trackers if parent tracker is restarted
|
- restart child trackers if parent tracker is restarted
|
||||||
- fixes for general stability of crossdomain iframe tracking
|
- fixes for general stability of crossdomain iframe tracking
|
||||||
|
- refactored usage of memory for everything regarding dom nodes to prevent possible memory leaks (i.e switched Map/Set to WeakMap/WeakSet where possible)
|
||||||
|
- introduced configurable Maintainer to drop nodes that are not in the dom anymore from memory;
|
||||||
|
|
||||||
# 14.0.9
|
```
|
||||||
|
interface MaintainerOptions {
|
||||||
|
/**
|
||||||
|
* Run cleanup each X ms
|
||||||
|
*
|
||||||
|
* @default 30 * 1000
|
||||||
|
* */
|
||||||
|
interval: number
|
||||||
|
/**
|
||||||
|
* Maintainer checks nodes in small batches over 50ms timeouts
|
||||||
|
*
|
||||||
|
* @default 2500
|
||||||
|
* */
|
||||||
|
batchSize: number
|
||||||
|
/**
|
||||||
|
* @default true
|
||||||
|
* */
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
new Tracker({
|
||||||
|
...yourOptions,
|
||||||
|
nodes: {
|
||||||
|
maintainer: {
|
||||||
|
interval: 60 * 1000,
|
||||||
|
batchSize: 2500,
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- added `startCallback` option callback to tracker.start options (returns `{ success: false, reason: string } | { success: true, sessionToken, userUUID, sessionID }`)
|
||||||
|
|
||||||
|
## 14.0.9
|
||||||
|
|
||||||
- more stable crossdomain iframe tracking (refactored child/parent process discovery)
|
- more stable crossdomain iframe tracking (refactored child/parent process discovery)
|
||||||
- checks for bad start error
|
- checks for bad start error
|
||||||
|
|
||||||
# 14.0.8
|
## 14.0.8
|
||||||
|
|
||||||
- use separate library to handle network requests ([@openreplay/network-proxy](https://www.npmjs.com/package/@openreplay/network-proxy))
|
- use separate library to handle network requests ([@openreplay/network-proxy](https://www.npmjs.com/package/@openreplay/network-proxy))
|
||||||
- fixes for window.message listeners
|
- fixes for window.message listeners
|
||||||
|
|
||||||
# 14.0.7
|
## 14.0.7
|
||||||
|
|
||||||
- check for stopping status during restarts
|
- check for stopping status during restarts
|
||||||
- restart if token expired during canvas fetch
|
- restart if token expired during canvas fetch
|
||||||
|
|
||||||
# 14.0.6
|
## 14.0.6
|
||||||
|
|
||||||
- support feature off toggle for feature flags and usability testing
|
- support feature off toggle for feature flags and usability testing
|
||||||
- additional checks for canvas snapshots
|
- additional checks for canvas snapshots
|
||||||
|
|
||||||
# 14.0.5
|
## 14.0.5
|
||||||
|
|
||||||
- remove canvas snapshot interval if canvas is gone
|
- remove canvas snapshot interval if canvas is gone
|
||||||
|
|
||||||
# 14.0.4
|
## 14.0.4
|
||||||
|
|
||||||
- remove reject from start
|
- remove reject from start
|
||||||
|
|
||||||
# 14.0.3
|
## 14.0.3
|
||||||
|
|
||||||
- send integer instead of float for normalizedX/Y coords (basically moving from 0-100 to 0-10000 range)
|
- send integer instead of float for normalizedX/Y coords (basically moving from 0-100 to 0-10000 range)
|
||||||
|
|
||||||
# 14.0.2
|
## 14.0.2
|
||||||
|
|
||||||
- fix logger check
|
- fix logger check
|
||||||
|
|
||||||
# 14.0.0 & .1
|
## 14.0.0 & .1
|
||||||
|
|
||||||
- titles for tabs
|
- titles for tabs
|
||||||
- new `MouseClick` message to introduce heatmaps instead of clickmaps
|
- new `MouseClick` message to introduce heatmaps instead of clickmaps
|
||||||
- crossdomain iframe tracking functionality
|
- crossdomain iframe tracking functionality
|
||||||
- updated graphql plugin and messages
|
- updated graphql plugin and messages
|
||||||
|
|
||||||
# 13.0.2
|
## 13.0.2
|
||||||
|
|
||||||
- more file extensions for canvas
|
- more file extensions for canvas
|
||||||
|
|
||||||
# 13.0.1
|
## 13.0.1
|
||||||
|
|
||||||
- moved canvas snapshots to webp, additional option to utilize useAnimationFrame method (for webgl)
|
- moved canvas snapshots to webp, additional option to utilize useAnimationFrame method (for webgl)
|
||||||
- simpler, faster canvas recording manager
|
- simpler, faster canvas recording manager
|
||||||
|
|
||||||
# 13.0.0
|
## 13.0.0
|
||||||
|
|
||||||
- `assistOnly` flag for tracker options (EE only feature)
|
- `assistOnly` flag for tracker options (EE only feature)
|
||||||
|
|
||||||
# 12.0.12
|
## 12.0.12
|
||||||
|
|
||||||
- fix for potential redux plugin issues after .11 ...
|
- fix for potential redux plugin issues after .11 ...
|
||||||
|
|
||||||
# 12.0.11
|
## 12.0.11
|
||||||
|
|
||||||
- better restart on unauth (new token assign for long sessions)
|
- better restart on unauth (new token assign for long sessions)
|
||||||
- more safeguards around arraybuffer and dataview types for network proxy
|
- more safeguards around arraybuffer and dataview types for network proxy
|
||||||
|
|
||||||
# 12.0.10
|
## 12.0.10
|
||||||
|
|
||||||
- improved logs for node binding errors, full nodelist clear before start, getSessionInfo method
|
- improved logs for node binding errors, full nodelist clear before start, getSessionInfo method
|
||||||
|
|
||||||
# 12.0.9
|
## 12.0.9
|
||||||
|
|
||||||
- moved logging to query
|
- moved logging to query
|
||||||
|
|
||||||
# 12.0.8
|
## 12.0.8
|
||||||
|
|
||||||
- better logging for network batches
|
- better logging for network batches
|
||||||
|
|
||||||
# 12.0.7
|
## 12.0.7
|
||||||
|
|
||||||
- fixes for window.open reinit method
|
- fixes for window.open reinit method
|
||||||
|
|
||||||
# 12.0.6
|
## 12.0.6
|
||||||
|
|
||||||
- allow network sanitizer to return null (will ignore network req)
|
- allow network sanitizer to return null (will ignore network req)
|
||||||
|
|
||||||
# 12.0.5
|
## 12.0.5
|
||||||
|
|
||||||
- patch for img.ts srcset detector
|
- patch for img.ts srcset detector
|
||||||
|
|
||||||
# 12.0.4
|
## 12.0.4
|
||||||
|
|
||||||
- patch for email sanitizer (supports + now)
|
- patch for email sanitizer (supports + now)
|
||||||
- update fflate version for better compression
|
- update fflate version for better compression
|
||||||
- `disableCanvas` option to disable canvas capture
|
- `disableCanvas` option to disable canvas capture
|
||||||
- better check for adopted stylesheets in doc (old browser support)
|
- better check for adopted stylesheets in doc (old browser support)
|
||||||
|
|
||||||
# 12.0.3
|
## 12.0.3
|
||||||
|
|
||||||
- fixed scaling option for canvas (to ignore window.devicePixelRatio and always render the canvas as 1)
|
- fixed scaling option for canvas (to ignore window.devicePixelRatio and always render the canvas as 1)
|
||||||
|
|
||||||
# 12.0.2
|
## 12.0.2
|
||||||
|
|
||||||
- fix for canvas snapshot check
|
- fix for canvas snapshot check
|
||||||
|
|
||||||
# 12.0.1
|
## 12.0.1
|
||||||
|
|
||||||
- pause canvas snapshotting when its offscreen
|
- pause canvas snapshotting when its offscreen
|
||||||
|
|
||||||
# 12.0.0
|
## 12.0.0
|
||||||
|
|
||||||
- offline session recording and manual sending
|
- offline session recording and manual sending
|
||||||
- conditional recording with 30s buffer
|
- conditional recording with 30s buffer
|
||||||
- websockets tracking hook
|
- websockets tracking hook
|
||||||
|
|
||||||
# 11.0.5
|
## 11.0.5
|
||||||
|
|
||||||
- add method to restart canvas tracking (in case of context recreation)
|
- add method to restart canvas tracking (in case of context recreation)
|
||||||
- scan dom tree for canvas els on tracker start
|
- scan dom tree for canvas els on tracker start
|
||||||
|
|
||||||
# 11.0.4
|
## 11.0.4
|
||||||
|
|
||||||
- some additional security for canvas capture (check if canvas el itself is obscured/ignored)
|
- some additional security for canvas capture (check if canvas el itself is obscured/ignored)
|
||||||
|
|
||||||
# 11.0.3
|
## 11.0.3
|
||||||
|
|
||||||
- move all logs under internal debugger
|
- move all logs under internal debugger
|
||||||
- fix for XHR proxy ORSC 'abort' state
|
- fix for XHR proxy ORSC 'abort' state
|
||||||
|
|
||||||
# 11.0.1 & 11.0.2
|
## 11.0.1 & 11.0.2
|
||||||
|
|
||||||
- minor fixes and refactoring
|
- minor fixes and refactoring
|
||||||
|
|
||||||
# 11.0.0
|
## 11.0.0
|
||||||
|
|
||||||
- canvas support
|
- canvas support
|
||||||
- some safety guards for iframe components
|
- some safety guards for iframe components
|
||||||
- user testing module
|
- user testing module
|
||||||
|
|
||||||
# 10.0.2
|
## 10.0.2
|
||||||
|
|
||||||
- fix default ignore headers
|
- fix default ignore headers
|
||||||
|
|
||||||
# 10.0.1
|
## 10.0.1
|
||||||
|
|
||||||
- network proxy api is now default turned on
|
- network proxy api is now default turned on
|
||||||
|
|
||||||
# 10.0.0
|
## 10.0.0
|
||||||
|
|
||||||
- networkRequest message changed to include `TransferredBodySize`
|
- networkRequest message changed to include `TransferredBodySize`
|
||||||
- tracker now attempts to create proxy for beacon api as well (if its in scope of the current env)
|
- tracker now attempts to create proxy for beacon api as well (if its in scope of the current env)
|
||||||
- safe wrapper for angular apps
|
- safe wrapper for angular apps
|
||||||
- better browser lag handling (and some performance improvements as a bonus)
|
- better browser lag handling (and some performance improvements as a bonus)
|
||||||
|
|
||||||
# 9.0.11
|
## 9.0.11
|
||||||
|
|
||||||
- new `resetTabOnWindowOpen` option to fix window.open issue with sessionStorage being inherited (replicating tabId bug), users still should use 'noopener=true' in window.open to prevent it in general...
|
- new `resetTabOnWindowOpen` option to fix window.open issue with sessionStorage being inherited (replicating tabId bug), users still should use 'noopener=true' in window.open to prevent it in general...
|
||||||
- do not create BC channel in iframe context, add regeneration of tabid incase of duplication
|
- do not create BC channel in iframe context, add regeneration of tabid incase of duplication
|
||||||
|
|
||||||
# 9.0.10
|
## 9.0.10
|
||||||
|
|
||||||
- added `excludedResourceUrls` to timings options to better sanitize network data
|
- added `excludedResourceUrls` to timings options to better sanitize network data
|
||||||
|
|
||||||
# 9.0.9
|
## 9.0.9
|
||||||
|
|
||||||
- Fix for `{disableStringDict: true}` behavior
|
- Fix for `{disableStringDict: true}` behavior
|
||||||
|
|
||||||
# 9.0.8
|
## 9.0.8
|
||||||
|
|
||||||
- added slight delay to iframe handler (rapid updates of stacked frames used to break player)
|
- added slight delay to iframe handler (rapid updates of stacked frames used to break player)
|
||||||
|
|
||||||
# 9.0.7
|
## 9.0.7
|
||||||
|
|
||||||
- fix for `getSessionURL` method
|
- fix for `getSessionURL` method
|
||||||
|
|
||||||
# 9.0.6
|
## 9.0.6
|
||||||
|
|
||||||
- added `tokenUrlMatcher` option to network settings, allowing to ingest session token header to custom allowed urls
|
- added `tokenUrlMatcher` option to network settings, allowing to ingest session token header to custom allowed urls
|
||||||
|
|
||||||
# 9.0.5
|
## 9.0.5
|
||||||
|
|
||||||
- same fixes but for fetch proxy
|
- same fixes but for fetch proxy
|
||||||
|
|
||||||
# 9.0.2 & 9.0.3 & 9.0.4
|
## 9.0.2 & 9.0.3 & 9.0.4
|
||||||
|
|
||||||
- fixes for "setSessionTokenHeader" method
|
- fixes for "setSessionTokenHeader" method
|
||||||
|
|
||||||
# 9.0.1
|
## 9.0.1
|
||||||
|
|
||||||
- Warning about SSR mode
|
- Warning about SSR mode
|
||||||
- Prevent crashes due to network proxy in SSR
|
- Prevent crashes due to network proxy in SSR
|
||||||
|
|
||||||
# 9.0.0
|
## 9.0.0
|
||||||
|
|
||||||
- Option to disable string dictionary `{disableStringDict: true}` in Tracker constructor
|
- Option to disable string dictionary `{disableStringDict: true}` in Tracker constructor
|
||||||
- Introduced Feature flags api
|
- Introduced Feature flags api
|
||||||
- Fixed input durations recorded on programmable autofill
|
- Fixed input durations recorded on programmable autofill
|
||||||
- change InputMode from enum to const Object
|
- change InputMode from enum to const Object
|
||||||
|
|
||||||
# 8.1.2
|
## 8.1.2
|
||||||
|
|
||||||
- option to disable string dictionary `{disableStringDict: true}` in Tracker constructor
|
- option to disable string dictionary `{disableStringDict: true}` in Tracker constructor
|
||||||
|
|
||||||
# 8.1.1
|
## 8.1.1
|
||||||
|
|
||||||
[collective patch]
|
[collective patch]
|
||||||
|
|
||||||
- Console and network are now using proxy objects to capture calls (opt in for network), use ` { network: { useProxy: true } }` to enable it
|
- Console and network are now using proxy objects to capture calls (opt in for network), use ` { network: { useProxy: true } }` to enable it
|
||||||
- Force disable Multitab feature for old browsers (2016 and older + safari 14)
|
- Force disable Multitab feature for old browsers (2016 and older + safari 14)
|
||||||
|
|
||||||
# 8.0.0
|
## 8.0.0
|
||||||
|
|
||||||
- **[breaking]** support for multi-tab sessions
|
- **[breaking]** support for multi-tab sessions
|
||||||
|
|
||||||
# 7.0.4
|
## 7.0.4
|
||||||
|
|
||||||
- option to disable string dictionary `{disableStringDict: true}` in Tracker constructor
|
- option to disable string dictionary `{disableStringDict: true}` in Tracker constructor
|
||||||
|
|
||||||
# 7.0.3
|
## 7.0.3
|
||||||
|
|
||||||
- Prevent auto restart after manual stop
|
- Prevent auto restart after manual stop
|
||||||
|
|
||||||
# 7.0.2
|
## 7.0.2
|
||||||
|
|
||||||
- fixed header sanitization for axios causing empty string in some cases
|
- fixed header sanitization for axios causing empty string in some cases
|
||||||
|
|
||||||
# 7.0.1
|
## 7.0.1
|
||||||
|
|
||||||
- fix time inputs capturing
|
- fix time inputs capturing
|
||||||
- add option `{ network: { captureInIframes: boolean } }` to disable network tracking inside iframes (default true)
|
- add option `{ network: { captureInIframes: boolean } }` to disable network tracking inside iframes (default true)
|
||||||
- added option `{ network: { axiosInstances: AxiosInstance[] } }` to include custom axios instances for better tracking
|
- added option `{ network: { axiosInstances: AxiosInstance[] } }` to include custom axios instances for better tracking
|
||||||
|
|
||||||
# 7.0.0
|
## 7.0.0
|
||||||
|
|
||||||
- **[breaking]** added gzip compression to large messages
|
- **[breaking]** added gzip compression to large messages
|
||||||
- fix email regexp to significantly improve performance
|
- fix email regexp to significantly improve performance
|
||||||
|
|
||||||
# 6.0.2
|
## 6.0.2
|
||||||
|
|
||||||
- fix network tracking for same domain iframes created by js code
|
- fix network tracking for same domain iframes created by js code
|
||||||
|
|
||||||
# 6.0.1
|
## 6.0.1
|
||||||
|
|
||||||
- fix webworker writer re-init request
|
- fix webworker writer re-init request
|
||||||
- remove useless logs
|
- remove useless logs
|
||||||
|
|
@ -256,7 +292,7 @@
|
||||||
- fix iframe handling
|
- fix iframe handling
|
||||||
- optimise node counting for dom drop
|
- optimise node counting for dom drop
|
||||||
|
|
||||||
# 6.0.0
|
## 6.0.0
|
||||||
|
|
||||||
**(Compatible with OpenReplay v1.11.0+ only)**
|
**(Compatible with OpenReplay v1.11.0+ only)**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,29 @@
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./lib/index.js",
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"require": "./dist/cjs/index.js",
|
||||||
|
"import": "./dist/lib/index.js",
|
||||||
|
"types": "./dist/lib/main/index.d.ts"
|
||||||
|
},
|
||||||
|
"./cjs": {
|
||||||
|
"require": "./dist/cjs/index.js",
|
||||||
|
"types": "./dist/cjs/main/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/lib/**/*",
|
||||||
|
"dist/cjs/**/*"
|
||||||
|
],
|
||||||
|
"main": "./dist/cjs/index.js",
|
||||||
|
"module": "./dist/lib/index.js",
|
||||||
|
"types": "./dist/lib/main/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint src --ext .ts,.js --fix --quiet",
|
"lint": "eslint src --ext .ts,.js --fix --quiet",
|
||||||
"clean": "rm -Rf build && rm -Rf lib && rm -Rf cjs",
|
"clean": "rm -Rf build && rm -Rf lib && rm -Rf cjs",
|
||||||
"tscRun": "tsc -b src/main && tsc -b src/webworker && tsc --project src/main/tsconfig-cjs.json",
|
"build:common": "tsc -b src/common",
|
||||||
"rollup": "rollup --config rollup.config.js",
|
"build": "yarn run clean && yarn build:common && rollup --config rollup.config.js",
|
||||||
"compile": "node --experimental-modules --experimental-json-modules scripts/compile.cjs",
|
|
||||||
"build": "bun run clean && bun run tscRun && bun run rollup && bun run compile",
|
|
||||||
"lint-front": "lint-staged",
|
"lint-front": "lint-staged",
|
||||||
"test": "jest --coverage=false",
|
"test": "jest --coverage=false",
|
||||||
"test:ci": "jest --coverage=true",
|
"test:ci": "jest --coverage=true",
|
||||||
|
|
@ -32,7 +47,9 @@
|
||||||
"@jest/globals": "^29.3.1",
|
"@jest/globals": "^29.3.1",
|
||||||
"@rollup/plugin-babel": "^6.0.4",
|
"@rollup/plugin-babel": "^6.0.4",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
|
"@rollup/plugin-replace": "^6.0.1",
|
||||||
"@rollup/plugin-terser": "0.4.4",
|
"@rollup/plugin-terser": "0.4.4",
|
||||||
|
"@rollup/plugin-typescript": "^12.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||||
"@typescript-eslint/parser": "^5.30.0",
|
"@typescript-eslint/parser": "^5.30.0",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
|
|
@ -46,12 +63,13 @@
|
||||||
"rollup": "^4.1.4",
|
"rollup": "^4.1.4",
|
||||||
"semver": "^6.3.0",
|
"semver": "^6.3.0",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medv/finder": "^3.2.0",
|
"@medv/finder": "^3.2.0",
|
||||||
"@openreplay/network-proxy": "^1.0.3",
|
"@openreplay/network-proxy": "^1.0.3",
|
||||||
"error-stack-parser": "^2.0.6",
|
"error-stack-parser": "^2.0.6",
|
||||||
|
"error-stack-parser-es": "^0.1.5",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"web-vitals": "^4.2.3"
|
"web-vitals": "^4.2.3"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,79 @@
|
||||||
import resolve from '@rollup/plugin-node-resolve'
|
import resolve from '@rollup/plugin-node-resolve'
|
||||||
import { babel } from '@rollup/plugin-babel'
|
import typescript from '@rollup/plugin-typescript'
|
||||||
import terser from '@rollup/plugin-terser';
|
import terser from '@rollup/plugin-terser'
|
||||||
|
import replace from '@rollup/plugin-replace'
|
||||||
|
import { rollup } from 'rollup'
|
||||||
|
import { createRequire } from 'module'
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
const packageConfig = require('./package.json')
|
||||||
|
|
||||||
export default {
|
export default async () => {
|
||||||
input: 'build/webworker/index.js',
|
const webworkerContent = await buildWebWorker()
|
||||||
output: {
|
|
||||||
file: 'build/webworker.js',
|
const commonPlugins = [
|
||||||
format: 'cjs',
|
resolve(),
|
||||||
},
|
// terser(),
|
||||||
plugins: [resolve(), babel({ babelHelpers: 'bundled' }), terser({ mangle: { reserved: ['$'] } })],
|
replace({
|
||||||
|
preventAssignment: true,
|
||||||
|
values: {
|
||||||
|
TRACKER_VERSION: packageConfig.version,
|
||||||
|
WEBWORKER_BODY: JSON.stringify(webworkerContent),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
input: 'src/main/index.ts',
|
||||||
|
output: {
|
||||||
|
dir: 'dist/lib',
|
||||||
|
format: 'es',
|
||||||
|
sourcemap: true,
|
||||||
|
entryFileNames: '[name].js',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
...commonPlugins,
|
||||||
|
typescript({
|
||||||
|
tsconfig: 'src/main/tsconfig.json',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/main/index.ts',
|
||||||
|
output: {
|
||||||
|
dir: 'dist/cjs',
|
||||||
|
format: 'cjs',
|
||||||
|
sourcemap: true,
|
||||||
|
entryFileNames: '[name].js',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
...commonPlugins,
|
||||||
|
typescript({
|
||||||
|
tsconfig: 'src/main/tsconfig-cjs.json',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildWebWorker() {
|
||||||
|
console.log('building wworker')
|
||||||
|
const bundle = await rollup({
|
||||||
|
input: 'src/webworker/index.ts',
|
||||||
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
typescript({
|
||||||
|
tsconfig: 'src/webworker/tsconfig.json',
|
||||||
|
}),
|
||||||
|
terser(),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const { output } = await bundle.generate({
|
||||||
|
format: 'iife',
|
||||||
|
name: 'WebWorker',
|
||||||
|
inlineDynamicImports: true,
|
||||||
|
})
|
||||||
|
const webWorkerCode = output[0].code
|
||||||
|
console.log('webworker done!')
|
||||||
|
return webWorkerCode.replace(/"/g, '\\"').replace(/\n/g, '')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
tracker/tracker/src/common/interaction.d.ts
vendored
Normal file
37
tracker/tracker/src/common/interaction.d.ts
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import Message from './messages.gen.js';
|
||||||
|
export interface Options {
|
||||||
|
connAttemptCount?: number;
|
||||||
|
connAttemptGap?: number;
|
||||||
|
}
|
||||||
|
type Start = {
|
||||||
|
type: 'start';
|
||||||
|
ingestPoint: string;
|
||||||
|
pageNo: number;
|
||||||
|
timestamp: number;
|
||||||
|
url: string;
|
||||||
|
tabId: string;
|
||||||
|
} & Options;
|
||||||
|
type Auth = {
|
||||||
|
type: 'auth';
|
||||||
|
token: string;
|
||||||
|
beaconSizeLimit?: number;
|
||||||
|
};
|
||||||
|
export type ToWorkerData = null | 'stop' | Start | Auth | Array<Message> | {
|
||||||
|
type: 'compressed';
|
||||||
|
batch: Uint8Array;
|
||||||
|
} | {
|
||||||
|
type: 'uncompressed';
|
||||||
|
batch: Uint8Array;
|
||||||
|
} | 'forceFlushBatch' | 'check_queue';
|
||||||
|
type Failure = {
|
||||||
|
type: 'failure';
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
type QEmpty = {
|
||||||
|
type: 'queue_empty';
|
||||||
|
};
|
||||||
|
export type FromWorkerData = 'a_stop' | 'a_start' | Failure | 'not_init' | {
|
||||||
|
type: 'compress';
|
||||||
|
batch: Uint8Array;
|
||||||
|
} | QEmpty;
|
||||||
|
export {};
|
||||||
1
tracker/tracker/src/common/interaction.js
Normal file
1
tracker/tracker/src/common/interaction.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
||||||
1
tracker/tracker/src/common/interaction.js.map
Normal file
1
tracker/tracker/src/common/interaction.js.map
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"interaction.js","sourceRoot":"","sources":["interaction.ts"],"names":[],"mappings":""}
|
||||||
548
tracker/tracker/src/common/messages.gen.d.ts
vendored
Normal file
548
tracker/tracker/src/common/messages.gen.d.ts
vendored
Normal file
|
|
@ -0,0 +1,548 @@
|
||||||
|
export declare const enum Type {
|
||||||
|
Timestamp = 0,
|
||||||
|
SetPageLocationDeprecated = 4,
|
||||||
|
SetViewportSize = 5,
|
||||||
|
SetViewportScroll = 6,
|
||||||
|
CreateDocument = 7,
|
||||||
|
CreateElementNode = 8,
|
||||||
|
CreateTextNode = 9,
|
||||||
|
MoveNode = 10,
|
||||||
|
RemoveNode = 11,
|
||||||
|
SetNodeAttribute = 12,
|
||||||
|
RemoveNodeAttribute = 13,
|
||||||
|
SetNodeData = 14,
|
||||||
|
SetNodeScroll = 16,
|
||||||
|
SetInputTarget = 17,
|
||||||
|
SetInputValue = 18,
|
||||||
|
SetInputChecked = 19,
|
||||||
|
MouseMove = 20,
|
||||||
|
NetworkRequestDeprecated = 21,
|
||||||
|
ConsoleLog = 22,
|
||||||
|
PageLoadTiming = 23,
|
||||||
|
PageRenderTiming = 24,
|
||||||
|
CustomEvent = 27,
|
||||||
|
UserID = 28,
|
||||||
|
UserAnonymousID = 29,
|
||||||
|
Metadata = 30,
|
||||||
|
CSSInsertRule = 37,
|
||||||
|
CSSDeleteRule = 38,
|
||||||
|
Fetch = 39,
|
||||||
|
Profiler = 40,
|
||||||
|
OTable = 41,
|
||||||
|
StateAction = 42,
|
||||||
|
ReduxDeprecated = 44,
|
||||||
|
Vuex = 45,
|
||||||
|
MobX = 46,
|
||||||
|
NgRx = 47,
|
||||||
|
GraphQLDeprecated = 48,
|
||||||
|
PerformanceTrack = 49,
|
||||||
|
StringDict = 50,
|
||||||
|
SetNodeAttributeDict = 51,
|
||||||
|
ResourceTimingDeprecated = 53,
|
||||||
|
ConnectionInformation = 54,
|
||||||
|
SetPageVisibility = 55,
|
||||||
|
LoadFontFace = 57,
|
||||||
|
SetNodeFocus = 58,
|
||||||
|
LongTask = 59,
|
||||||
|
SetNodeAttributeURLBased = 60,
|
||||||
|
SetCSSDataURLBased = 61,
|
||||||
|
TechnicalInfo = 63,
|
||||||
|
CustomIssue = 64,
|
||||||
|
CSSInsertRuleURLBased = 67,
|
||||||
|
MouseClick = 68,
|
||||||
|
MouseClickDeprecated = 69,
|
||||||
|
CreateIFrameDocument = 70,
|
||||||
|
AdoptedSSReplaceURLBased = 71,
|
||||||
|
AdoptedSSInsertRuleURLBased = 73,
|
||||||
|
AdoptedSSDeleteRule = 75,
|
||||||
|
AdoptedSSAddOwner = 76,
|
||||||
|
AdoptedSSRemoveOwner = 77,
|
||||||
|
JSException = 78,
|
||||||
|
Zustand = 79,
|
||||||
|
BatchMetadata = 81,
|
||||||
|
PartitionedMessage = 82,
|
||||||
|
NetworkRequest = 83,
|
||||||
|
WSChannel = 84,
|
||||||
|
InputChange = 112,
|
||||||
|
SelectionChange = 113,
|
||||||
|
MouseThrashing = 114,
|
||||||
|
UnbindNodes = 115,
|
||||||
|
ResourceTiming = 116,
|
||||||
|
TabChange = 117,
|
||||||
|
TabData = 118,
|
||||||
|
CanvasNode = 119,
|
||||||
|
TagTrigger = 120,
|
||||||
|
Redux = 121,
|
||||||
|
SetPageLocation = 122,
|
||||||
|
GraphQL = 123
|
||||||
|
}
|
||||||
|
export type Timestamp = [
|
||||||
|
Type.Timestamp,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type SetPageLocationDeprecated = [
|
||||||
|
Type.SetPageLocationDeprecated,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type SetViewportSize = [
|
||||||
|
Type.SetViewportSize,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type SetViewportScroll = [
|
||||||
|
Type.SetViewportScroll,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type CreateDocument = [
|
||||||
|
Type.CreateDocument
|
||||||
|
];
|
||||||
|
export type CreateElementNode = [
|
||||||
|
Type.CreateElementNode,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
boolean
|
||||||
|
];
|
||||||
|
export type CreateTextNode = [
|
||||||
|
Type.CreateTextNode,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type MoveNode = [
|
||||||
|
Type.MoveNode,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type RemoveNode = [
|
||||||
|
Type.RemoveNode,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type SetNodeAttribute = [
|
||||||
|
Type.SetNodeAttribute,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type RemoveNodeAttribute = [
|
||||||
|
Type.RemoveNodeAttribute,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type SetNodeData = [
|
||||||
|
Type.SetNodeData,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type SetNodeScroll = [
|
||||||
|
Type.SetNodeScroll,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type SetInputTarget = [
|
||||||
|
Type.SetInputTarget,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type SetInputValue = [
|
||||||
|
Type.SetInputValue,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type SetInputChecked = [
|
||||||
|
Type.SetInputChecked,
|
||||||
|
number,
|
||||||
|
boolean
|
||||||
|
];
|
||||||
|
export type MouseMove = [
|
||||||
|
Type.MouseMove,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type NetworkRequestDeprecated = [
|
||||||
|
Type.NetworkRequestDeprecated,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type ConsoleLog = [
|
||||||
|
Type.ConsoleLog,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type PageLoadTiming = [
|
||||||
|
Type.PageLoadTiming,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type PageRenderTiming = [
|
||||||
|
Type.PageRenderTiming,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type CustomEvent = [
|
||||||
|
Type.CustomEvent,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type UserID = [
|
||||||
|
Type.UserID,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type UserAnonymousID = [
|
||||||
|
Type.UserAnonymousID,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type Metadata = [
|
||||||
|
Type.Metadata,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type CSSInsertRule = [
|
||||||
|
Type.CSSInsertRule,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type CSSDeleteRule = [
|
||||||
|
Type.CSSDeleteRule,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type Fetch = [
|
||||||
|
Type.Fetch,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type Profiler = [
|
||||||
|
Type.Profiler,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type OTable = [
|
||||||
|
Type.OTable,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type StateAction = [
|
||||||
|
Type.StateAction,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type ReduxDeprecated = [
|
||||||
|
Type.ReduxDeprecated,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type Vuex = [
|
||||||
|
Type.Vuex,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type MobX = [
|
||||||
|
Type.MobX,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type NgRx = [
|
||||||
|
Type.NgRx,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type GraphQLDeprecated = [
|
||||||
|
Type.GraphQLDeprecated,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type PerformanceTrack = [
|
||||||
|
Type.PerformanceTrack,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type StringDict = [
|
||||||
|
Type.StringDict,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type SetNodeAttributeDict = [
|
||||||
|
Type.SetNodeAttributeDict,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type ResourceTimingDeprecated = [
|
||||||
|
Type.ResourceTimingDeprecated,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type ConnectionInformation = [
|
||||||
|
Type.ConnectionInformation,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type SetPageVisibility = [
|
||||||
|
Type.SetPageVisibility,
|
||||||
|
boolean
|
||||||
|
];
|
||||||
|
export type LoadFontFace = [
|
||||||
|
Type.LoadFontFace,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type SetNodeFocus = [
|
||||||
|
Type.SetNodeFocus,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type LongTask = [
|
||||||
|
Type.LongTask,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type SetNodeAttributeURLBased = [
|
||||||
|
Type.SetNodeAttributeURLBased,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type SetCSSDataURLBased = [
|
||||||
|
Type.SetCSSDataURLBased,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type TechnicalInfo = [
|
||||||
|
Type.TechnicalInfo,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type CustomIssue = [
|
||||||
|
Type.CustomIssue,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type CSSInsertRuleURLBased = [
|
||||||
|
Type.CSSInsertRuleURLBased,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type MouseClick = [
|
||||||
|
Type.MouseClick,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type MouseClickDeprecated = [
|
||||||
|
Type.MouseClickDeprecated,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type CreateIFrameDocument = [
|
||||||
|
Type.CreateIFrameDocument,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type AdoptedSSReplaceURLBased = [
|
||||||
|
Type.AdoptedSSReplaceURLBased,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type AdoptedSSInsertRuleURLBased = [
|
||||||
|
Type.AdoptedSSInsertRuleURLBased,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type AdoptedSSDeleteRule = [
|
||||||
|
Type.AdoptedSSDeleteRule,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type AdoptedSSAddOwner = [
|
||||||
|
Type.AdoptedSSAddOwner,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type AdoptedSSRemoveOwner = [
|
||||||
|
Type.AdoptedSSRemoveOwner,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type JSException = [
|
||||||
|
Type.JSException,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type Zustand = [
|
||||||
|
Type.Zustand,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type BatchMetadata = [
|
||||||
|
Type.BatchMetadata,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type PartitionedMessage = [
|
||||||
|
Type.PartitionedMessage,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type NetworkRequest = [
|
||||||
|
Type.NetworkRequest,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type WSChannel = [
|
||||||
|
Type.WSChannel,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type InputChange = [
|
||||||
|
Type.InputChange,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
boolean,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type SelectionChange = [
|
||||||
|
Type.SelectionChange,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type MouseThrashing = [
|
||||||
|
Type.MouseThrashing,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type UnbindNodes = [
|
||||||
|
Type.UnbindNodes,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type ResourceTiming = [
|
||||||
|
Type.ResourceTiming,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
boolean
|
||||||
|
];
|
||||||
|
export type TabChange = [
|
||||||
|
Type.TabChange,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type TabData = [
|
||||||
|
Type.TabData,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type CanvasNode = [
|
||||||
|
Type.CanvasNode,
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type TagTrigger = [
|
||||||
|
Type.TagTrigger,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type Redux = [
|
||||||
|
Type.Redux,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
export type SetPageLocation = [
|
||||||
|
Type.SetPageLocation,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
string
|
||||||
|
];
|
||||||
|
export type GraphQL = [
|
||||||
|
Type.GraphQL,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
type Message = Timestamp | SetPageLocationDeprecated | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | ReduxDeprecated | Vuex | MobX | NgRx | GraphQLDeprecated | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | MouseClickDeprecated | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode | TagTrigger | Redux | SetPageLocation | GraphQL;
|
||||||
|
export default Message;
|
||||||
3
tracker/tracker/src/common/messages.gen.js
Normal file
3
tracker/tracker/src/common/messages.gen.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Auto-generated, do not edit
|
||||||
|
/* eslint-disable */
|
||||||
|
export {};
|
||||||
1
tracker/tracker/src/common/messages.gen.js.map
Normal file
1
tracker/tracker/src/common/messages.gen.js.map
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"messages.gen.js","sourceRoot":"","sources":["messages.gen.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,oBAAoB"}
|
||||||
1
tracker/tracker/src/common/tsconfig.tsbuildinfo
Normal file
1
tracker/tracker/src/common/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,7 @@ import Message, {
|
||||||
UserID,
|
UserID,
|
||||||
WSChannel,
|
WSChannel,
|
||||||
} from './messages.gen.js'
|
} from './messages.gen.js'
|
||||||
import Nodes from './nodes.js'
|
import Nodes from './nodes/index.js'
|
||||||
import type { Options as ObserverOptions } from './observer/top_observer.js'
|
import type { Options as ObserverOptions } from './observer/top_observer.js'
|
||||||
import Observer from './observer/top_observer.js'
|
import Observer from './observer/top_observer.js'
|
||||||
import type { Options as SanitizerOptions } from './sanitizer.js'
|
import type { Options as SanitizerOptions } from './sanitizer.js'
|
||||||
|
|
@ -40,6 +40,7 @@ import Sanitizer from './sanitizer.js'
|
||||||
import type { Options as SessOptions } from './session.js'
|
import type { Options as SessOptions } from './session.js'
|
||||||
import Session from './session.js'
|
import Session from './session.js'
|
||||||
import Ticker from './ticker.js'
|
import Ticker from './ticker.js'
|
||||||
|
import { MaintainerOptions } from './nodes/maintainer.js'
|
||||||
|
|
||||||
interface TypedWorker extends Omit<Worker, 'postMessage'> {
|
interface TypedWorker extends Omit<Worker, 'postMessage'> {
|
||||||
postMessage(data: ToWorkerData): void
|
postMessage(data: ToWorkerData): void
|
||||||
|
|
@ -168,11 +169,22 @@ type AppOptions = {
|
||||||
|
|
||||||
network?: NetworkOptions
|
network?: NetworkOptions
|
||||||
/**
|
/**
|
||||||
* use this flag if you're using Angular
|
* use this flag to force angular detection to be offline
|
||||||
|
*
|
||||||
* basically goes around window.Zone api changes to mutation observer
|
* basically goes around window.Zone api changes to mutation observer
|
||||||
* and event listeners
|
* and event listeners
|
||||||
* */
|
* */
|
||||||
angularMode?: boolean
|
forceNgOff?: boolean
|
||||||
|
/**
|
||||||
|
* This option is used to change how tracker handles potentially detached nodes
|
||||||
|
*
|
||||||
|
* defaults here are tested and proven to be lightweight and easy on cpu
|
||||||
|
*
|
||||||
|
* consult the docs before changing it
|
||||||
|
* */
|
||||||
|
nodes?: {
|
||||||
|
maintainer: Partial<MaintainerOptions>
|
||||||
|
}
|
||||||
} & WebworkerOptions &
|
} & WebworkerOptions &
|
||||||
SessOptions
|
SessOptions
|
||||||
|
|
||||||
|
|
@ -318,7 +330,7 @@ export default class App {
|
||||||
__save_canvas_locally: false,
|
__save_canvas_locally: false,
|
||||||
useAnimationFrame: false,
|
useAnimationFrame: false,
|
||||||
},
|
},
|
||||||
angularMode: false,
|
forceNgOff: false,
|
||||||
}
|
}
|
||||||
this.options = simpleMerge(defaultOptions, options)
|
this.options = simpleMerge(defaultOptions, options)
|
||||||
|
|
||||||
|
|
@ -338,7 +350,8 @@ export default class App {
|
||||||
this.sanitizer = new Sanitizer({ app: this, options })
|
this.sanitizer = new Sanitizer({ app: this, options })
|
||||||
this.nodes = new Nodes({
|
this.nodes = new Nodes({
|
||||||
node_id: this.options.node_id,
|
node_id: this.options.node_id,
|
||||||
angularMode: Boolean(options.angularMode),
|
forceNgOff: Boolean(options.forceNgOff),
|
||||||
|
maintainer: this.options.nodes?.maintainer,
|
||||||
})
|
})
|
||||||
this.observer = new Observer({ app: this, options })
|
this.observer = new Observer({ app: this, options })
|
||||||
this.ticker = new Ticker(this)
|
this.ticker = new Ticker(this)
|
||||||
|
|
@ -378,25 +391,25 @@ export default class App {
|
||||||
* */
|
* */
|
||||||
window.addEventListener('message', this.parentCrossDomainFrameListener)
|
window.addEventListener('message', this.parentCrossDomainFrameListener)
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
if (document.hidden) {
|
||||||
|
return
|
||||||
|
}
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
{
|
{
|
||||||
line: proto.polling,
|
line: proto.polling,
|
||||||
context: this.contextId,
|
context: this.contextId,
|
||||||
},
|
},
|
||||||
'*',
|
options.crossdomain?.parentDomain ?? '*',
|
||||||
)
|
)
|
||||||
}, 250)
|
}, 250)
|
||||||
} else {
|
} else {
|
||||||
this.initWorker()
|
this.initWorker()
|
||||||
}
|
|
||||||
if (!this.insideIframe) {
|
|
||||||
/**
|
/**
|
||||||
* if we get a signal from child iframes, we check for their node_id and send it back,
|
* if we get a signal from child iframes, we check for their node_id and send it back,
|
||||||
* so they can act as if it was just a same-domain iframe
|
* so they can act as if it was just a same-domain iframe
|
||||||
* */
|
* */
|
||||||
window.addEventListener('message', this.crossDomainIframeListener)
|
window.addEventListener('message', this.crossDomainIframeListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.bc !== null) {
|
if (this.bc !== null) {
|
||||||
this.bc.postMessage({
|
this.bc.postMessage({
|
||||||
line: proto.ask,
|
line: proto.ask,
|
||||||
|
|
@ -436,7 +449,6 @@ export default class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** used by child iframes for crossdomain only */
|
|
||||||
/** used by child iframes for crossdomain only */
|
/** used by child iframes for crossdomain only */
|
||||||
parentActive = false
|
parentActive = false
|
||||||
checkStatus = () => {
|
checkStatus = () => {
|
||||||
|
|
@ -484,13 +496,11 @@ export default class App {
|
||||||
if (data.line === proto.iframeSignal) {
|
if (data.line === proto.iframeSignal) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*')
|
event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*')
|
||||||
const pageIframes = Array.from(document.querySelectorAll('iframe'))
|
|
||||||
this.pageFrames = pageIframes
|
|
||||||
const signalId = async () => {
|
const signalId = async () => {
|
||||||
if (event.source === null) {
|
if (event.source === null) {
|
||||||
return console.error('Couldnt connect to event.source for child iframe tracking')
|
return console.error('Couldnt connect to event.source for child iframe tracking')
|
||||||
}
|
}
|
||||||
const id = await this.checkNodeId(pageIframes, event.source)
|
const id = await this.checkNodeId(event.source)
|
||||||
if (id && !this.trackedFrames.includes(data.context)) {
|
if (id && !this.trackedFrames.includes(data.context)) {
|
||||||
try {
|
try {
|
||||||
this.trackedFrames.push(data.context)
|
this.trackedFrames.push(data.context)
|
||||||
|
|
@ -518,7 +528,7 @@ export default class App {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.debug.log('Couldnt get node id for iframe', event.source, pageIframes)
|
this.debug.log('Couldnt get node id for iframe', event.source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void signalId()
|
void signalId()
|
||||||
|
|
@ -614,7 +624,31 @@ export default class App {
|
||||||
|
|
||||||
signalIframeTracker = () => {
|
signalIframeTracker = () => {
|
||||||
const thisTab = this.session.getTabId()
|
const thisTab = this.session.getTabId()
|
||||||
const signalToParent = (n: number) => {
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
line: proto.iframeSignal,
|
||||||
|
source: thisTab,
|
||||||
|
context: this.contextId,
|
||||||
|
},
|
||||||
|
this.options.crossdomain?.parentDomain ?? '*',
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* since we need to wait uncertain amount of time
|
||||||
|
* and I don't want to have recursion going on,
|
||||||
|
* we'll just use a timeout loop with backoff
|
||||||
|
* */
|
||||||
|
const maxRetries = 10
|
||||||
|
let retries = 0
|
||||||
|
let delay = 250
|
||||||
|
let cumulativeDelay = 0
|
||||||
|
let stopAttempts = false
|
||||||
|
|
||||||
|
const checkAndSendMessage = () => {
|
||||||
|
if (stopAttempts || this.checkStatus()) {
|
||||||
|
stopAttempts = true
|
||||||
|
return
|
||||||
|
}
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
{
|
{
|
||||||
line: proto.iframeSignal,
|
line: proto.iframeSignal,
|
||||||
|
|
@ -623,13 +657,21 @@ export default class App {
|
||||||
},
|
},
|
||||||
this.options.crossdomain?.parentDomain ?? '*',
|
this.options.crossdomain?.parentDomain ?? '*',
|
||||||
)
|
)
|
||||||
setTimeout(() => {
|
console.log('Trying to signal to parent, attempt:', retries + 1)
|
||||||
if (!this.checkStatus() && n < 100) {
|
retries++
|
||||||
void signalToParent(n + 1)
|
}
|
||||||
}
|
|
||||||
}, 250)
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
|
if (this.checkStatus()) {
|
||||||
|
stopAttempts = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cumulativeDelay += delay
|
||||||
|
setTimeout(() => {
|
||||||
|
checkAndSendMessage()
|
||||||
|
}, cumulativeDelay)
|
||||||
|
delay *= 1.5
|
||||||
}
|
}
|
||||||
void signalToParent(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startTimeout: ReturnType<typeof setTimeout> | null = null
|
startTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
@ -641,32 +683,35 @@ export default class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkNodeId(
|
private async checkNodeId(source: MessageEventSource): Promise<number | null> {
|
||||||
iframes: HTMLIFrameElement[],
|
let targetFrame
|
||||||
source: MessageEventSource,
|
if (this.pageFrames.length > 0) {
|
||||||
): Promise<number | null> {
|
targetFrame = this.pageFrames.find((frame) => frame.contentWindow === source)
|
||||||
for (const iframe of iframes) {
|
}
|
||||||
if (iframe.contentWindow && iframe.contentWindow === source) {
|
if (!targetFrame || !this.pageFrames.length) {
|
||||||
/**
|
const pageIframes = Array.from(document.querySelectorAll('iframe'))
|
||||||
* Here we're trying to get node id from the iframe (which is kept in observer)
|
this.pageFrames = pageIframes
|
||||||
* because of async nature of dom initialization, we give 100 retries with 100ms delay each
|
targetFrame = pageIframes.find((frame) => frame.contentWindow === source)
|
||||||
* which equals to 10 seconds. This way we have a period where we give app some time to load
|
}
|
||||||
* and tracker some time to parse the initial DOM tree even on slower devices
|
if (!targetFrame) {
|
||||||
* */
|
return null
|
||||||
let tries = 0
|
}
|
||||||
while (tries < 100) {
|
/**
|
||||||
// @ts-ignore
|
* Here we're trying to get node id from the iframe (which is kept in observer)
|
||||||
const potentialId = iframe[this.options.node_id]
|
* because of async nature of dom initialization, we give 100 retries with 100ms delay each
|
||||||
if (potentialId !== undefined) {
|
* which equals to 10 seconds. This way we have a period where we give app some time to load
|
||||||
tries = 100
|
* and tracker some time to parse the initial DOM tree even on slower devices
|
||||||
return potentialId
|
* */
|
||||||
} else {
|
let tries = 0
|
||||||
tries++
|
while (tries < 100) {
|
||||||
await delay(100)
|
// @ts-ignore
|
||||||
}
|
const potentialId = targetFrame[this.options.node_id]
|
||||||
}
|
if (potentialId !== undefined) {
|
||||||
|
tries = 100
|
||||||
return null
|
return potentialId
|
||||||
|
} else {
|
||||||
|
tries++
|
||||||
|
await delay(100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -903,11 +948,11 @@ export default class App {
|
||||||
|
|
||||||
const createListener = () =>
|
const createListener = () =>
|
||||||
target
|
target
|
||||||
? createEventListener(target, type, listener, useCapture, this.options.angularMode)
|
? createEventListener(target, type, listener, useCapture, this.options.forceNgOff)
|
||||||
: null
|
: null
|
||||||
const deleteListener = () =>
|
const deleteListener = () =>
|
||||||
target
|
target
|
||||||
? deleteEventListener(target, type, listener, useCapture, this.options.angularMode)
|
? deleteEventListener(target, type, listener, useCapture, this.options.forceNgOff)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
this.attachStartCallback(createListener, useSafe)
|
this.attachStartCallback(createListener, useSafe)
|
||||||
|
|
@ -1505,7 +1550,7 @@ export default class App {
|
||||||
}
|
}
|
||||||
this.canvasRecorder?.startTracking()
|
this.canvasRecorder?.startTracking()
|
||||||
|
|
||||||
if (this.features['usability-test']) {
|
if (this.features['usability-test'] && !this.insideIframe) {
|
||||||
this.uxtManager = this.uxtManager
|
this.uxtManager = this.uxtManager
|
||||||
? this.uxtManager
|
? this.uxtManager
|
||||||
: new UserTestManager(this, uxtStorageKey)
|
: new UserTestManager(this, uxtStorageKey)
|
||||||
|
|
@ -1592,14 +1637,12 @@ export default class App {
|
||||||
|
|
||||||
async waitStart() {
|
async waitStart() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const check = () => {
|
const int = setInterval(() => {
|
||||||
if (this.canStart) {
|
if (this.canStart) {
|
||||||
|
clearInterval(int)
|
||||||
resolve(true)
|
resolve(true)
|
||||||
} else {
|
|
||||||
setTimeout(check, 25)
|
|
||||||
}
|
}
|
||||||
}
|
}, 100)
|
||||||
check()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1692,6 +1735,7 @@ export default class App {
|
||||||
|
|
||||||
stop(stopWorker = true): void {
|
stop(stopWorker = true): void {
|
||||||
if (this.activityState !== ActivityState.NotActive) {
|
if (this.activityState !== ActivityState.NotActive) {
|
||||||
|
console.trace('stopped')
|
||||||
try {
|
try {
|
||||||
if (!this.insideIframe && this.options.crossdomain?.enabled) {
|
if (!this.insideIframe && this.options.crossdomain?.enabled) {
|
||||||
this.killChildrenFrames()
|
this.killChildrenFrames()
|
||||||
|
|
@ -1711,7 +1755,6 @@ export default class App {
|
||||||
this.trackedFrames = []
|
this.trackedFrames = []
|
||||||
this.parentActive = false
|
this.parentActive = false
|
||||||
this.canStart = false
|
this.canStart = false
|
||||||
this.pollingQueue = { order: [] }
|
|
||||||
} finally {
|
} finally {
|
||||||
this.activityState = ActivityState.NotActive
|
this.activityState = ActivityState.NotActive
|
||||||
this.debug.log('OpenReplay tracking stopped.')
|
this.debug.log('OpenReplay tracking stopped.')
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,30 @@
|
||||||
import { createEventListener, deleteEventListener } from '../utils.js'
|
import { createEventListener, deleteEventListener } from '../../utils.js'
|
||||||
|
import Maintainer, { MaintainerOptions } from './maintainer.js'
|
||||||
|
|
||||||
type NodeCallback = (node: Node, isStart: boolean) => void
|
type NodeCallback = (node: Node, isStart: boolean) => void
|
||||||
type ElementListener = [string, EventListener, boolean]
|
type ElementListener = [string, EventListener, boolean]
|
||||||
|
|
||||||
|
export interface NodesOptions {
|
||||||
|
node_id: string
|
||||||
|
forceNgOff: boolean
|
||||||
|
maintainer?: Partial<MaintainerOptions>
|
||||||
|
}
|
||||||
|
|
||||||
export default class Nodes {
|
export default class Nodes {
|
||||||
private nodes: Array<Node | void> = []
|
private readonly nodes: Map<number, Node | void> = new Map()
|
||||||
private totalNodeAmount = 0
|
private totalNodeAmount = 0
|
||||||
private readonly nodeCallbacks: Array<NodeCallback> = []
|
private readonly nodeCallbacks: Array<NodeCallback> = []
|
||||||
private readonly elementListeners: Map<number, Array<ElementListener>> = new Map()
|
private readonly elementListeners: Map<number, Array<ElementListener>> = new Map()
|
||||||
private nextNodeId = 0
|
private nextNodeId = 0
|
||||||
private readonly node_id: string
|
private readonly node_id: string
|
||||||
private readonly angularMode: boolean
|
private readonly forceNgOff: boolean
|
||||||
|
private readonly maintainer: Maintainer
|
||||||
|
|
||||||
constructor(params: { node_id: string; angularMode: boolean }) {
|
constructor(params: NodesOptions) {
|
||||||
this.node_id = params.node_id
|
this.node_id = params.node_id
|
||||||
this.angularMode = params.angularMode
|
this.forceNgOff = params.forceNgOff
|
||||||
|
this.maintainer = new Maintainer(this.nodes, this.unregisterNode, params.maintainer)
|
||||||
|
this.maintainer.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
syntheticMode(frameOrder: number) {
|
syntheticMode(frameOrder: number) {
|
||||||
|
|
@ -30,12 +40,12 @@ export default class Nodes {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attached once per Tracker instance
|
// Attached once per Tracker instance
|
||||||
attachNodeCallback = (nodeCallback: NodeCallback): void => {
|
attachNodeCallback = (nodeCallback: NodeCallback): number => {
|
||||||
this.nodeCallbacks.push(nodeCallback)
|
return this.nodeCallbacks.push(nodeCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
scanTree = (cb: (node: Node | void) => void) => {
|
scanTree = (cb: (node: Node | void) => void) => {
|
||||||
this.nodes.forEach((node) => cb(node))
|
this.nodes.forEach((node) => (node ? cb(node) : undefined))
|
||||||
}
|
}
|
||||||
|
|
||||||
attachNodeListener = (
|
attachNodeListener = (
|
||||||
|
|
@ -48,7 +58,7 @@ export default class Nodes {
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
createEventListener(node, type, listener, useCapture, this.angularMode)
|
createEventListener(node, type, listener, useCapture, this.forceNgOff)
|
||||||
let listeners = this.elementListeners.get(id)
|
let listeners = this.elementListeners.get(id)
|
||||||
if (listeners === undefined) {
|
if (listeners === undefined) {
|
||||||
listeners = []
|
listeners = []
|
||||||
|
|
@ -64,23 +74,23 @@ export default class Nodes {
|
||||||
id = this.nextNodeId
|
id = this.nextNodeId
|
||||||
this.totalNodeAmount++
|
this.totalNodeAmount++
|
||||||
this.nextNodeId++
|
this.nextNodeId++
|
||||||
this.nodes[id] = node
|
this.nodes.set(id, node)
|
||||||
;(node as any)[this.node_id] = id
|
;(node as any)[this.node_id] = id
|
||||||
}
|
}
|
||||||
return [id, isNew]
|
return [id, isNew]
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterNode(node: Node): number | undefined {
|
unregisterNode = (node: Node): number | undefined => {
|
||||||
const id = (node as any)[this.node_id]
|
const id = (node as any)[this.node_id]
|
||||||
if (id !== undefined) {
|
if (id !== undefined) {
|
||||||
;(node as any)[this.node_id] = undefined
|
;(node as any)[this.node_id] = undefined
|
||||||
delete (node as any)[this.node_id]
|
delete (node as any)[this.node_id]
|
||||||
delete this.nodes[id]
|
this.nodes.delete(id)
|
||||||
const listeners = this.elementListeners.get(id)
|
const listeners = this.elementListeners.get(id)
|
||||||
if (listeners !== undefined) {
|
if (listeners !== undefined) {
|
||||||
this.elementListeners.delete(id)
|
this.elementListeners.delete(id)
|
||||||
listeners.forEach((listener) =>
|
listeners.forEach((listener) =>
|
||||||
deleteEventListener(node, listener[0], listener[1], listener[2], this.angularMode),
|
deleteEventListener(node, listener[0], listener[1], listener[2], this.forceNgOff),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
this.totalNodeAmount--
|
this.totalNodeAmount--
|
||||||
|
|
@ -93,8 +103,7 @@ export default class Nodes {
|
||||||
// but its still better than keeping dead nodes or undef elements
|
// but its still better than keeping dead nodes or undef elements
|
||||||
// plus we keep our index positions for new/alive nodes
|
// plus we keep our index positions for new/alive nodes
|
||||||
// performance test: 3ms for 30k nodes with 17k dead ones
|
// performance test: 3ms for 30k nodes with 17k dead ones
|
||||||
for (let i = 0; i < this.nodes.length; i++) {
|
for (const [_, node] of this.nodes) {
|
||||||
const node = this.nodes[i]
|
|
||||||
if (node && !document.contains(node)) {
|
if (node && !document.contains(node)) {
|
||||||
this.unregisterNode(node)
|
this.unregisterNode(node)
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +120,7 @@ export default class Nodes {
|
||||||
}
|
}
|
||||||
|
|
||||||
getNode(id: number) {
|
getNode(id: number) {
|
||||||
return this.nodes[id]
|
return this.nodes.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeCount() {
|
getNodeCount() {
|
||||||
|
|
@ -119,14 +128,13 @@ export default class Nodes {
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
for (let id = 0; id < this.nodes.length; id++) {
|
for (const [_, node] of this.nodes) {
|
||||||
const node = this.nodes[id]
|
if (node) {
|
||||||
if (!node) {
|
this.unregisterNode(node)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
this.unregisterNode(node)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nextNodeId = 0
|
this.nextNodeId = 0
|
||||||
this.nodes.length = 0
|
this.nodes.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
119
tracker/tracker/src/main/app/nodes/maintainer.ts
Normal file
119
tracker/tracker/src/main/app/nodes/maintainer.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
const SECOND = 1000
|
||||||
|
|
||||||
|
function processMapInBatches(
|
||||||
|
map: Map<number, Node | void>,
|
||||||
|
batchSize: number,
|
||||||
|
processBatchCallback: (node: Node) => void,
|
||||||
|
) {
|
||||||
|
const iterator = map.entries()
|
||||||
|
|
||||||
|
function processNextBatch() {
|
||||||
|
const batch = []
|
||||||
|
let result = iterator.next()
|
||||||
|
|
||||||
|
while (!result.done && batch.length < batchSize) {
|
||||||
|
batch.push(result.value)
|
||||||
|
result = iterator.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.length > 0) {
|
||||||
|
batch.forEach(([_, node]) => {
|
||||||
|
if (node) {
|
||||||
|
processBatchCallback(node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(processNextBatch, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processNextBatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNodeStillActive(node: Node): boolean {
|
||||||
|
try {
|
||||||
|
if (!node.isConnected) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeWindow = node.ownerDocument?.defaultView
|
||||||
|
|
||||||
|
if (!nodeWindow) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeWindow.closed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.ownerDocument.documentElement.isConnected) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error checking node activity:', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaintainerOptions {
|
||||||
|
/**
|
||||||
|
* Run cleanup each X ms
|
||||||
|
*
|
||||||
|
* @default 30 * 1000
|
||||||
|
* */
|
||||||
|
interval: number
|
||||||
|
/**
|
||||||
|
* Maintainer checks nodes in small batches over 50ms timeouts
|
||||||
|
*
|
||||||
|
* @default 2500
|
||||||
|
* */
|
||||||
|
batchSize: number
|
||||||
|
/**
|
||||||
|
* @default true
|
||||||
|
* */
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
interval: SECOND * 30,
|
||||||
|
batchSize: 2500,
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Maintainer {
|
||||||
|
private interval: ReturnType<typeof setInterval>
|
||||||
|
private readonly options: MaintainerOptions
|
||||||
|
constructor(
|
||||||
|
private readonly nodes: Map<number, Node | void>,
|
||||||
|
private readonly unregisterNode: (node: Node) => void,
|
||||||
|
options?: Partial<MaintainerOptions>,
|
||||||
|
) {
|
||||||
|
this.options = { ...defaults, ...options }
|
||||||
|
}
|
||||||
|
|
||||||
|
public start = () => {
|
||||||
|
if (!this.options.enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stop()
|
||||||
|
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
processMapInBatches(this.nodes, this.options.batchSize, (node) => {
|
||||||
|
if (!isNodeStillActive(node)) {
|
||||||
|
this.unregisterNode(node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, this.options.interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop = () => {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Maintainer
|
||||||
|
|
@ -8,7 +8,7 @@ type OffsetState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class IFrameOffsets {
|
export default class IFrameOffsets {
|
||||||
private readonly states: Map<Document, OffsetState> = new Map()
|
private states: WeakMap<Document, OffsetState> = new WeakMap()
|
||||||
|
|
||||||
private calcOffset(state: OffsetState): Offset {
|
private calcOffset(state: OffsetState): Offset {
|
||||||
let parLeft = 0,
|
let parLeft = 0,
|
||||||
|
|
@ -55,12 +55,10 @@ export default class IFrameOffsets {
|
||||||
// anything more reliable? This does not cover all cases (layout changes are ignored, for ex.)
|
// anything more reliable? This does not cover all cases (layout changes are ignored, for ex.)
|
||||||
parentDoc.addEventListener('scroll', invalidateOffset)
|
parentDoc.addEventListener('scroll', invalidateOffset)
|
||||||
parentDoc.defaultView?.addEventListener('resize', invalidateOffset)
|
parentDoc.defaultView?.addEventListener('resize', invalidateOffset)
|
||||||
|
|
||||||
this.states.set(doc, state)
|
this.states.set(doc, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.states.forEach((s) => s.clear())
|
this.states = new WeakMap()
|
||||||
this.states.clear()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,6 @@ export default abstract class Observer {
|
||||||
if (name === null) {
|
if (name === null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (target instanceof HTMLIFrameElement && name === 'src') {
|
|
||||||
this.handleIframeSrcChange(target)
|
|
||||||
}
|
|
||||||
let attr = this.attributesMap.get(id)
|
let attr = this.attributesMap.get(id)
|
||||||
if (attr === undefined) {
|
if (attr === undefined) {
|
||||||
this.attributesMap.set(id, (attr = new Set()))
|
this.attributesMap.set(id, (attr = new Set()))
|
||||||
|
|
@ -122,7 +119,7 @@ export default abstract class Observer {
|
||||||
}
|
}
|
||||||
this.commitNodes()
|
this.commitNodes()
|
||||||
}) as MutationCallback,
|
}) as MutationCallback,
|
||||||
this.app.options.angularMode,
|
this.app.options.forceNgOff,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
private clear(): void {
|
private clear(): void {
|
||||||
|
|
@ -134,7 +131,9 @@ export default abstract class Observer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unbinds the removed nodes in case of iframe src change.
|
* EXPERIMENTAL: Unbinds the removed nodes in case of iframe src change.
|
||||||
|
*
|
||||||
|
* right now, we're relying on nodes.maintainer
|
||||||
*/
|
*/
|
||||||
private handleIframeSrcChange(iframe: HTMLIFrameElement): void {
|
private handleIframeSrcChange(iframe: HTMLIFrameElement): void {
|
||||||
const oldContentDocument = iframe.contentDocument
|
const oldContentDocument = iframe.contentDocument
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import Observer from './observer.js'
|
import Observer from './observer.js'
|
||||||
import { isElementNode, hasTag } from '../guards.js'
|
import { isElementNode, hasTag } from '../guards.js'
|
||||||
import Network from '../../modules/network.js'
|
|
||||||
|
|
||||||
import IFrameObserver from './iframe_observer.js'
|
import IFrameObserver from './iframe_observer.js'
|
||||||
import ShadowRootObserver from './shadow_root_observer.js'
|
import ShadowRootObserver from './shadow_root_observer.js'
|
||||||
|
|
@ -56,7 +55,7 @@ export default class TopObserver extends Observer {
|
||||||
private readonly contextCallbacks: Array<ContextCallback> = []
|
private readonly contextCallbacks: Array<ContextCallback> = []
|
||||||
|
|
||||||
// Attached once per Tracker instance
|
// Attached once per Tracker instance
|
||||||
private readonly contextsSet: Set<Window> = new Set()
|
private readonly contextsSet: WeakSet<Window> = new WeakSet()
|
||||||
attachContextCallback(cb: ContextCallback) {
|
attachContextCallback(cb: ContextCallback) {
|
||||||
this.contextCallbacks.push(cb)
|
this.contextCallbacks.push(cb)
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +64,7 @@ export default class TopObserver extends Observer {
|
||||||
return this.iframeOffsets.getDocumentOffset(doc)
|
return this.iframeOffsets.getDocumentOffset(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
private iframeObservers: IFrameObserver[] = []
|
private iframeObservers: WeakMap<HTMLIFrameElement | Document, IFrameObserver> = new WeakMap()
|
||||||
private handleIframe(iframe: HTMLIFrameElement): void {
|
private handleIframe(iframe: HTMLIFrameElement): void {
|
||||||
let doc: Document | null = null
|
let doc: Document | null = null
|
||||||
// setTimeout is required. Otherwise some event listeners (scroll, mousemove) applied in modules
|
// setTimeout is required. Otherwise some event listeners (scroll, mousemove) applied in modules
|
||||||
|
|
@ -73,16 +72,12 @@ export default class TopObserver extends Observer {
|
||||||
const handle = this.app.safe(() =>
|
const handle = this.app.safe(() =>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const id = this.app.nodes.getID(iframe)
|
const id = this.app.nodes.getID(iframe)
|
||||||
if (id === undefined) {
|
if (id === undefined || !canAccessIframe(iframe)) return
|
||||||
//log
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!canAccessIframe(iframe)) return
|
|
||||||
const currentWin = iframe.contentWindow
|
const currentWin = iframe.contentWindow
|
||||||
const currentDoc = iframe.contentDocument
|
const currentDoc = iframe.contentDocument
|
||||||
if (currentDoc && currentDoc !== doc) {
|
if (currentDoc && currentDoc !== doc) {
|
||||||
const observer = new IFrameObserver(this.app)
|
const observer = new IFrameObserver(this.app)
|
||||||
this.iframeObservers.push(observer)
|
this.iframeObservers.set(iframe, observer)
|
||||||
observer.observe(iframe) // TODO: call unregisterNode for the previous doc if present (incapsulate: one iframe - one observer)
|
observer.observe(iframe) // TODO: call unregisterNode for the previous doc if present (incapsulate: one iframe - one observer)
|
||||||
doc = currentDoc
|
doc = currentDoc
|
||||||
|
|
||||||
|
|
@ -106,10 +101,10 @@ export default class TopObserver extends Observer {
|
||||||
handle()
|
handle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private shadowRootObservers: ShadowRootObserver[] = []
|
private shadowRootObservers: WeakMap<ShadowRoot, ShadowRootObserver> = new WeakMap()
|
||||||
private handleShadowRoot(shRoot: ShadowRoot) {
|
private handleShadowRoot(shRoot: ShadowRoot) {
|
||||||
const observer = new ShadowRootObserver(this.app)
|
const observer = new ShadowRootObserver(this.app)
|
||||||
this.shadowRootObservers.push(observer)
|
this.shadowRootObservers.set(shRoot, observer)
|
||||||
observer.observe(shRoot.host)
|
observer.observe(shRoot.host)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,17 +148,15 @@ export default class TopObserver extends Observer {
|
||||||
this.app.nodes.clear()
|
this.app.nodes.clear()
|
||||||
this.app.nodes.syntheticMode(frameOder)
|
this.app.nodes.syntheticMode(frameOder)
|
||||||
const iframeObserver = new IFrameObserver(this.app)
|
const iframeObserver = new IFrameObserver(this.app)
|
||||||
this.iframeObservers.push(iframeObserver)
|
this.iframeObservers.set(window.document, iframeObserver)
|
||||||
iframeObserver.syntheticObserve(rootNodeId, window.document)
|
iframeObserver.syntheticObserve(rootNodeId, window.document)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this.iframeOffsets.clear()
|
this.iframeOffsets.clear()
|
||||||
Element.prototype.attachShadow = attachShadowNativeFn
|
Element.prototype.attachShadow = attachShadowNativeFn
|
||||||
this.iframeObservers.forEach((o) => o.disconnect())
|
this.iframeObservers = new WeakMap()
|
||||||
this.iframeObservers = []
|
this.shadowRootObservers = new WeakMap()
|
||||||
this.shadowRootObservers.forEach((o) => o.disconnect())
|
|
||||||
this.shadowRootObservers = []
|
|
||||||
super.disconnect()
|
super.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,10 @@ export default class API {
|
||||||
if (!IN_BROWSER || !processOptions(options)) {
|
if (!IN_BROWSER || !processOptions(options)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ((window as any).__OPENREPLAY__) {
|
if (
|
||||||
|
(window as any).__OPENREPLAY__ ||
|
||||||
|
(!this.crossdomainMode && inIframe() && (window.top as any)?.__OPENREPLAY__)
|
||||||
|
) {
|
||||||
console.error('OpenReplay: one tracker instance has been initialised already')
|
console.error('OpenReplay: one tracker instance has been initialised already')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type App from '../app/index.js'
|
import type App from '../app/index.js'
|
||||||
import type Message from '../app/messages.gen.js'
|
import type Message from '../app/messages.gen.js'
|
||||||
import { JSException } from '../app/messages.gen.js'
|
import { JSException } from '../app/messages.gen.js'
|
||||||
import ErrorStackParser from 'error-stack-parser'
|
import { parse } from 'error-stack-parser-es'
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
captureExceptions: boolean
|
captureExceptions: boolean
|
||||||
|
|
@ -34,7 +34,7 @@ export function getExceptionMessage(
|
||||||
): Message {
|
): Message {
|
||||||
let stack = fallbackStack
|
let stack = fallbackStack
|
||||||
try {
|
try {
|
||||||
stack = ErrorStackParser.parse(error)
|
stack = parse(error)
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return JSException(error.name, error.message, JSON.stringify(stack), JSON.stringify(metadata))
|
return JSException(error.name, error.message, JSON.stringify(stack), JSON.stringify(metadata))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export default function (app: App): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) as MutationCallback,
|
}) as MutationCallback,
|
||||||
app.options.angularMode,
|
app.options.forceNgOff,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.attachStopCallback(() => {
|
app.attachStopCallback(() => {
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ class TagWatcher {
|
||||||
this.sessionStorage = params.sessionStorage
|
this.sessionStorage = params.sessionStorage
|
||||||
this.errLog = params.errLog
|
this.errLog = params.errLog
|
||||||
this.onTag = params.onTag
|
this.onTag = params.onTag
|
||||||
|
// @ts-ignore
|
||||||
const tags: { id: number; selector: string }[] = JSON.parse(
|
const tags: { id: number; selector: string }[] = JSON.parse(
|
||||||
sessionStorage.getItem(WATCHED_TAGS_KEY) ?? '[]',
|
params.sessionStorage.getItem(WATCHED_TAGS_KEY) ?? '[]',
|
||||||
)
|
)
|
||||||
this.setTags(tags)
|
this.setTags(tags)
|
||||||
this.observer = new IntersectionObserver((entries) => {
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"outDir": "../../build/cjs"
|
"moduleResolution": "Node",
|
||||||
|
"declarationDir": "../../dist/cjs"
|
||||||
},
|
},
|
||||||
|
"references": [{ "path": "../common" }]
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"lib": ["es2020", "dom"],
|
"lib": ["es2020", "dom"],
|
||||||
"declaration": true
|
"declaration": true,
|
||||||
|
"declarationDir": "../../dist/lib",
|
||||||
},
|
},
|
||||||
"references": [{ "path": "../common" }]
|
"references": [{ "path": "../common" }]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,15 +95,27 @@ export function canAccessIframe(iframe: HTMLIFrameElement) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canAccessTarget(target: any) {
|
export function canAccessTarget(target: EventTarget): boolean {
|
||||||
try {
|
try {
|
||||||
if (target.contentWindow) {
|
if (target instanceof HTMLIFrameElement) {
|
||||||
// If this property is inaccessible, it will throw due to cross-origin restrictions
|
void target.contentDocument
|
||||||
return Boolean(target.contentWindow.location)
|
} else if (target instanceof Window) {
|
||||||
|
void target.document
|
||||||
|
} else if (target instanceof Document) {
|
||||||
|
void target.defaultView
|
||||||
|
} else if ('nodeType' in target) {
|
||||||
|
void (target as Node).nodeType
|
||||||
|
} else if ('addEventListener' in target) {
|
||||||
|
void (target as EventTarget).addEventListener
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false
|
if (e instanceof DOMException && e.name === 'SecurityError') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function dec2hex(dec: number) {
|
function dec2hex(dec: number) {
|
||||||
|
|
@ -143,8 +155,8 @@ export function ngSafeBrowserMethod(method: string): string {
|
||||||
: method
|
: method
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMutationObserver(cb: MutationCallback, angularMode?: boolean) {
|
export function createMutationObserver(cb: MutationCallback, forceNgOff?: boolean) {
|
||||||
if (angularMode) {
|
if (!forceNgOff) {
|
||||||
const mObserver = ngSafeBrowserMethod('MutationObserver') as 'MutationObserver'
|
const mObserver = ngSafeBrowserMethod('MutationObserver') as 'MutationObserver'
|
||||||
return new window[mObserver](cb)
|
return new window[mObserver](cb)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -157,20 +169,24 @@ export function createEventListener(
|
||||||
event: string,
|
event: string,
|
||||||
cb: EventListenerOrEventListenerObject,
|
cb: EventListenerOrEventListenerObject,
|
||||||
capture?: boolean,
|
capture?: boolean,
|
||||||
angularMode?: boolean,
|
forceNgOff?: boolean,
|
||||||
) {
|
) {
|
||||||
// we need to check if target is crossorigin frame or no and if we can access it
|
// we need to check if target is crossorigin frame or no and if we can access it
|
||||||
if (target instanceof HTMLIFrameElement && !canAccessIframe(target)) {
|
if (!canAccessTarget(target)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let safeAddEventListener: 'addEventListener'
|
let safeAddEventListener = 'addEventListener' as unknown as 'addEventListener'
|
||||||
if (angularMode) {
|
if (!forceNgOff) {
|
||||||
safeAddEventListener = ngSafeBrowserMethod('addEventListener') as 'addEventListener'
|
safeAddEventListener = ngSafeBrowserMethod('addEventListener') as 'addEventListener'
|
||||||
} else {
|
|
||||||
safeAddEventListener = 'addEventListener'
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
target[safeAddEventListener](event, cb, capture)
|
// parent has angular, but child frame don't
|
||||||
|
if (target[safeAddEventListener]) {
|
||||||
|
target[safeAddEventListener](event, cb, capture)
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
target.addEventListener(event, cb, capture)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e.message
|
const msg = e.message
|
||||||
console.error(
|
console.error(
|
||||||
|
|
@ -187,19 +203,22 @@ export function deleteEventListener(
|
||||||
event: string,
|
event: string,
|
||||||
cb: EventListenerOrEventListenerObject,
|
cb: EventListenerOrEventListenerObject,
|
||||||
capture?: boolean,
|
capture?: boolean,
|
||||||
angularMode?: boolean,
|
forceNgOff?: boolean,
|
||||||
) {
|
) {
|
||||||
if (target instanceof HTMLIFrameElement && !canAccessIframe(target)) {
|
if (!canAccessTarget(target)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let safeRemoveEventListener: 'removeEventListener'
|
let safeRemoveEventListener = 'removeEventListener' as unknown as 'removeEventListener'
|
||||||
if (angularMode) {
|
if (!forceNgOff) {
|
||||||
safeRemoveEventListener = ngSafeBrowserMethod('removeEventListener') as 'removeEventListener'
|
safeRemoveEventListener = ngSafeBrowserMethod('removeEventListener') as 'removeEventListener'
|
||||||
} else {
|
|
||||||
safeRemoveEventListener = 'removeEventListener'
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
target[safeRemoveEventListener](event, cb, capture)
|
if (target[safeRemoveEventListener]) {
|
||||||
|
target[safeRemoveEventListener](event, cb, capture)
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
target.removeEventListener(event, cb, capture)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e.message
|
const msg = e.message
|
||||||
console.error(
|
console.error(
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,10 @@ describe('AttributeSender', () => {
|
||||||
appMock = {
|
appMock = {
|
||||||
send: (...args: any[]) => args,
|
send: (...args: any[]) => args,
|
||||||
}
|
}
|
||||||
attributeSender = new AttributeSender(appMock, false)
|
attributeSender = new AttributeSender({
|
||||||
|
app: appMock,
|
||||||
|
isDictDisabled: false,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import FeatureFlags, { FetchPersistFlagsData, IFeatureFlag } from '../main/modules/FeatureFlags'
|
import FeatureFlags, { FetchPersistFlagsData, IFeatureFlag } from '../main/modules/featureFlags'
|
||||||
import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globals'
|
import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globals'
|
||||||
|
|
||||||
jest.mock('../main/app/index.js')
|
jest.mock('../main/app/index.js')
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ describe('Nodes', () => {
|
||||||
const mockCallback = jest.fn()
|
const mockCallback = jest.fn()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
nodes = new Nodes(nodeId, false)
|
nodes = new Nodes({
|
||||||
|
node_id: nodeId,
|
||||||
|
forceNgOff: false,
|
||||||
|
})
|
||||||
mockCallback.mockClear()
|
mockCallback.mockClear()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,11 @@ describe('Sanitizer', () => {
|
||||||
getID: (el: { mockId: number }) => el.mockId,
|
getID: (el: { mockId: number }) => el.mockId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// @ts-expect-error
|
sanitizer = new Sanitizer({
|
||||||
sanitizer = new Sanitizer(app, options)
|
// @ts-expect-error
|
||||||
|
app,
|
||||||
|
options,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -78,7 +81,7 @@ describe('Sanitizer', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
sanitizer = new Sanitizer(app, options)
|
sanitizer = new Sanitizer({ app, options })
|
||||||
|
|
||||||
const spanNode = document.createElement('span')
|
const spanNode = document.createElement('span')
|
||||||
const divNode = document.createElement('div')
|
const divNode = document.createElement('div')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { jest, test, describe, beforeEach, afterEach, expect } from '@jest/globals'
|
import { jest, test, describe, beforeEach, afterEach, expect } from '@jest/globals'
|
||||||
import Session from '../main/app/Session'
|
import Session from '../main/app/session'
|
||||||
import App from '../main/app/index.js'
|
import App from '../main/app/index.js'
|
||||||
import { generateRandomId } from '../main/utils.js'
|
import { generateRandomId } from '../main/utils.js'
|
||||||
|
|
||||||
|
|
@ -34,7 +34,10 @@ describe('Session', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
generateRandomId.mockReturnValue('random_id')
|
generateRandomId.mockReturnValue('random_id')
|
||||||
|
|
||||||
session = new Session(mockApp as unknown as App, mockOptions)
|
session = new Session({
|
||||||
|
app: mockApp as App,
|
||||||
|
options: mockOptions,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import TagWatcher, { WATCHED_TAGS_KEY } from '../main/modules/TagWatcher'
|
import TagWatcher, { WATCHED_TAGS_KEY } from '../main/modules/tagWatcher'
|
||||||
import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globals'
|
import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globals'
|
||||||
|
|
||||||
|
const getMockSaved = () => '[{"id":1,"selector":"div"},{"id":2,"selector":"span"}]'
|
||||||
describe('TagWatcher', () => {
|
describe('TagWatcher', () => {
|
||||||
let sessionStorageMock: Storage
|
const sessionStorageMock = {
|
||||||
|
getItem: getMockSaved,
|
||||||
|
setItem: jest.fn(),
|
||||||
|
} as unknown as Storage
|
||||||
let errLogMock: (args: any[]) => void
|
let errLogMock: (args: any[]) => void
|
||||||
const onTag = jest.fn()
|
const onTag = jest.fn()
|
||||||
let mockObserve: Function
|
let mockObserve: Function
|
||||||
|
|
@ -10,11 +14,6 @@ describe('TagWatcher', () => {
|
||||||
let mockDisconnect: Function
|
let mockDisconnect: Function
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sessionStorageMock = {
|
|
||||||
// @ts-ignore
|
|
||||||
getItem: jest.fn(),
|
|
||||||
setItem: jest.fn(),
|
|
||||||
}
|
|
||||||
errLogMock = jest.fn()
|
errLogMock = jest.fn()
|
||||||
mockObserve = jest.fn()
|
mockObserve = jest.fn()
|
||||||
mockUnobserve = jest.fn()
|
mockUnobserve = jest.fn()
|
||||||
|
|
@ -46,11 +45,9 @@ describe('TagWatcher', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
test('constructor initializes with tags from sessionStorage', () => {
|
test('constructor initializes with tags from sessionStorage', () => {
|
||||||
// @ts-ignore
|
const watcher = new TagWatcher({
|
||||||
sessionStorageMock.getItem.mockReturnValue(
|
sessionStorage: sessionStorageMock, errLog: errLogMock, onTag
|
||||||
'[{"id":1,"selector":"div"},{"id":2,"selector":"span"}]',
|
})
|
||||||
)
|
|
||||||
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
|
|
||||||
expect(watcher.tags).toEqual([
|
expect(watcher.tags).toEqual([
|
||||||
{ id: 1, selector: 'div' },
|
{ id: 1, selector: 'div' },
|
||||||
{ id: 2, selector: 'span' },
|
{ id: 2, selector: 'span' },
|
||||||
|
|
@ -73,7 +70,9 @@ describe('TagWatcher', () => {
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
|
const watcher = new TagWatcher({
|
||||||
|
sessionStorage: sessionStorageMock, errLog: errLogMock, onTag
|
||||||
|
})
|
||||||
await watcher.fetchTags('https://localhost.com', '123')
|
await watcher.fetchTags('https://localhost.com', '123')
|
||||||
expect(watcher.tags).toEqual([
|
expect(watcher.tags).toEqual([
|
||||||
{ id: 1, selector: 'div' },
|
{ id: 1, selector: 'div' },
|
||||||
|
|
@ -87,7 +86,9 @@ describe('TagWatcher', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('setTags sets intervals for each tag', () => {
|
test('setTags sets intervals for each tag', () => {
|
||||||
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
|
const watcher = new TagWatcher({
|
||||||
|
sessionStorage: sessionStorageMock, errLog: errLogMock, onTag
|
||||||
|
})
|
||||||
watcher.setTags([
|
watcher.setTags([
|
||||||
{ id: 1, selector: 'div' },
|
{ id: 1, selector: 'div' },
|
||||||
{ id: 2, selector: 'p' },
|
{ id: 2, selector: 'p' },
|
||||||
|
|
@ -98,7 +99,9 @@ describe('TagWatcher', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('onTagRendered sends messages', () => {
|
test('onTagRendered sends messages', () => {
|
||||||
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
|
const watcher = new TagWatcher({
|
||||||
|
sessionStorage: sessionStorageMock, errLog: errLogMock, onTag
|
||||||
|
})
|
||||||
watcher.setTags([{ id: 1, selector: 'div' }])
|
watcher.setTags([{ id: 1, selector: 'div' }])
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
document.querySelectorAll.mockReturnValue([{ __or_watcher_tagname: 'div' }]) // Mock a found element
|
document.querySelectorAll.mockReturnValue([{ __or_watcher_tagname: 'div' }]) // Mock a found element
|
||||||
|
|
@ -109,7 +112,9 @@ describe('TagWatcher', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('clear method clears all intervals and resets tags', () => {
|
test('clear method clears all intervals and resets tags', () => {
|
||||||
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
|
const watcher = new TagWatcher({
|
||||||
|
sessionStorage: sessionStorageMock, errLog: errLogMock, onTag
|
||||||
|
})
|
||||||
watcher.setTags([
|
watcher.setTags([
|
||||||
{ id: 1, selector: 'div' },
|
{ id: 1, selector: 'div' },
|
||||||
{ id: 2, selector: 'p' },
|
{ id: 2, selector: 'p' },
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig-base.json",
|
"extends": "../../tsconfig-base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["es6"]
|
"lib": ["es6", "webworker"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "es2016",
|
||||||
|
"preserveConstEnums": false,
|
||||||
|
"declaration": false
|
||||||
},
|
},
|
||||||
"references": [{ "path": "../common" }]
|
"references": [{ "path": "../common" }]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "build",
|
|
||||||
"declaration": true,
|
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"noImplicitThis": true,
|
"noImplicitThis": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
"target": "es2020",
|
"target": "es2020",
|
||||||
"module": "es6",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
},
|
},
|
||||||
"exclude": ["**/*.test.ts"]
|
"exclude": ["**/*.test.ts"],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue