diff --git a/frontend/app/player/web/managers/DOM/DOMManager.ts b/frontend/app/player/web/managers/DOM/DOMManager.ts index 198c096ad..559f45e27 100644 --- a/frontend/app/player/web/managers/DOM/DOMManager.ts +++ b/frontend/app/player/web/managers/DOM/DOMManager.ts @@ -43,6 +43,7 @@ export default class DOMManager extends ListWalker { private activeIframeRoots: Map = new Map() private styleSheets: Map = new Map() private ppStyleSheets: Map = new Map() + private stringDict: Record = {} private upperBodyId: number = -1; @@ -134,6 +135,31 @@ export default class DOMManager extends ListWalker { parent.insertChildAt(child, index) } + private setNodeAttribute(msg: { id: number, name: string, value: string }) { + let { name, value } = msg; + const vn = this.vElements.get(msg.id) + if (!vn) { logger.error("Node not found", msg); return } + if (vn.node.tagName === "INPUT" && name === "name") { + // Otherwise binds local autocomplete values (maybe should ignore on the tracker level) + return + } + if (name === "href" && vn.node.tagName === "LINK") { + // @ts-ignore ?global ENV type // It've been done on backend (remove after testing in saas) + // if (value.startsWith(window.env.ASSETS_HOST || window.location.origin + '/assets')) { + // value = value.replace("?", "%3F"); + // } + if (!value.startsWith("http")) { return } + // blob:... value happened here. https://foss.openreplay.com/3/session/7013553567419137 + // that resulted in that link being unable to load and having 4sec timeout in the below function. + this.stylesManager.setStyleHandlers(vn.node as HTMLLinkElement, value); + } + if (vn.node.namespaceURI === 'http://www.w3.org/2000/svg' && value.startsWith("url(")) { + value = "url(#" + (value.split("#")[1] ||")") + } + vn.setAttribute(name, value) + this.removeBodyScroll(msg.id, vn) + } + private applyMessage = (msg: Message): Promise | undefined => { let node: Node | undefined let vn: VNode | undefined @@ -160,10 +186,11 @@ export default class DOMManager extends ListWalker { this.vRoots.clear() this.vRoots.set(0, vDoc) // watchout: id==0 for both Document and documentElement // this is done for the AdoptedCSS logic - // todo: start from 0 (sync logic with tracker) + // todo: start from 0-node (sync logic with tracker) this.vTexts.clear() this.stylesManager.reset() this.activeIframeRoots.clear() + this.stringDict = {} return case MType.CreateTextNode: vn = new VText() @@ -200,28 +227,20 @@ export default class DOMManager extends ListWalker { vn.parentNode.removeChild(vn) return case MType.SetNodeAttribute: - let { name, value } = msg; - vn = this.vElements.get(msg.id) - if (!vn) { logger.error("Node not found", msg); return } - if (vn.node.tagName === "INPUT" && name === "name") { - // Otherwise binds local autocomplete values (maybe should ignore on the tracker level) - return - } - if (name === "href" && vn.node.tagName === "LINK") { - // @ts-ignore ?global ENV type // It've been done on backend (remove after testing in saas) - // if (value.startsWith(window.env.ASSETS_HOST || window.location.origin + '/assets')) { - // value = value.replace("?", "%3F"); - // } - if (!value.startsWith("http")) { return } - // blob:... value happened here. https://foss.openreplay.com/3/session/7013553567419137 - // that resulted in that link being unable to load and having 4sec timeout in the below function. - this.stylesManager.setStyleHandlers(vn.node as HTMLLinkElement, value); - } - if (vn.node.namespaceURI === 'http://www.w3.org/2000/svg' && value.startsWith("url(")) { - value = "url(#" + (value.split("#")[1] ||")") - } - vn.setAttribute(name, value) - this.removeBodyScroll(msg.id, vn) + this.setNodeAttribute(msg) + return + case MType.StringDict: + this.stringDict[msg.key] = msg.value + return + case MType.SetNodeAttributeDict: + this.stringDict[msg.name] === undefined && logger.error("No dictionary key for msg 'name': ", msg) + this.stringDict[msg.value] === undefined && logger.error("No dictionary key for msg 'value': ", msg) + if (!this.stringDict[msg.name] || !this.stringDict[msg.value]) { return } + this.setNodeAttribute({ + id: msg.id, + name: this.stringDict[msg.name], + value: this.stringDict[msg.value], + }) return case MType.RemoveNodeAttribute: vn = this.vElements.get(msg.id) diff --git a/tracker/tracker/src/webworker/BatchWriter.ts b/tracker/tracker/src/webworker/BatchWriter.ts index 167c1aff6..b08b71416 100644 --- a/tracker/tracker/src/webworker/BatchWriter.ts +++ b/tracker/tracker/src/webworker/BatchWriter.ts @@ -1,6 +1,7 @@ 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 @@ -9,6 +10,7 @@ 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 @@ -84,6 +86,14 @@ export default class BatchWriter { this.beaconSizeLimit = limit } + private applyDict(str: string): string { + 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 @@ -91,6 +101,14 @@ 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/StringDictionary.ts b/tracker/tracker/src/webworker/StringDictionary.ts new file mode 100644 index 000000000..52632c9f0 --- /dev/null +++ b/tracker/tracker/src/webworker/StringDictionary.ts @@ -0,0 +1,13 @@ +export default class StringDictionary { + private idx = 0 + private backDict: Record = {} + + getKey(str: string): [string, boolean] { + let isNew = false + if (!this.backDict[str]) { + isNew = true + this.backDict[str] = `${this.idx++}` + } + return [this.backDict[str], isNew] + } +}