diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.tsx b/frontend/app/components/Session_/Player/Controls/Timeline.tsx index 7dee7cab0..31cde2d87 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.tsx +++ b/frontend/app/components/Session_/Player/Controls/Timeline.tsx @@ -43,14 +43,15 @@ function Timeline(props: IProps) { devtoolsLoading, domLoading, tabStates, - currentTab, } = store.get() const { issues } = props; const notes = notesStore.sessionNotes const progressRef = useRef(null) const timelineRef = useRef(null) - const events = tabStates[currentTab]?.eventList || []; + const events = Object.keys(tabStates).length > 0 ? Object.keys(tabStates).reduce((acc, tabId) => { + return acc.concat(tabStates[tabId].eventList) + }, []) : [] const scale = 100 / endTime; diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index d17c0e8d2..f058dfa64 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -188,11 +188,11 @@ export default class MessageManager { this.screen.cursor.shake(); } - if (tabId) { if (this.activeTab !== tabId) { this.state.update({ currentTab: tabId }); this.activeTab = tabId; + this.tabs[this.activeTab].clean(); } const activeTabs = this.state.get().tabs; if (activeTabs.length !== this.activeTabManager.tabInstances.size) { @@ -226,6 +226,7 @@ export default class MessageManager { public changeTab(tabId: string) { this.activeTab = tabId; this.state.update({ currentTab: tabId }); + this.tabs[tabId].clean(); this.tabs[tabId].move(this.state.get().time); } diff --git a/frontend/app/player/web/assist/AssistManager.ts b/frontend/app/player/web/assist/AssistManager.ts index a921a889b..50ddc369c 100644 --- a/frontend/app/player/web/assist/AssistManager.ts +++ b/frontend/app/player/web/assist/AssistManager.ts @@ -186,20 +186,20 @@ export default class AssistManager { }) socket.on('UPDATE_SESSION', (evData) => { - const { metadata = {}, data = {} } = evData - const { tabId } = metadata + const { meta = {}, data = {} } = evData + const { tabId } = meta const { active } = data + const currentTab = this.store.get().currentTab this.clearDisconnectTimeout() !this.inactiveTimeout && this.setStatus(ConnectionStatus.Connected) - if (Boolean(tabId) && tabId !== this.store.get().currentTab) { - this.store.update({ currentTab: tabId }) - } if (typeof active === "boolean") { this.clearInactiveTimeout() if (active) { this.setStatus(ConnectionStatus.Connected) } else { - this.inactiveTimeout = setTimeout(() => this.setStatus(ConnectionStatus.Inactive), 5000) + if (tabId === currentTab) { + this.inactiveTimeout = setTimeout(() => this.setStatus(ConnectionStatus.Inactive), 5000) + } } } }) diff --git a/frontend/app/player/web/managers/DOM/DOMManager.ts b/frontend/app/player/web/managers/DOM/DOMManager.ts index 2b98a8cfe..18700fd50 100644 --- a/frontend/app/player/web/managers/DOM/DOMManager.ts +++ b/frontend/app/player/web/managers/DOM/DOMManager.ts @@ -13,7 +13,6 @@ import { VDocument, VElement, VHTMLElement, - VNode, VShadowRoot, VText, OnloadVRoot, diff --git a/frontend/app/player/web/managers/PagesManager.ts b/frontend/app/player/web/managers/PagesManager.ts index e90b10cfd..18722f962 100644 --- a/frontend/app/player/web/managers/PagesManager.ts +++ b/frontend/app/player/web/managers/PagesManager.ts @@ -68,5 +68,4 @@ export default class PagesManager extends ListWalker { } return Promise.resolve() } - } \ No newline at end of file diff --git a/tracker/tracker-assist/package.json b/tracker/tracker-assist/package.json index 836050fa2..bcdee50af 100644 --- a/tracker/tracker-assist/package.json +++ b/tracker/tracker-assist/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker-assist", "description": "Tracker plugin for screen assistance through the WebRTC", - "version": "6.0.0-beta.10", + "version": "6.0.0-beta.11", "keywords": [ "WebRTC", "assistance", diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index 0a1dcc089..d76c42964 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": "8.0.0-beta.1", + "version": "8.0.0-beta.5", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index a765d2b08..2b4ea4837 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -9,7 +9,7 @@ import Logger, { LogLevel } from './logger.js' import Session from './session.js' import { gzip } from 'fflate' import { deviceMemory, jsHeapSizeLimit } from '../modules/performance.js' - +import AttributeSender from '../modules/attributeSender.js' import type { Options as ObserverOptions } from './observer/top_observer.js' import type { Options as SanitizerOptions } from './sanitizer.js' import type { Options as LoggerOptions } from './logger.js' @@ -118,6 +118,7 @@ export default class App { private compressionThreshold = 24 * 1000 private restartAttempts = 0 private readonly bc: BroadcastChannel = new BroadcastChannel('rick') + public attributeSender: AttributeSender constructor(projectKey: string, sessionToken: string | undefined, options: Partial) { // if (options.onStart !== undefined) { @@ -158,6 +159,7 @@ export default class App { this.debug = new Logger(this.options.__debug__) this.notify = new Logger(this.options.verbose ? LogLevel.Warnings : LogLevel.Silent) this.session = new Session(this, this.options) + this.attributeSender = new AttributeSender(this) this.session.attachUpdateCallback(({ userID, metadata }) => { if (userID != null) { // TODO: nullable userID @@ -643,6 +645,7 @@ export default class App { stop(stopWorker = true): void { if (this.activityState !== ActivityState.NotActive) { try { + this.attributeSender.clear() this.sanitizer.clear() this.observer.disconnect() this.nodes.clear() diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts index 4fabdf3e5..77d66e6a7 100644 --- a/tracker/tracker/src/main/app/observer/observer.ts +++ b/tracker/tracker/src/main/app/observer/observer.ts @@ -1,6 +1,5 @@ import { RemoveNodeAttribute, - SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, @@ -139,7 +138,7 @@ export default abstract class Observer { } this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())) } else { - this.app.send(SetNodeAttribute(id, name, value)) + this.app.attributeSender.sendSetAttribute(id, name, value) } return } @@ -173,7 +172,7 @@ export default abstract class Observer { if (name === 'href' || value.length > 1e5) { value = '' } - this.app.send(SetNodeAttribute(id, name, value)) + this.app.attributeSender.sendSetAttribute(id, name, value) } private sendNodeData(id: number, parentElement: Element, data: string): void { diff --git a/tracker/tracker/src/main/modules/attributeSender.ts b/tracker/tracker/src/main/modules/attributeSender.ts new file mode 100644 index 000000000..9b99a1552 --- /dev/null +++ b/tracker/tracker/src/main/modules/attributeSender.ts @@ -0,0 +1,44 @@ +import { SetNodeAttributeDict, Type } from '../../common/messages.gen.js' +import App from '../app/index.js' + +export class StringDictionary { + private idx = 1 + private backDict: Record = {} + + getKey(str: string): [number, boolean] { + let isNew = false + if (!this.backDict[str]) { + isNew = true + this.backDict[str] = this.idx++ + } + return [this.backDict[str], isNew] + } +} + +export default class AttributeSender { + private dict = new StringDictionary() + + constructor(private readonly app: App) {} + + public sendSetAttribute(id: number, name: string, value: string) { + const message: SetNodeAttributeDict = [ + Type.SetNodeAttributeDict, + id, + this.applyDict(name), + this.applyDict(value), + ] + this.app.send(message) + } + + private applyDict(str: string): number { + const [key, isNew] = this.dict.getKey(str) + if (isNew) { + this.app.send([Type.StringDict, key, str]) + } + return key + } + + clear() { + this.dict = new StringDictionary() + } +} diff --git a/tracker/tracker/src/main/modules/img.ts b/tracker/tracker/src/main/modules/img.ts index 0099f6969..d9ac6cd1c 100644 --- a/tracker/tracker/src/main/modules/img.ts +++ b/tracker/tracker/src/main/modules/img.ts @@ -1,6 +1,6 @@ import type App from '../app/index.js' import { isURL, IS_FIREFOX, MAX_STR_LEN } from '../utils.js' -import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from '../app/messages.gen.js' +import { ResourceTiming, SetNodeAttributeURLBased } from '../app/messages.gen.js' import { hasTag } from '../app/guards.js' function resolveURL(url: string, location: Location = document.location) { @@ -28,13 +28,13 @@ const PLACEHOLDER_SRC = 'https://static.openreplay.com/tracker/placeholder.jpeg' export default function (app: App): void { function sendPlaceholder(id: number, node: HTMLImageElement): void { - app.send(SetNodeAttribute(id, 'src', PLACEHOLDER_SRC)) + app.attributeSender.sendSetAttribute(id, 'src', PLACEHOLDER_SRC) const { width, height } = node.getBoundingClientRect() if (!node.hasAttribute('width')) { - app.send(SetNodeAttribute(id, 'width', String(width))) + app.attributeSender.sendSetAttribute(id, 'width', String(width)) } if (!node.hasAttribute('height')) { - app.send(SetNodeAttribute(id, 'height', String(height))) + app.attributeSender.sendSetAttribute(id, 'height', String(height)) } } @@ -47,7 +47,7 @@ export default function (app: App): void { .split(',') .map((str) => resolveURL(str)) .join(',') - app.send(SetNodeAttribute(id, 'srcset', resolvedSrcset)) + app.attributeSender.sendSetAttribute(id, 'srcset', resolvedSrcset) } const sendSrc = function (id: number, img: HTMLImageElement): void { diff --git a/tracker/tracker/src/webworker/StringDictionary.unit.test.ts b/tracker/tracker/src/tests/StringDictionary.unit.test.ts similarity index 92% rename from tracker/tracker/src/webworker/StringDictionary.unit.test.ts rename to tracker/tracker/src/tests/StringDictionary.unit.test.ts index 8bf17e14b..dfe742d51 100644 --- a/tracker/tracker/src/webworker/StringDictionary.unit.test.ts +++ b/tracker/tracker/src/tests/StringDictionary.unit.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, jest, beforeEach, afterEach } from '@jest/globals' -import StringDictionary from './StringDictionary.js' +import { StringDictionary } from '../main/modules/attributeSender.js' describe('StringDictionary', () => { test('key is non-zero', () => { diff --git a/tracker/tracker/src/tests/attributeSender.unit.test.ts b/tracker/tracker/src/tests/attributeSender.unit.test.ts new file mode 100644 index 000000000..eb46dd9c8 --- /dev/null +++ b/tracker/tracker/src/tests/attributeSender.unit.test.ts @@ -0,0 +1,93 @@ +import { Type } from '../common/messages.gen.js' +import AttributeSender from '../main/modules/attributeSender.js' +import { describe, expect, test, jest, beforeEach, afterEach } from '@jest/globals' + +describe('AttributeSender', () => { + let attributeSender + let appMock + + beforeEach(() => { + appMock = { + send: (...args) => args, + } + attributeSender = new AttributeSender(appMock) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + test('should send the set attribute message to the app', () => { + const sendSpy = jest.spyOn(appMock, 'send') + const id = 1 + const name = 'color' + const value = 'red' + const expectedMessage = [Type.SetNodeAttributeDict, id, 1, 2] + + attributeSender.sendSetAttribute(id, name, value) + + expect(sendSpy).toHaveBeenCalledWith(expectedMessage) + }) + + test('should apply dictionary to the attribute name and value', () => { + const id = 1 + const name = 'color' + const value = 'red' + const sendSpy = jest.spyOn(appMock, 'send') + + attributeSender.sendSetAttribute(id, name, value) + + expect(sendSpy).toHaveBeenCalledWith( + expect.arrayContaining([ + Type.SetNodeAttributeDict, + id, + expect.any(Number), + expect.any(Number), + ]), + ) + }) + + test('should send the string dictionary entry if the attribute is new', () => { + const id = 1 + const name = 'color' + const value = 'red' + const sendSpy = jest.spyOn(appMock, 'send') + + attributeSender.sendSetAttribute(id, name, value) + + expect(sendSpy).toHaveBeenCalledWith([Type.StringDict, expect.any(Number), name]) + }) + + test('should not send the string dictionary entry if the attribute already exists', () => { + const id = 1 + const name = 'color' + const value = 'red' + const sendSpy = jest.spyOn(appMock, 'send') + + attributeSender.sendSetAttribute(id, name, value) + attributeSender.sendSetAttribute(id, name, value) + + // 2 attributes + 1 stringDict name + 1 stringDict value + expect(sendSpy).toHaveBeenCalledTimes(4) + expect(sendSpy).toHaveBeenCalledWith( + expect.not.arrayContaining([Type.StringDict, expect.any(Number), name]), + ) + }) + + test('should clear the dictionary', () => { + const id = 1 + const name = 'color' + const value = 'red' + const sendSpy = jest.spyOn(appMock, 'send') + + attributeSender.sendSetAttribute(id, name, value) + attributeSender.clear() + attributeSender.sendSetAttribute(id, name, value) + + // (attribute + stringDict name + stringDict value) * 2 = 6 + expect(sendSpy).toHaveBeenCalledTimes(6) + expect(sendSpy).toHaveBeenCalledWith( + expect.arrayContaining([Type.StringDict, expect.any(Number), name]), + ) + }) +}) diff --git a/tracker/tracker/src/webworker/BatchWriter.ts b/tracker/tracker/src/webworker/BatchWriter.ts index e69784768..cf704f505 100644 --- a/tracker/tracker/src/webworker/BatchWriter.ts +++ b/tracker/tracker/src/webworker/BatchWriter.ts @@ -1,7 +1,6 @@ import type Message from '../common/messages.gen.js' import * as Messages from '../common/messages.gen.js' import MessageEncoder from './MessageEncoder.gen.js' -import StringDictionary from './StringDictionary.js' const SIZE_BYTES = 3 const MAX_M_SIZE = (1 << (SIZE_BYTES * 8)) - 1 @@ -10,7 +9,6 @@ export default class BatchWriter { private nextIndex = 0 private beaconSize = 2 * 1e5 // Default 200kB private encoder = new MessageEncoder(this.beaconSize) - private strDict = new StringDictionary() private readonly sizeBuffer = new Uint8Array(SIZE_BYTES) private isEmpty = true @@ -91,14 +89,6 @@ export default class BatchWriter { this.beaconSizeLimit = limit } - private applyDict(str: string): number { - const [key, isNew] = this.strDict.getKey(str) - if (isNew) { - this.writeMessage([Messages.Type.StringDict, key, str]) - } - return key - } - writeMessage(message: Message) { if (message[0] === Messages.Type.Timestamp) { this.timestamp = message[1] // .timestamp @@ -106,14 +96,6 @@ export default class BatchWriter { if (message[0] === Messages.Type.SetPageLocation) { this.url = message[1] // .url } - if (message[0] === Messages.Type.SetNodeAttribute) { - message = [ - Messages.Type.SetNodeAttributeDict, - message[1], - this.applyDict(message[2]), - this.applyDict(message[3]), - ] as Messages.SetNodeAttributeDict - } if (this.writeWithSize(message)) { return } diff --git a/tracker/tracker/src/webworker/BatchWriter.unit.test.ts b/tracker/tracker/src/webworker/BatchWriter.unit.test.ts index 14dfdfd83..cd0d8330e 100644 --- a/tracker/tracker/src/webworker/BatchWriter.unit.test.ts +++ b/tracker/tracker/src/webworker/BatchWriter.unit.test.ts @@ -25,7 +25,6 @@ describe('BatchWriter', () => { expect(batchWriter['nextIndex']).toBe(1) expect(batchWriter['beaconSize']).toBe(200000) expect(batchWriter['encoder']).toBeDefined() - expect(batchWriter['strDict']).toBeDefined() expect(batchWriter['sizeBuffer']).toHaveLength(3) expect(batchWriter['isEmpty']).toBe(true) }) @@ -66,15 +65,6 @@ describe('BatchWriter', () => { expect(batchWriter['beaconSizeLimit']).toBe(500000) }) - test('Set note attribute tries to use dictionary', () => { - const spyOnStrGetKey = jest.spyOn(batchWriter['strDict'], 'getKey') - // @ts-ignore - batchWriter['writeMessage']([Messages.Type.SetNodeAttribute, 1, 'name', 'value']) - expect(spyOnStrGetKey).toHaveBeenCalledTimes(2) - expect(spyOnStrGetKey).toHaveBeenCalledWith('name') - expect(spyOnStrGetKey).toHaveBeenCalledWith('value') - }) - test('writeMessage writes the given message', () => { // @ts-ignore const message = [Messages.Type.Timestamp, 987654321] diff --git a/tracker/tracker/src/webworker/StringDictionary.ts b/tracker/tracker/src/webworker/StringDictionary.ts deleted file mode 100644 index b183ce862..000000000 --- a/tracker/tracker/src/webworker/StringDictionary.ts +++ /dev/null @@ -1,13 +0,0 @@ -export default class StringDictionary { - private idx = 1 - private backDict: Record = {} - - getKey(str: string): [number, boolean] { - let isNew = false - if (!this.backDict[str]) { - isNew = true - this.backDict[str] = this.idx++ - } - return [this.backDict[str], isNew] - } -}