tracker: fix maintainer doc node stability and build process

This commit is contained in:
nick-delirium 2024-10-21 14:04:27 +02:00
parent f041859a06
commit b205dbed70
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
14 changed files with 91 additions and 52 deletions

View file

@ -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)

View file

@ -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"
}

View file

@ -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
}

View file

@ -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

View file

@ -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'

View file

@ -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)

View file

@ -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

View file

@ -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)
}
})

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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

View file

@ -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" }]
}