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:
parent
b901400ab3
commit
4f2f2d6b2c
16 changed files with 162 additions and 64 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import {
|
|||
VDocument,
|
||||
VElement,
|
||||
VHTMLElement,
|
||||
VNode,
|
||||
VShadowRoot,
|
||||
VText,
|
||||
OnloadVRoot,
|
||||
|
|
|
|||
|
|
@ -68,5 +68,4 @@ export default class PagesManager extends ListWalker<DOMManager> {
|
|||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
44
tracker/tracker/src/main/modules/attributeSender.ts
Normal file
44
tracker/tracker/src/main/modules/attributeSender.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
93
tracker/tracker/src/tests/attributeSender.unit.test.ts
Normal file
93
tracker/tracker/src/tests/attributeSender.unit.test.ts
Normal 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]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue