tracker css batching/inlining (#3334)
* tracker: initial css inlining functionality * tracker: add tests, adjust sheet id, stagger rule sending * removed sorting * upgrade css inliner * ui: better logging for ocunter * tracker: force-fetch mode for cssInliner * tracker: fix ts warns * tracker: use debug opts * tracker: 16.2.0 changelogs, inliner opts * tracker: remove debug options --------- Co-authored-by: Андрей Бабушкин <andreybabushkin2000@gmail.com>
This commit is contained in:
parent
0360e3726e
commit
85e30b3692
9 changed files with 103 additions and 35 deletions
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ export class VElement extends VParent<Element> {
|
|||
|
||||
applyChanges() {
|
||||
this.prioritized && this.applyPrioritizedChanges();
|
||||
this.node.data = this.data;
|
||||
this.applyAttributeChanges();
|
||||
super.applyChanges();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue