tracker: fix maintainer doc node stability and build process
This commit is contained in:
parent
f041859a06
commit
b205dbed70
14 changed files with 91 additions and 52 deletions
Binary file not shown.
|
|
@ -1,3 +1,7 @@
|
|||
## 14.0.11 & .12
|
||||
|
||||
- fix for node maintainer stability around `#document` nodes (mainly iframes field)
|
||||
|
||||
## 14.0.10
|
||||
|
||||
- adjust timestamps for messages from tracker instances inside child iframes (if they were loaded later)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"description": "The OpenReplay tracker main package",
|
||||
"version": "14.0.10",
|
||||
"version": "14.0.12",
|
||||
"keywords": [
|
||||
"logging",
|
||||
"replay"
|
||||
|
|
@ -50,19 +50,19 @@
|
|||
"@rollup/plugin-replace": "^6.0.1",
|
||||
"@rollup/plugin-terser": "0.4.4",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
"@typescript-eslint/parser": "^5.30.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.10.0",
|
||||
"@typescript-eslint/parser": "^8.10.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"prettier": "^3.0.3",
|
||||
"replace-in-files": "^2.0.3",
|
||||
"rollup": "^4.1.4",
|
||||
"semver": "^6.3.0",
|
||||
"ts-jest": "^29.0.3",
|
||||
"tslib": "^2.8.0",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -75,13 +75,5 @@
|
|||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,mjs,jsx,ts,tsx}": [
|
||||
"eslint --fix --quiet"
|
||||
],
|
||||
"*.{json,md,html,js,jsx,ts,tsx}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.5.0"
|
||||
"packageManager": "yarn@4.5.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ async function buildWebWorker() {
|
|||
inlineDynamicImports: true,
|
||||
})
|
||||
const webWorkerCode = output[0].code
|
||||
console.log('webworker done!')
|
||||
return webWorkerCode.replace(/"/g, '\\"').replace(/\n/g, '')
|
||||
|
||||
console.log('webworker done!', output.length)
|
||||
|
||||
return webWorkerCode
|
||||
}
|
||||
|
|
|
|||
10
tracker/tracker/src/common/messages.gen.d.ts
vendored
10
tracker/tracker/src/common/messages.gen.d.ts
vendored
|
|
@ -74,7 +74,8 @@ export declare const enum Type {
|
|||
TagTrigger = 120,
|
||||
Redux = 121,
|
||||
SetPageLocation = 122,
|
||||
GraphQL = 123
|
||||
GraphQL = 123,
|
||||
WebVitals = 124
|
||||
}
|
||||
export type Timestamp = [
|
||||
Type.Timestamp,
|
||||
|
|
@ -544,5 +545,10 @@ export type GraphQL = [
|
|||
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 type WebVitals = [
|
||||
Type.WebVitals,
|
||||
string,
|
||||
string
|
||||
];
|
||||
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 | WebVitals;
|
||||
export default Message;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -20,7 +20,7 @@ interface Options {
|
|||
|
||||
class CanvasRecorder {
|
||||
private snapshots: Record<number, CanvasSnapshot> = {}
|
||||
private readonly intervals: NodeJS.Timeout[] = []
|
||||
private readonly intervals: ReturnType<typeof setInterval>[] = []
|
||||
private readonly interval: number
|
||||
private readonly fileExt: 'webp' | 'png' | 'jpeg' | 'avif'
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,11 @@ interface OnStartInfo {
|
|||
userUUID: string
|
||||
}
|
||||
|
||||
/**
|
||||
* this value is injected during build time via rollup
|
||||
* */
|
||||
// @ts-ignore
|
||||
const workerBodyFn = WEBWORKER_BODY
|
||||
const CANCELED = 'canceled' as const
|
||||
const uxtStorageKey = 'or_uxt_active'
|
||||
const bufferStorageKey = 'or_buffer_1'
|
||||
|
|
@ -657,7 +662,7 @@ export default class App {
|
|||
},
|
||||
this.options.crossdomain?.parentDomain ?? '*',
|
||||
)
|
||||
console.log('Trying to signal to parent, attempt:', retries + 1)
|
||||
this.debug.info('Trying to signal to parent, attempt:', retries + 1)
|
||||
retries++
|
||||
}
|
||||
|
||||
|
|
@ -693,6 +698,7 @@ export default class App {
|
|||
this.pageFrames = pageIframes
|
||||
targetFrame = pageIframes.find((frame) => frame.contentWindow === source)
|
||||
}
|
||||
|
||||
if (!targetFrame) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -721,7 +727,7 @@ export default class App {
|
|||
private initWorker() {
|
||||
try {
|
||||
this.worker = new Worker(
|
||||
URL.createObjectURL(new Blob(['WEBWORKER_BODY'], { type: 'text/javascript' })),
|
||||
URL.createObjectURL(new Blob([workerBodyFn], { type: 'text/javascript' })),
|
||||
)
|
||||
this.worker.onerror = (e) => {
|
||||
this._debug('webworker_error', e)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ export default class Logger {
|
|||
return this.level >= level
|
||||
}
|
||||
|
||||
info = (...args: any[]) => {
|
||||
if (this.shouldLog(LogLevel.Verbose)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
console.info(...args)
|
||||
}
|
||||
}
|
||||
|
||||
log = (...args: any[]) => {
|
||||
if (this.shouldLog(LogLevel.Log)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
|
|
|
|||
|
|
@ -30,30 +30,32 @@ function processMapInBatches(
|
|||
processNextBatch()
|
||||
}
|
||||
|
||||
function isNodeStillActive(node: Node): boolean {
|
||||
function isNodeStillActive(node: Node): [isCon: boolean, reason: string] {
|
||||
try {
|
||||
if (!node.isConnected) {
|
||||
return false
|
||||
return [false, 'not connected']
|
||||
}
|
||||
const nodeIsDocument = node.nodeType === Node.DOCUMENT_NODE
|
||||
const nodeWindow = nodeIsDocument
|
||||
? (node as Document).defaultView
|
||||
: node.ownerDocument?.defaultView
|
||||
|
||||
const nodeWindow = node.ownerDocument?.defaultView
|
||||
|
||||
const ownerDoc = nodeIsDocument ? (node as Document) : node.ownerDocument
|
||||
if (!nodeWindow) {
|
||||
return false
|
||||
return [false, 'no window']
|
||||
}
|
||||
|
||||
if (nodeWindow.closed) {
|
||||
return false
|
||||
return [false, 'window closed']
|
||||
}
|
||||
|
||||
if (!node.ownerDocument.documentElement.isConnected) {
|
||||
return false
|
||||
if (!ownerDoc?.documentElement.isConnected) {
|
||||
return [false, 'documentElement not connected']
|
||||
}
|
||||
|
||||
return true
|
||||
return [true, 'ok']
|
||||
} catch (e) {
|
||||
console.error('Error checking node activity:', e)
|
||||
return false
|
||||
return [false, e]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +104,8 @@ class Maintainer {
|
|||
|
||||
this.interval = setInterval(() => {
|
||||
processMapInBatches(this.nodes, this.options.batchSize, (node) => {
|
||||
if (!isNodeStillActive(node)) {
|
||||
const isActive = isNodeStillActive(node)[0]
|
||||
if (!isActive) {
|
||||
this.unregisterNode(node)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import Observer from './observer.js'
|
||||
import { CreateIFrameDocument } from '../messages.gen.js'
|
||||
import { CreateIFrameDocument, RemoveNode } from '../messages.gen.js'
|
||||
|
||||
export default class IFrameObserver extends Observer {
|
||||
docId: number | undefined
|
||||
observe(iframe: HTMLIFrameElement) {
|
||||
const doc = iframe.contentDocument
|
||||
const hostID = this.app.nodes.getID(iframe)
|
||||
if (!doc || hostID === undefined) {
|
||||
return
|
||||
} //log TODO common app.logger
|
||||
}
|
||||
// Have to observe document, because the inner <html> might be changed
|
||||
this.observeRoot(doc, (docID) => {
|
||||
//MBTODO: do not send if empty (send on load? it might be in-place iframe, like our replayer, which does not get loaded)
|
||||
|
|
@ -15,6 +16,7 @@ export default class IFrameObserver extends Observer {
|
|||
this.app.debug.log('OpenReplay: Iframe document not bound')
|
||||
return
|
||||
}
|
||||
this.docId = docID
|
||||
this.app.send(CreateIFrameDocument(hostID, docID))
|
||||
})
|
||||
}
|
||||
|
|
@ -28,4 +30,11 @@ export default class IFrameObserver extends Observer {
|
|||
this.app.send(CreateIFrameDocument(rootNodeId, docID))
|
||||
})
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.docId !== undefined) {
|
||||
this.app.send(RemoveNode(this.docId))
|
||||
}
|
||||
super.disconnect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ export default class TopObserver extends Observer {
|
|||
},
|
||||
params.options,
|
||||
)
|
||||
|
||||
// IFrames
|
||||
this.app.nodes.attachNodeCallback((node) => {
|
||||
if (
|
||||
|
|
@ -64,25 +63,34 @@ export default class TopObserver extends Observer {
|
|||
return this.iframeOffsets.getDocumentOffset(doc)
|
||||
}
|
||||
|
||||
private iframeObserversArr: IFrameObserver[] = []
|
||||
private iframeObservers: WeakMap<HTMLIFrameElement | Document, IFrameObserver> = new WeakMap()
|
||||
private docObservers: WeakMap<Document, IFrameObserver> = new WeakMap()
|
||||
private handleIframe(iframe: HTMLIFrameElement): void {
|
||||
let doc: Document | null = null
|
||||
// setTimeout is required. Otherwise some event listeners (scroll, mousemove) applied in modules
|
||||
// do not work on the iframe document when it 've been loaded dynamically ((why?))
|
||||
// do not work on the iframe document when it 've been loaded dynamically ((why?))
|
||||
const handle = this.app.safe(() =>
|
||||
setTimeout(() => {
|
||||
const id = this.app.nodes.getID(iframe)
|
||||
if (id === undefined || !canAccessIframe(iframe)) return
|
||||
const currentWin = iframe.contentWindow
|
||||
const currentDoc = iframe.contentDocument
|
||||
if (currentDoc && currentDoc !== doc) {
|
||||
const observer = new IFrameObserver(this.app)
|
||||
this.iframeObservers.set(iframe, observer)
|
||||
observer.observe(iframe) // TODO: call unregisterNode for the previous doc if present (incapsulate: one iframe - one observer)
|
||||
doc = currentDoc
|
||||
|
||||
this.iframeOffsets.observe(iframe)
|
||||
if (!currentDoc) {
|
||||
this.app.debug.warn('no doc for iframe found', iframe)
|
||||
return
|
||||
}
|
||||
if (currentDoc && this.docObservers.has(currentDoc)) {
|
||||
this.app.debug.info('doc already observed for', id)
|
||||
return
|
||||
}
|
||||
const observer = new IFrameObserver(this.app)
|
||||
this.iframeObservers.set(iframe, observer)
|
||||
this.docObservers.set(currentDoc, observer)
|
||||
this.iframeObserversArr.push(observer)
|
||||
|
||||
observer.observe(iframe)
|
||||
|
||||
this.iframeOffsets.observe(iframe)
|
||||
if (
|
||||
currentWin &&
|
||||
// Sometimes currentWin.window is null (not in specification). Such window object is not functional
|
||||
|
|
@ -91,13 +99,13 @@ export default class TopObserver extends Observer {
|
|||
//TODO: more explicit logic
|
||||
) {
|
||||
this.contextsSet.add(currentWin)
|
||||
//@ts-ignore https://github.com/microsoft/TypeScript/issues/41684
|
||||
// @ts-ignore https://github.com/microsoft/TypeScript/issues/41684
|
||||
this.contextCallbacks.forEach((cb) => cb(currentWin))
|
||||
}
|
||||
// we need this delay because few iframes stacked one in another with rapid updates will break the player (or browser engine rather?)
|
||||
}, 100),
|
||||
}, 250),
|
||||
)
|
||||
iframe.addEventListener('load', handle) // why app.attachEventListener not working?
|
||||
iframe.addEventListener('load', handle)
|
||||
handle()
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +126,6 @@ export default class TopObserver extends Observer {
|
|||
observer.handleShadowRoot(shadow)
|
||||
return shadow
|
||||
}
|
||||
|
||||
this.app.nodes.clear()
|
||||
// Can observe documentElement (<html>) here, because it is not supposed to be changing.
|
||||
// However, it is possible in some exotic cases and may cause an ignorance of the newly created <html>
|
||||
|
|
@ -155,8 +162,11 @@ export default class TopObserver extends Observer {
|
|||
disconnect() {
|
||||
this.iframeOffsets.clear()
|
||||
Element.prototype.attachShadow = attachShadowNativeFn
|
||||
this.iframeObserversArr.forEach((observer) => observer.disconnect())
|
||||
this.iframeObserversArr = []
|
||||
this.iframeObservers = new WeakMap()
|
||||
this.shadowRootObservers = new WeakMap()
|
||||
this.docObservers = new WeakMap()
|
||||
super.disconnect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export default function (app: App, options?: MouseHandlerOptions): void {
|
|||
let direction = 0
|
||||
let directionChangeCount = 0
|
||||
let distance = 0
|
||||
let checkIntervalId: NodeJS.Timer
|
||||
let checkIntervalId: ReturnType<typeof setInterval>
|
||||
const shakeThreshold = 0.008
|
||||
const shakeCheckInterval = 225
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"extends": "../../tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["es6", "webworker"],
|
||||
"lib": ["ES2020", "webworker"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"target": "es2016",
|
||||
"preserveConstEnums": false,
|
||||
"declaration": false
|
||||
},
|
||||
"references": [{ "path": "../common" }]
|
||||
"references": [{ "path": "../../common" }]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue