From 4a9a082896597f935f8b1992faf73aaaa2345c26 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Fri, 2 May 2025 10:05:44 +0200 Subject: [PATCH] tracker: handle emotion-js style population --- tracker/tracker/package.json | 2 +- tracker/tracker/src/main/index.ts | 3 +- .../main/modules/constructedStyleSheets.ts | 6 +- tracker/tracker/src/main/modules/cssrules.ts | 162 +++++++++++++----- 4 files changed, 123 insertions(+), 50 deletions(-) diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index 2b36bcd67..a077c0c76 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": "16.2.1", + "version": "16.2.2-beta.19", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index 765f30d94..d1f345d55 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -61,6 +61,7 @@ export type Options = Partial< } // dev only __DISABLE_SECURE_MODE?: boolean + checkCssInterval?: number } const DOCS_SETUP = '/en/sdk' @@ -192,7 +193,7 @@ export default class API { Mouse(app, options.mouse) // inside iframe, we ignore viewport scroll Scroll(app, this.crossdomainMode) - CSSRules(app) + CSSRules(app, options) ConstructedStyleSheets(app) Console(app, options) Exception(app, options) diff --git a/tracker/tracker/src/main/modules/constructedStyleSheets.ts b/tracker/tracker/src/main/modules/constructedStyleSheets.ts index 2f4a6185a..21e949dee 100644 --- a/tracker/tracker/src/main/modules/constructedStyleSheets.ts +++ b/tracker/tracker/src/main/modules/constructedStyleSheets.ts @@ -81,9 +81,9 @@ export default function (app: App | null) { } adoptedStyleSheetsOwnings.set(nodeID, nowOwning) }, 20) // Mysterious bug: - /* On the page https://explore.fast.design/components/fast-accordion + /* On the page https://explore.fast.design/components/fast-accordion the only rule inside the only adoptedStyleSheet of the iframe-s document - gets changed during first milliseconds after the load. + gets changed during first milliseconds after the load. However, none of the documented methods (replace, insertRule) is triggered. The rule is not substituted (remains the same object), however the text gets changed. */ @@ -127,6 +127,7 @@ export default function (app: App | null) { return replace.call(this, text).then((sheet: CSSStyleSheet) => { const sheetID = styleSheetIDMap.get(this) if (sheetID) { + console.log('replace') app.send(AdoptedSSReplaceURLBased(sheetID, text, app.getBaseHref())) } return sheet @@ -136,6 +137,7 @@ export default function (app: App | null) { context.CSSStyleSheet.prototype.replaceSync = function (text: string) { const sheetID = styleSheetIDMap.get(this) if (sheetID) { + console.log('replaceSync') app.send(AdoptedSSReplaceURLBased(sheetID, text, app.getBaseHref())) } return replaceSync.call(this, text) diff --git a/tracker/tracker/src/main/modules/cssrules.ts b/tracker/tracker/src/main/modules/cssrules.ts index 8636c68c0..1cc45f282 100644 --- a/tracker/tracker/src/main/modules/cssrules.ts +++ b/tracker/tracker/src/main/modules/cssrules.ts @@ -1,6 +1,6 @@ import type App from '../app/index.js' import { - AdoptedSSInsertRuleURLBased, // TODO: rename to common StyleSheet names + AdoptedSSInsertRuleURLBased, AdoptedSSDeleteRule, AdoptedSSAddOwner, TechnicalInfo, @@ -8,104 +8,174 @@ import { import { hasTag } from '../app/guards.js' import { nextID, styleSheetIDMap } from './constructedStyleSheets.js' -export default function (app: App | null) { - if (app === null) { - return - } +export default function (app: App, opts: { checkCssInterval?: number }) { + if (app === null) return if (!window.CSSStyleSheet) { app.send(TechnicalInfo('no_stylesheet_prototype_in_window', '')) return } + // Track CSS rule snapshots by sheetID:index + const ruleSnapshots = new Map() + let checkInterval: number | null = null + const checkIntervalMs = opts.checkCssInterval || 200 // Check every 200ms + + // Check all rules for changes + function checkRuleChanges() { + for (let i = 0; i < document.styleSheets.length; i++) { + try { + const sheet = document.styleSheets[i] + const sheetID = styleSheetIDMap.get(sheet) + if (!sheetID) continue + + // Check each rule in the sheet + for (let j = 0; j < sheet.cssRules.length; j++) { + try { + const rule = sheet.cssRules[j] + const key = `${sheetID}:${j}` + const newText = rule.cssText + const oldText = ruleSnapshots.get(key) + + if (oldText !== newText) { + // Rule is new or changed + if (oldText !== undefined) { + // Rule changed - send update + app.send(AdoptedSSDeleteRule(sheetID, j)) + app.send(AdoptedSSInsertRuleURLBased(sheetID, newText, j, app.getBaseHref())) + } else { + // Rule added - send insert + app.send(AdoptedSSInsertRuleURLBased(sheetID, newText, j, app.getBaseHref())) + } + ruleSnapshots.set(key, newText) + } + } catch (e) { + /* Skip inaccessible rules */ + } + } + + // Check for deleted rules + const keysToCheck = Array.from(ruleSnapshots.keys()).filter((key) => + key.startsWith(`${sheetID}:`), + ) + + for (const key of keysToCheck) { + const index = parseInt(key.split(':')[1], 10) + if (index >= sheet.cssRules.length) { + ruleSnapshots.delete(key) + } + } + } catch (e) { + /* Skip inaccessible sheets */ + } + } + } + + // Standard API hooks const sendInsertDeleteRule = app.safe((sheet: CSSStyleSheet, index: number, rule?: string) => { const sheetID = styleSheetIDMap.get(sheet) - if (!sheetID) { - // OK-case. Sheet haven't been registered yet. Rules will be sent on registration. - return - } + if (!sheetID) return + if (typeof rule === 'string') { app.send(AdoptedSSInsertRuleURLBased(sheetID, rule, index, app.getBaseHref())) + ruleSnapshots.set(`${sheetID}:${index}`, rule) } else { app.send(AdoptedSSDeleteRule(sheetID, index)) + ruleSnapshots.delete(`${sheetID}:${index}`) } }) - // TODO: proper rule insertion/removal (how?) const sendReplaceGroupingRule = app.safe((rule: CSSGroupingRule) => { let topmostRule: CSSRule = rule - while (topmostRule.parentRule) { - topmostRule = topmostRule.parentRule - } + while (topmostRule.parentRule) topmostRule = topmostRule.parentRule + const sheet = topmostRule.parentStyleSheet - if (!sheet) { - app.debug.warn('No parent StyleSheet found for', topmostRule, rule) - return - } + if (!sheet) return + const sheetID = styleSheetIDMap.get(sheet) - if (!sheetID) { - app.debug.warn('No sheedID found for', sheet, styleSheetIDMap) - return - } + if (!sheetID) return + const cssText = topmostRule.cssText - const ruleList = sheet.cssRules - const idx = Array.from(ruleList).indexOf(topmostRule) + const idx = Array.from(sheet.cssRules).indexOf(topmostRule) + if (idx >= 0) { app.send(AdoptedSSInsertRuleURLBased(sheetID, cssText, idx, app.getBaseHref())) - app.send(AdoptedSSDeleteRule(sheetID, idx + 1)) // Remove previous clone - } else { - app.debug.warn('Rule index not found in', sheet, topmostRule) + app.send(AdoptedSSDeleteRule(sheetID, idx + 1)) + ruleSnapshots.set(`${sheetID}:${idx}`, cssText) } }) + // Patch prototype methods const patchContext = app.safe((context: typeof globalThis) => { + if ((context as any).__css_tracking_patched__) return + ;(context as any).__css_tracking_patched__ = true + const { insertRule, deleteRule } = context.CSSStyleSheet.prototype const { insertRule: groupInsertRule, deleteRule: groupDeleteRule } = context.CSSGroupingRule.prototype - context.CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0): number { - sendInsertDeleteRule(this, index, rule) - return insertRule.call(this, rule, index) + context.CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) { + const result = insertRule.call(this, rule, index) + sendInsertDeleteRule(this, result, rule) + return result } - context.CSSStyleSheet.prototype.deleteRule = function (index: number): void { + + context.CSSStyleSheet.prototype.deleteRule = function (index: number) { sendInsertDeleteRule(this, index) return deleteRule.call(this, index) } - context.CSSGroupingRule.prototype.insertRule = function (rule: string, index = 0): number { - const result = groupInsertRule.call(this, rule, index) as number + context.CSSGroupingRule.prototype.insertRule = function (rule: string, index = 0) { + const result = groupInsertRule.call(this, rule, index) sendReplaceGroupingRule(this) return result } - context.CSSGroupingRule.prototype.deleteRule = function (index = 0): void { - const result = groupDeleteRule.call(this, index) as void + + context.CSSGroupingRule.prototype.deleteRule = function (index: number) { + const result = groupDeleteRule.call(this, index) sendReplaceGroupingRule(this) return result } }) + // Apply patches patchContext(window) app.observer.attachContextCallback(patchContext) + // Track style nodes app.nodes.attachNodeCallback((node: Node): void => { - if (!hasTag(node, 'style') || !node.sheet) { - return - } - if (node.textContent !== null && node.textContent.trim().length > 0) { - return // Non-virtual styles captured by the observer as a text - } + if (!hasTag(node, 'style') || !node.sheet) return + if (node.textContent !== null && node.textContent.trim().length > 0) return const nodeID = app.nodes.getID(node) - if (!nodeID) { - return - } + if (!nodeID) return + const sheet = node.sheet const sheetID = nextID() styleSheetIDMap.set(sheet, sheetID) app.send(AdoptedSSAddOwner(sheetID, nodeID)) - const rules = sheet.cssRules - for (let i = 0; i < rules.length; i++) { - sendInsertDeleteRule(sheet, i, rules[i].cssText) + for (let i = 0; i < sheet.cssRules.length; i++) { + try { + sendInsertDeleteRule(sheet, i, sheet.cssRules[i].cssText) + } catch (e) { + // Skip inaccessible rules + } } }) + + // Start checking and setup cleanup + function startChecking() { + if (checkInterval) return + checkInterval = window.setInterval(checkRuleChanges, checkIntervalMs) + } + + setTimeout(startChecking, 50) + + app.attachStopCallback(() => { + if (checkInterval) { + clearInterval(checkInterval) + checkInterval = null + } + ruleSnapshots.clear() + }) }