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:
parent
b33261914d
commit
8251aecede
12 changed files with 125 additions and 66 deletions
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface UserInfo {
|
|||
userState: string
|
||||
}
|
||||
|
||||
interface SessionInfo {
|
||||
export interface SessionInfo {
|
||||
sessionID: string | undefined
|
||||
metadata: Record<string, string>
|
||||
userID: string | null
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue