From f8ba3f6d8963191f5320ea8f4b8199c5ab5d5f5b Mon Sep 17 00:00:00 2001 From: Andrey Babushkin <55714097+reyand43@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:59:25 +0200 Subject: [PATCH] Css batching (#3326) * tracker: initial css inlining functionality * tracker: add tests, adjust sheet id, stagger rule sending * ui: rereoute custom html component fragments * removed sorting --------- Co-authored-by: nick-delirium --- .../app/player/web/managers/DOM/DOMManager.ts | 6 +- tracker/tracker/src/main/app/index.ts | 37 +- .../src/main/app/observer/cssInliner.ts | 339 ++++-------------- .../tracker/src/main/app/observer/observer.ts | 35 +- .../src/main/app/observer/top_observer.ts | 6 +- tracker/tracker/src/tests/cssInliner.test.ts | 20 +- 6 files changed, 103 insertions(+), 340 deletions(-) diff --git a/frontend/app/player/web/managers/DOM/DOMManager.ts b/frontend/app/player/web/managers/DOM/DOMManager.ts index 9e4f06f8d..76e194916 100644 --- a/frontend/app/player/web/managers/DOM/DOMManager.ts +++ b/frontend/app/player/web/managers/DOM/DOMManager.ts @@ -1,5 +1,4 @@ import logger from 'App/logger'; -import { resolveCSS } from '../../messages/rewriter/urlResolve'; import type Screen from '../../Screen/Screen'; import type { Message, SetNodeScroll } from '../../messages'; @@ -450,8 +449,9 @@ export default class DOMManager extends ListWalker { logger.error('CreateIFrameDocument: Node not found', msg); return; } - // shadow DOM for a custom element + SALESFORCE () - const isCustomElement = vElem.tagName.includes('-') || vElem.tagName === 'SLOT'; + + // shadow DOM for a custom element + const isCustomElement = vElem.tagName.includes('-'); const isNotActualIframe = !["IFRAME", "FRAME"].includes(vElem.tagName.toUpperCase()); const isLikelyShadowRoot = isCustomElement && isNotActualIframe; diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index 04aa62f74..89de2be40 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -272,6 +272,7 @@ export default class App { 'feature-flags': true, 'usability-test': true, } + private emptyBatchCounter = 0 constructor( projectKey: string, @@ -318,14 +319,16 @@ export default class App { __save_canvas_locally: false, localStorage: null, sessionStorage: null, + disableStringDict: true, forceSingleTab: false, assistSocketHost: '', fixedCanvasScaling: false, disableCanvas: false, captureIFrames: true, - obscureTextEmails: false, + disableSprites: false, + inlineRemoteCss: true, + obscureTextEmails: true, obscureTextNumbers: false, - disableStringDict: false, crossdomain: { parentDomain: '*', }, @@ -336,12 +339,6 @@ export default class App { useAnimationFrame: false, }, forceNgOff: false, - inlineRemoteCss: false, - disableSprites: false, - inlinerOptions: { - forceFetch: false, - forcePlain: false, - } } this.options = simpleMerge(defaultOptions, options) @@ -436,6 +433,7 @@ export default class App { if (ev.data.context === this.contextId) { return } + this.debug.log(ev) if (ev.data.line === proto.resp) { const sessionToken = ev.data.token this.session.setSessionToken(sessionToken) @@ -853,8 +851,7 @@ export default class App { * */ private _nCommit(): void { if (this.socketMode) { - this.messages.unshift(TabData(this.session.getTabId())) - this.messages.unshift(Timestamp(this.timestamp())) + this.messages.unshift(Timestamp(this.timestamp()), TabData(this.session.getTabId())) this.commitCallbacks.forEach((cb) => cb(this.messages)) this.messages.length = 0 return @@ -877,10 +874,19 @@ export default class App { return } + if (!this.messages.length) { + // Release empty batches every 30 secs (1000 * 30ms) + if (this.emptyBatchCounter < 1000) { + this.emptyBatchCounter++; + return; + } + } + + this.emptyBatchCounter = 0 + try { requestIdleCb(() => { - this.messages.unshift(TabData(this.session.getTabId())) - this.messages.unshift(Timestamp(this.timestamp())) + this.messages.unshift(Timestamp(this.timestamp()), TabData(this.session.getTabId())) this.worker?.postMessage(this.messages) this.commitCallbacks.forEach((cb) => cb(this.messages)) this.messages.length = 0 @@ -905,10 +911,9 @@ export default class App { private _cStartCommit(): void { this.coldStartCommitN += 1 if (this.coldStartCommitN === 2) { - this.bufferedMessages1.push(Timestamp(this.timestamp())) - this.bufferedMessages1.push(TabData(this.session.getTabId())) - this.bufferedMessages2.push(Timestamp(this.timestamp())) - this.bufferedMessages2.push(TabData(this.session.getTabId())) + const payload = [Timestamp(this.timestamp()), TabData(this.session.getTabId())] + this.bufferedMessages1.push(...payload) + this.bufferedMessages2.push(...payload) this.coldStartCommitN = 0 } } diff --git a/tracker/tracker/src/main/app/observer/cssInliner.ts b/tracker/tracker/src/main/app/observer/cssInliner.ts index b8c5fb57d..df0133dba 100644 --- a/tracker/tracker/src/main/app/observer/cssInliner.ts +++ b/tracker/tracker/src/main/app/observer/cssInliner.ts @@ -1,5 +1,3 @@ -let fakeIdHolder = 1000000 * 99; - export function inlineRemoteCss( node: HTMLLinkElement, id: number, @@ -7,286 +5,83 @@ export function inlineRemoteCss( getNextID: () => number, insertRule: (id: number, cssText: string, index: number, baseHref: string) => any[], addOwner: (sheetId: number, ownerId: number) => any[], - forceFetch?: boolean, - sendPlain?: boolean, - onPlain?: (cssText: string, id: number) => void, ) { - const sheetId = getNextID(); + const sheet = node.sheet; + const sheetId = getNextID() addOwner(sheetId, id); - const sheet = node.sheet; - - if (sheet && !forceFetch) { - try { - const cssText = stringifyStylesheet(sheet); - - if (cssText) { - processCssText(cssText); - return; - } - } catch (e) { - console.warn("Could not stringify sheet, falling back to fetch:", e); + const processRules = (rules: CSSRuleList) => { + if (rules.length) { + setTimeout(() => { + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + insertRule(sheetId, rule.cssText, i, baseHref); + } + }, 0) } - } + }; - // Fall back to fetching if we couldn't get or stringify the sheet - if (node.href) { + const processCssText = (cssText: string) => { + cssText = cssText.replace(/\/\*[\s\S]*?\*\//g, ''); + + const ruleTexts: string[] = []; + let depth = 0; + let currentRule = ''; + + for (let i = 0; i < cssText.length; i++) { + const char = cssText[i]; + + if (char === '{') { + depth++; + } else if (char === '}') { + depth--; + if (depth === 0) { + currentRule += char; + ruleTexts.push(currentRule.trim()); + currentRule = ''; + continue; + } + } + + currentRule += char; + } + + for (let i = 0; i < ruleTexts.length; i++) { + const ruleText = ruleTexts[i]; + insertRule(sheetId, ruleText, i, baseHref); + } + }; + + if (sheet) { + try { + const rules = sheet.cssRules; + processRules(rules); + } catch (e) { + const href = node.href; + if (href) { + fetch(href) + .then(response => { + if (!response.ok) { + throw new Error(`Failed to fetch CSS: ${response.status}`); + } + return response.text(); + }) + .then(cssText => { + processCssText(cssText); + }) + .catch(error => { + console.error(`Failed to fetch or process CSS from ${href}:`, error); + }); + } + } + } else if (node.href) { fetch(node.href) - .then(response => { - if (!response.ok) { - throw new Error(`Failed to fetch CSS: ${response.status}`); - } - return response.text(); - }) + .then(response => response.text()) .then(cssText => { - if (sendPlain && onPlain) { - onPlain(cssText, fakeIdHolder++); - } processCssText(cssText); }) .catch(error => { console.error(`Failed to fetch CSS from ${node.href}:`, error); }); } - - function processCssText(cssText: string) { - // Remove comments - cssText = cssText.replace(/\/\*[\s\S]*?\*\//g, ''); - - // Parse and process the CSS text to extract rules - const ruleTexts = parseCSS(cssText); - - for (let i = 0; i < ruleTexts.length; i++) { - insertRule(sheetId, ruleTexts[i], i, baseHref); - } - } - - - function parseCSS(cssText: string): string[] { - const rules: string[] = []; - let inComment = false; - let inString = false; - let stringChar = ''; - let braceLevel = 0; - let currentRule = ''; - - for (let i = 0; i < cssText.length; i++) { - const char = cssText[i]; - const nextChar = cssText[i + 1] || ''; - - // comments - if (!inString && char === '/' && nextChar === '*') { - inComment = true; - i++; // Skip the next character - continue; - } - - if (inComment) { - if (char === '*' && nextChar === '/') { - inComment = false; - i++; // Skip the next character - } - continue; - } - - - if (!inString && (char === '"' || char === "'")) { - inString = true; - stringChar = char; - currentRule += char; - continue; - } - - if (inString) { - currentRule += char; - if (char === stringChar && cssText[i - 1] !== '\\') { - inString = false; - } - continue; - } - - - currentRule += char; - - if (char === '{') { - braceLevel++; - } else if (char === '}') { - braceLevel--; - - if (braceLevel === 0) { - // End of a top-level rule - rules.push(currentRule.trim()); - currentRule = ''; - } - } - } - - // Handle any remaining text (should be rare) - if (currentRule.trim()) { - rules.push(currentRule.trim()); - } - - return rules; - } - - - function stringifyStylesheet(s: CSSStyleSheet): string | null { - try { - const rules = s.rules || s.cssRules; - if (!rules) { - return null; - } - - let sheetHref = s.href; - if (!sheetHref && s.ownerNode && (s.ownerNode as HTMLElement).ownerDocument) { - // an inline