fix tracler: improved logs for node binding errors, full nodelist clear before start, state check, getSessionInfo method

* fix tracker: some extra debug logs and safety around node mapping

* fix tracker: some extra debug logs and safety around node mapping

* white spaces

* cleanup

* new fifo scheduler

* node clearing and start check

* new sess info method, better session start logging

* snippet beta
This commit is contained in:
Delirium 2024-04-25 10:09:30 +02:00 committed by GitHub
parent b33261914d
commit 8251aecede
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 125 additions and 66 deletions

View file

@ -84,7 +84,9 @@ export default class MessageLoader {
msg.time = msg.actionTime - this.session.startedAt;
} else {
// @ts-ignore
Object.assign(msg, { actionTime: msg.time + this.session.startedAt });
Object.assign(msg, {
actionTime: msg.time + this.session.startedAt,
});
}
}
if (

Binary file not shown.

View file

@ -1,3 +1,7 @@
# 12.0.10
- improved logs for node binding errors, full nodelist clear before start, getSessionInfo method
# 12.0.9
- moved logging to query

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "12.0.9",
"version": "12.0.10-beta.0",
"keywords": [
"logging",
"replay"

View file

@ -409,14 +409,22 @@ export default class App {
* */
private _nCommit(): void {
if (this.worker !== undefined && this.messages.length) {
requestIdleCb(() => {
this.messages.unshift(TabData(this.session.getTabId()))
this.messages.unshift(Timestamp(this.timestamp()))
// why I need to add opt chaining?
this.worker?.postMessage(this.messages)
this.commitCallbacks.forEach((cb) => cb(this.messages))
this.messages.length = 0
})
try {
requestIdleCb(() => {
this.messages.unshift(TabData(this.session.getTabId()))
this.messages.unshift(Timestamp(this.timestamp()))
// why I need to add opt chaining?
this.worker?.postMessage(this.messages)
this.commitCallbacks.forEach((cb) => cb(this.messages))
this.messages.length = 0
})
} catch (e) {
this._debug('worker_commit', e)
this.stop(true)
setTimeout(() => {
void this.start()
}, 500)
}
}
}
@ -1177,6 +1185,15 @@ export default class App {
* and here we just apply 10ms delay just in case
* */
start(...args: Parameters<App['_start']>): Promise<StartPromiseReturn> {
if (
this.activityState === ActivityState.Active ||
this.activityState === ActivityState.Starting
) {
const reason =
'OpenReplay: trying to call `start()` on the instance that has been started already.'
return Promise.resolve(UnsuccessfulStart(reason))
}
if (!document.hidden) {
return new Promise((resolve) => {
setTimeout(() => {

View file

@ -49,6 +49,7 @@ export default class Nodes {
unregisterNode(node: Node): number | undefined {
const id = (node as any)[this.node_id]
if (id !== undefined) {
;(node as any)[this.node_id] = undefined
delete (node as any)[this.node_id]
delete this.nodes[id]
const listeners = this.elementListeners.get(id)

View file

@ -206,10 +206,14 @@ export default abstract class Observer {
node,
NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT,
{
acceptNode: (node) =>
isIgnored(node) || this.app.nodes.getID(node) !== undefined
acceptNode: (node) => {
if (this.app.nodes.getID(node) !== undefined) {
this.app.debug.error('! Node is already bound', node)
}
return isIgnored(node) || this.app.nodes.getID(node) !== undefined
? NodeFilter.FILTER_REJECT
: NodeFilter.FILTER_ACCEPT,
: NodeFilter.FILTER_ACCEPT
},
},
// @ts-ignore
false,

View file

@ -122,6 +122,7 @@ export default class TopObserver extends Observer {
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>
// In this case context.document have to be observed, but this will cause

View file

@ -10,7 +10,7 @@ interface UserInfo {
userState: string
}
interface SessionInfo {
export interface SessionInfo {
sessionID: string | undefined
metadata: Record<string, string>
userID: string | null

View file

@ -39,6 +39,7 @@ import type { Options as PerformanceOptions } from './modules/performance.js'
import type { Options as TimingOptions } from './modules/timing.js'
import type { Options as NetworkOptions } from './modules/network.js'
import type { MouseHandlerOptions } from './modules/mouse.js'
import type { SessionInfo } from './app/session.js'
import type { StartOptions } from './app/index.js'
//TODO: unique options init
@ -381,6 +382,13 @@ export default class API {
return this.app.getSessionToken()
}
getSessionInfo(): SessionInfo | null {
if (this.app === null) {
return null
}
return this.app.session.getInfo()
}
getSessionID(): string | null | undefined {
if (this.app === null) {
return null

View file

@ -167,27 +167,70 @@ export function deleteEventListener(
}
}
/**
* This is a brief polyfill that suits our needs
* I took inspiration from Microsoft Clarity polyfill on this one
* then adapted it a little bit
*
* I'm very grateful for their bright idea
* */
export function requestIdleCb(callback: () => void) {
const taskTimeout = 3000
if (window.requestIdleCallback) {
return window.requestIdleCallback(callback, { timeout: taskTimeout })
} else {
const channel = new MessageChannel()
const incoming = channel.port1
const outgoing = channel.port2
incoming.onmessage = (): void => {
callback()
class FIFOTaskScheduler {
taskQueue: any[]
isRunning: boolean
constructor() {
this.taskQueue = []
this.isRunning = false
}
// Adds a task to the queue
addTask(task: () => any) {
this.taskQueue.push(task)
this.runTasks()
}
// Runs tasks from the queue
runTasks() {
if (this.isRunning || this.taskQueue.length === 0) {
return
}
requestAnimationFrame((): void => {
outgoing.postMessage(1)
})
this.isRunning = true
const executeNextTask = () => {
if (this.taskQueue.length === 0) {
this.isRunning = false
return
}
// Get the next task and execute it
const nextTask = this.taskQueue.shift()
Promise.resolve(nextTask()).then(() => {
requestAnimationFrame(() => executeNextTask())
})
}
executeNextTask()
}
}
const scheduler = new FIFOTaskScheduler()
export function requestIdleCb(callback: () => void) {
// performance improvement experiment;
scheduler.addTask(callback)
/**
* This is a brief polyfill that suits our needs
* I took inspiration from Microsoft Clarity polyfill on this one
* then adapted it a little bit
*
* I'm very grateful for their bright idea
* */
// const taskTimeout = 3000
// if (window.requestIdleCallback) {
// return window.requestIdleCallback(callback, { timeout: taskTimeout })
// } else {
// const channel = new MessageChannel()
// const incoming = channel.port1
// const outgoing = channel.port2
//
// incoming.onmessage = (): void => {
// callback()
// }
// requestAnimationFrame((): void => {
// outgoing.postMessage(1)
// })
// }
}

View file

@ -204,41 +204,20 @@ describe('ngSafeBrowserMethod', () => {
})
describe('requestIdleCb', () => {
test('uses window.requestIdleCallback when available', () => {
const callback = jest.fn()
test('testing FIFO scheduler', async () => {
jest.useFakeTimers()
// @ts-ignore
window.requestIdleCallback = callback
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
const cb1 = jest.fn()
const cb2 = jest.fn()
requestIdleCb(callback)
requestIdleCb(cb1)
requestIdleCb(cb2)
expect(callback).toBeCalled()
})
expect(cb1).toBeCalled()
expect(cb2).toBeCalledTimes(0)
await jest.advanceTimersToNextTimerAsync(1)
test('falls back to using a MessageChannel if requestIdleCallback is not available', () => {
const callback = jest.fn()
class MessageChannelMock {
port1 = {
// @ts-ignore
postMessage: (v: any) => this.port2.onmessage(v),
onmessage: null,
}
port2 = {
onmessage: null,
// @ts-ignore
postMessage: (v: any) => this.port1.onmessage(v),
}
}
// @ts-ignore
globalThis.MessageChannel = MessageChannelMock
// @ts-ignore
globalThis.requestAnimationFrame = (cb: () => void) => cb()
// @ts-ignore
window.requestIdleCallback = undefined
requestIdleCb(callback)
// You can assert that the callback was called using the MessageChannel approach.
// This is more challenging to test, so it's recommended to mock MessageChannel.
expect(callback).toBeCalled()
expect(cb2).toBeCalledTimes(1)
})
})