fix(tracker/ui): fix string dictionary handling, reset tab state on tab change (#1334)

* fix(tracker/ui): fix string dictionary handling, reset tab state on tab change

* fix(tracker/ui): fix activity update

* fix(tracker/ui): rm console log

* fix(tracker/ui): fix events timeline
This commit is contained in:
Delirium 2023-06-12 13:31:40 +02:00 committed by GitHub
parent b901400ab3
commit 4f2f2d6b2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 162 additions and 64 deletions

View file

@ -43,14 +43,15 @@ function Timeline(props: IProps) {
devtoolsLoading,
domLoading,
tabStates,
currentTab,
} = store.get()
const { issues } = props;
const notes = notesStore.sessionNotes
const progressRef = useRef<HTMLDivElement>(null)
const timelineRef = useRef<HTMLDivElement>(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;

View file

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

View file

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

View file

@ -13,7 +13,6 @@ import {
VDocument,
VElement,
VHTMLElement,
VNode,
VShadowRoot,
VText,
OnloadVRoot,

View file

@ -68,5 +68,4 @@ export default class PagesManager extends ListWalker<DOMManager> {
}
return Promise.resolve()
}
}

View file

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

View file

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

View file

@ -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<Options>) {
// 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()

View file

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

View file

@ -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<string, number> = {}
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()
}
}

View file

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

View file

@ -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', () => {

View file

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

View file

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

View file

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

View file

@ -1,13 +0,0 @@
export default class StringDictionary {
private idx = 1
private backDict: Record<string, number> = {}
getKey(str: string): [number, boolean] {
let isNew = false
if (!this.backDict[str]) {
isNew = true
this.backDict[str] = this.idx++
}
return [this.backDict[str], isNew]
}
}