diff --git a/frontend/app/player/web/MessageLoader.ts b/frontend/app/player/web/MessageLoader.ts index 35dbcfe4d..3131029d8 100644 --- a/frontend/app/player/web/MessageLoader.ts +++ b/frontend/app/player/web/MessageLoader.ts @@ -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 ( diff --git a/tracker/tracker-assist/bun.lockb b/tracker/tracker-assist/bun.lockb index 378da5fb7..6dea11a76 100755 Binary files a/tracker/tracker-assist/bun.lockb and b/tracker/tracker-assist/bun.lockb differ diff --git a/tracker/tracker/CHANGELOG.md b/tracker/tracker/CHANGELOG.md index 5d73d8447..de821b7ab 100644 --- a/tracker/tracker/CHANGELOG.md +++ b/tracker/tracker/CHANGELOG.md @@ -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 diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index ce35e3561..b3d7a0de2 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -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" diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index dcb0560ce..fbbfd444d 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -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): Promise { + 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(() => { diff --git a/tracker/tracker/src/main/app/nodes.ts b/tracker/tracker/src/main/app/nodes.ts index 92612d2e5..b2b9cc136 100644 --- a/tracker/tracker/src/main/app/nodes.ts +++ b/tracker/tracker/src/main/app/nodes.ts @@ -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) diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts index da02138ab..adbada4e9 100644 --- a/tracker/tracker/src/main/app/observer/observer.ts +++ b/tracker/tracker/src/main/app/observer/observer.ts @@ -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, diff --git a/tracker/tracker/src/main/app/observer/top_observer.ts b/tracker/tracker/src/main/app/observer/top_observer.ts index f5db56549..24cb3e270 100644 --- a/tracker/tracker/src/main/app/observer/top_observer.ts +++ b/tracker/tracker/src/main/app/observer/top_observer.ts @@ -122,6 +122,7 @@ export default class TopObserver extends Observer { return shadow } + this.app.nodes.clear() // Can observe documentElement () here, because it is not supposed to be changing. // However, it is possible in some exotic cases and may cause an ignorance of the newly created // In this case context.document have to be observed, but this will cause diff --git a/tracker/tracker/src/main/app/session.ts b/tracker/tracker/src/main/app/session.ts index 2580843b9..7a5ca81c1 100644 --- a/tracker/tracker/src/main/app/session.ts +++ b/tracker/tracker/src/main/app/session.ts @@ -10,7 +10,7 @@ interface UserInfo { userState: string } -interface SessionInfo { +export interface SessionInfo { sessionID: string | undefined metadata: Record userID: string | null diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index daae49b32..933ec08e0 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -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 diff --git a/tracker/tracker/src/main/utils.ts b/tracker/tracker/src/main/utils.ts index 9dd27a21c..15946b4a6 100644 --- a/tracker/tracker/src/main/utils.ts +++ b/tracker/tracker/src/main/utils.ts @@ -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) + // }) + // } +} diff --git a/tracker/tracker/src/tests/utils.unit.test.ts b/tracker/tracker/src/tests/utils.unit.test.ts index 8aef77110..19d83418b 100644 --- a/tracker/tracker/src/tests/utils.unit.test.ts +++ b/tracker/tracker/src/tests/utils.unit.test.ts @@ -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) }) })