diff --git a/frontend/app/player/web/TabManager.ts b/frontend/app/player/web/TabManager.ts index 7ab04291f..574f7490e 100644 --- a/frontend/app/player/web/TabManager.ts +++ b/frontend/app/player/web/TabManager.ts @@ -348,19 +348,19 @@ export default class TabSessionManager { break; case MType.CreateTextNode: case MType.CreateElementNode: - this.windowNodeCounter.addNode(msg.id, msg.parentID); + this.windowNodeCounter.addNode(msg); this.performanceTrackManager.setCurrentNodesCount( this.windowNodeCounter.count, ); break; case MType.MoveNode: - this.windowNodeCounter.moveNode(msg.id, msg.parentID); + this.windowNodeCounter.moveNode(msg); this.performanceTrackManager.setCurrentNodesCount( this.windowNodeCounter.count, ); break; case MType.RemoveNode: - this.windowNodeCounter.removeNode(msg.id); + this.windowNodeCounter.removeNode(msg); this.performanceTrackManager.setCurrentNodesCount( this.windowNodeCounter.count, ); diff --git a/frontend/app/player/web/managers/DOM/VirtualDOM.ts b/frontend/app/player/web/managers/DOM/VirtualDOM.ts index 15ce85cf4..daeb7fe4e 100644 --- a/frontend/app/player/web/managers/DOM/VirtualDOM.ts +++ b/frontend/app/player/web/managers/DOM/VirtualDOM.ts @@ -257,6 +257,7 @@ export class VElement extends VParent { applyChanges() { this.prioritized && this.applyPrioritizedChanges(); + this.node.data = this.data; this.applyAttributeChanges(); super.applyChanges(); } diff --git a/frontend/app/player/web/managers/WindowNodeCounter.ts b/frontend/app/player/web/managers/WindowNodeCounter.ts index 17b5b4d58..97791a01a 100644 --- a/frontend/app/player/web/managers/WindowNodeCounter.ts +++ b/frontend/app/player/web/managers/WindowNodeCounter.ts @@ -54,40 +54,45 @@ export default class WindowNodeCounter { this.nodes = [this.root]; } - addNode(id: number, parentID: number) { + addNode(msg: { id: number, parentID: number, time: number }): boolean { + const { id, parentID } = msg; if (!this.nodes[parentID]) { // TODO: iframe case // console.error(`Wrong! Node with id ${ parentID } (parentId) not found.`); - return; + return false; } if (this.nodes[id]) { // console.error(`Wrong! Node with id ${ id } already exists.`); - return; + return false; } this.nodes[id] = this.nodes[parentID].newChild(); + return true; } - removeNode(id: number) { + removeNode({ id }: { id: number }) { if (!this.nodes[id]) { // Might be text node // console.error(`Wrong! Node with id ${ id } not found.`); - return; + return false; } this.nodes[id].removeNode(); + return true; } - moveNode(id: number, parentId: number) { + moveNode(msg: { id: number, parentID: number, time: number }) { + const { id, parentID, time } = msg; if (!this.nodes[id]) { - console.warn(`Node Counter: Node with id ${id} not found.`); - return; + console.warn(`Node Counter: Node with id ${id} (parent: ${parentID}) not found. time: ${time}`); + return false; } - if (!this.nodes[parentId]) { + if (!this.nodes[parentID]) { console.warn( - `Node Counter: Node with id ${parentId} (parentId) not found.`, + `Node Counter: Node with id ${parentID} (parentId) not found. time: ${time}`, ); - return; + return false; } - this.nodes[id].moveNode(this.nodes[parentId]); + this.nodes[id].moveNode(this.nodes[parentID]); + return true; } get count() { diff --git a/tracker/tracker/CHANGELOG.md b/tracker/tracker/CHANGELOG.md index ef8e97fd4..434eb35e6 100644 --- a/tracker/tracker/CHANGELOG.md +++ b/tracker/tracker/CHANGELOG.md @@ -1,3 +1,46 @@ +## 16.2.0 + +- css batching and inlining via (!plain mode will cause fake text nodes in style tags occupying 99*10^6 id space, can conflict with crossdomain iframes!) + +``` +inlineRemoteCss: boolean + inlinerOptions?: { + forceFetch?: boolean, + forcePlain?: boolean, + } +``` + +## 16.1.4 + +- bump proxy version to .3 + +## 16.1.3 + +- same as previous, more strict checks for body obj + +## 16.1.2 + +- bump networkProxy version (prevent reusage of body streams, fix for sanitizer network body checks) + +## 16.1.1 + +- fixing debug logs from 16.1.0 + +## 16.1.0 + +- new `privateMode` option to hide all possible data from tracking +- update `networkProxy` to 1.1.0 (auto sanitizer for sensitive parameters in network requests) +- reduced the frequency of performance tracker calls +- reduced the number of events when the user is idle + +## 16.0.3 + +- better handling for local svg spritemaps + +## 16.0.2 + +- fix attributeSender key generation to prevent calling native methods on objects + ## 16.0.1 - drop computing ts digits @@ -36,7 +79,7 @@ tracker.start() ## 15.0.5 - update medv/finder to 4.0.2 for better support of css-in-js libs -- fixes for single tab recording +- fixes for single tab recording - add option to disable network completely `{ network: { disabled: true } }` - fix for batching during offline recording syncs diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index 790eae73a..04aa62f74 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -318,16 +318,14 @@ export default class App { __save_canvas_locally: false, localStorage: null, sessionStorage: null, - disableStringDict: true, forceSingleTab: false, assistSocketHost: '', fixedCanvasScaling: false, disableCanvas: false, captureIFrames: true, - disableSprites: false, - inlineRemoteCss: true, - obscureTextEmails: true, + obscureTextEmails: false, obscureTextNumbers: false, + disableStringDict: false, crossdomain: { parentDomain: '*', }, @@ -338,6 +336,12 @@ export default class App { useAnimationFrame: false, }, forceNgOff: false, + inlineRemoteCss: false, + disableSprites: false, + inlinerOptions: { + forceFetch: false, + forcePlain: false, + } } this.options = simpleMerge(defaultOptions, options) diff --git a/tracker/tracker/src/main/app/observer/cssInliner.ts b/tracker/tracker/src/main/app/observer/cssInliner.ts index b0aeb034f..b8c5fb57d 100644 --- a/tracker/tracker/src/main/app/observer/cssInliner.ts +++ b/tracker/tracker/src/main/app/observer/cssInliner.ts @@ -11,10 +11,8 @@ export function inlineRemoteCss( sendPlain?: boolean, onPlain?: (cssText: string, id: number) => void, ) { - const sheetId = sendPlain ? null : getNextID(); - if (!sendPlain) { - addOwner(sheetId!, id); - } + const sheetId = getNextID(); + addOwner(sheetId, id); const sheet = node.sheet; @@ -27,7 +25,7 @@ export function inlineRemoteCss( return; } } catch (e) { - // console.warn("Could not stringify sheet, falling back to fetch:", e); + console.warn("Could not stringify sheet, falling back to fetch:", e); } } @@ -36,19 +34,18 @@ export function inlineRemoteCss( fetch(node.href) .then(response => { if (!response.ok) { - throw new Error(`response status ${response.status}`); + throw new Error(`Failed to fetch CSS: ${response.status}`); } return response.text(); }) .then(cssText => { if (sendPlain && onPlain) { onPlain(cssText, fakeIdHolder++); - } else { - processCssText(cssText); } + processCssText(cssText); }) .catch(error => { - console.error(`OpenReplay: Failed to fetch CSS from ${node.href}:`, error); + console.error(`Failed to fetch CSS from ${node.href}:`, error); }); } @@ -60,7 +57,7 @@ export function inlineRemoteCss( const ruleTexts = parseCSS(cssText); for (let i = 0; i < ruleTexts.length; i++) { - insertRule(sheetId!, ruleTexts[i], i, baseHref); + insertRule(sheetId, ruleTexts[i], i, baseHref); } } diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts index a0ef5723f..1a7da668a 100644 --- a/tracker/tracker/src/main/app/observer/observer.ts +++ b/tracker/tracker/src/main/app/observer/observer.ts @@ -394,7 +394,7 @@ export default abstract class Observer { (cssText: string, fakeTextId: number) => { this.app.send(CreateTextNode(fakeTextId, id, 0)) setTimeout(() => { - this.app.send(SetCSSDataURLBased(fakeTextId, cssText, this.app.getBaseHref())) + this.app.send(SetNodeData(fakeTextId, cssText)) }, 10) } ) diff --git a/tracker/tracker/src/main/app/observer/top_observer.ts b/tracker/tracker/src/main/app/observer/top_observer.ts index f7fdea6fc..6049288ca 100644 --- a/tracker/tracker/src/main/app/observer/top_observer.ts +++ b/tracker/tracker/src/main/app/observer/top_observer.ts @@ -20,6 +20,10 @@ export interface Options { * @default false * */ inlineRemoteCss: boolean + inlinerOptions?: { + forceFetch?: boolean, + forcePlain?: boolean, + } } type Context = Window & typeof globalThis @@ -95,7 +99,7 @@ export default class TopObserver extends Observer { this.app.debug.info('doc already observed for', id) return } - const observer = new IFrameObserver(this.app) + const observer = new IFrameObserver(this.app, false, {}) this.iframeObservers.set(iframe, observer) this.docObservers.set(currentDoc, observer) this.iframeObserversArr.push(observer) diff --git a/tracker/tracker/src/tests/cssInliner.test.ts b/tracker/tracker/src/tests/cssInliner.test.ts index 0e6936032..99b7f5924 100644 --- a/tracker/tracker/src/tests/cssInliner.test.ts +++ b/tracker/tracker/src/tests/cssInliner.test.ts @@ -53,7 +53,7 @@ describe('inlineRemoteCss', () => { jest.runAllTimers(); expect(mockNextID).toHaveBeenCalled(); expect(mockAdoptedSSAddOwner).toHaveBeenCalledWith(123, 456); - expect(mockAdoptedSSInsertRuleURLBased).toHaveBeenCalledWith(456, 'body { color: red; }', 0, 'http://example.com'); + expect(mockAdoptedSSInsertRuleURLBased).toHaveBeenCalledWith(123, 'body { color: red; }', 0, 'http://example.com'); jest.useRealTimers(); }); @@ -73,6 +73,12 @@ describe('inlineRemoteCss', () => { test('should handle successful fetch and process CSS text', async () => { mockNode.href = 'http://example.com/style.css'; const mockSheet = {}; + global.fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + text: () => Promise.resolve('body { color: red; }') + }) + ); Object.defineProperty(mockSheet, 'cssRules', { get: () => { throw new Error('CORS error'); } }); @@ -80,18 +86,26 @@ describe('inlineRemoteCss', () => { get: () => mockSheet }); inlineRemoteCss(mockNode, 456, 'http://example.com',mockNextID,mockAdoptedSSInsertRuleURLBased, mockAdoptedSSAddOwner); - await new Promise(process.nextTick); + await new Promise(resolve => setTimeout(resolve, 0)); expect(mockNextID).toHaveBeenCalled(); expect(mockAdoptedSSAddOwner).toHaveBeenCalledWith(123, 456); expect(mockAdoptedSSInsertRuleURLBased).toHaveBeenCalledWith(123, 'body { color: red; }', 0, 'http://example.com'); + jest.useRealTimers(); }); - test('should fetch CSS if node has no sheet but has href', () => { + test('should fetch CSS if node has no sheet but has href', async () => { Object.defineProperty(mockNode, 'sheet', { get: () => null }); + global.fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + text: () => Promise.resolve('body { color: red; }') + }) + ); mockNode.href = 'http://example.com/style.css'; inlineRemoteCss(mockNode, 456, 'http://example.com',mockNextID,mockAdoptedSSInsertRuleURLBased, mockAdoptedSSAddOwner); + await new Promise(resolve => setTimeout(resolve, 0)); expect(global.fetch).toHaveBeenCalledWith('http://example.com/style.css'); });