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;
|
break;
|
||||||
case MType.CreateTextNode:
|
case MType.CreateTextNode:
|
||||||
case MType.CreateElementNode:
|
case MType.CreateElementNode:
|
||||||
this.windowNodeCounter.addNode(msg.id, msg.parentID);
|
this.windowNodeCounter.addNode(msg);
|
||||||
this.performanceTrackManager.setCurrentNodesCount(
|
this.performanceTrackManager.setCurrentNodesCount(
|
||||||
this.windowNodeCounter.count,
|
this.windowNodeCounter.count,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case MType.MoveNode:
|
case MType.MoveNode:
|
||||||
this.windowNodeCounter.moveNode(msg.id, msg.parentID);
|
this.windowNodeCounter.moveNode(msg);
|
||||||
this.performanceTrackManager.setCurrentNodesCount(
|
this.performanceTrackManager.setCurrentNodesCount(
|
||||||
this.windowNodeCounter.count,
|
this.windowNodeCounter.count,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case MType.RemoveNode:
|
case MType.RemoveNode:
|
||||||
this.windowNodeCounter.removeNode(msg.id);
|
this.windowNodeCounter.removeNode(msg);
|
||||||
this.performanceTrackManager.setCurrentNodesCount(
|
this.performanceTrackManager.setCurrentNodesCount(
|
||||||
this.windowNodeCounter.count,
|
this.windowNodeCounter.count,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,7 @@ export class VElement extends VParent<Element> {
|
||||||
|
|
||||||
applyChanges() {
|
applyChanges() {
|
||||||
this.prioritized && this.applyPrioritizedChanges();
|
this.prioritized && this.applyPrioritizedChanges();
|
||||||
|
this.node.data = this.data;
|
||||||
this.applyAttributeChanges();
|
this.applyAttributeChanges();
|
||||||
super.applyChanges();
|
super.applyChanges();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,40 +54,45 @@ export default class WindowNodeCounter {
|
||||||
this.nodes = [this.root];
|
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]) {
|
if (!this.nodes[parentID]) {
|
||||||
// TODO: iframe case
|
// TODO: iframe case
|
||||||
// console.error(`Wrong! Node with id ${ parentID } (parentId) not found.`);
|
// console.error(`Wrong! Node with id ${ parentID } (parentId) not found.`);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.nodes[id]) {
|
if (this.nodes[id]) {
|
||||||
// console.error(`Wrong! Node with id ${ id } already exists.`);
|
// console.error(`Wrong! Node with id ${ id } already exists.`);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
this.nodes[id] = this.nodes[parentID].newChild();
|
this.nodes[id] = this.nodes[parentID].newChild();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeNode(id: number) {
|
removeNode({ id }: { id: number }) {
|
||||||
if (!this.nodes[id]) {
|
if (!this.nodes[id]) {
|
||||||
// Might be text node
|
// Might be text node
|
||||||
// console.error(`Wrong! Node with id ${ id } not found.`);
|
// console.error(`Wrong! Node with id ${ id } not found.`);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
this.nodes[id].removeNode();
|
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]) {
|
if (!this.nodes[id]) {
|
||||||
console.warn(`Node Counter: Node with id ${id} not found.`);
|
console.warn(`Node Counter: Node with id ${id} (parent: ${parentID}) not found. time: ${time}`);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.nodes[parentId]) {
|
if (!this.nodes[parentID]) {
|
||||||
console.warn(
|
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() {
|
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
|
## 16.0.1
|
||||||
|
|
||||||
- drop computing ts digits
|
- drop computing ts digits
|
||||||
|
|
|
||||||
|
|
@ -318,16 +318,14 @@ export default class App {
|
||||||
__save_canvas_locally: false,
|
__save_canvas_locally: false,
|
||||||
localStorage: null,
|
localStorage: null,
|
||||||
sessionStorage: null,
|
sessionStorage: null,
|
||||||
disableStringDict: true,
|
|
||||||
forceSingleTab: false,
|
forceSingleTab: false,
|
||||||
assistSocketHost: '',
|
assistSocketHost: '',
|
||||||
fixedCanvasScaling: false,
|
fixedCanvasScaling: false,
|
||||||
disableCanvas: false,
|
disableCanvas: false,
|
||||||
captureIFrames: true,
|
captureIFrames: true,
|
||||||
disableSprites: false,
|
obscureTextEmails: false,
|
||||||
inlineRemoteCss: true,
|
|
||||||
obscureTextEmails: true,
|
|
||||||
obscureTextNumbers: false,
|
obscureTextNumbers: false,
|
||||||
|
disableStringDict: false,
|
||||||
crossdomain: {
|
crossdomain: {
|
||||||
parentDomain: '*',
|
parentDomain: '*',
|
||||||
},
|
},
|
||||||
|
|
@ -338,6 +336,12 @@ export default class App {
|
||||||
useAnimationFrame: false,
|
useAnimationFrame: false,
|
||||||
},
|
},
|
||||||
forceNgOff: false,
|
forceNgOff: false,
|
||||||
|
inlineRemoteCss: false,
|
||||||
|
disableSprites: false,
|
||||||
|
inlinerOptions: {
|
||||||
|
forceFetch: false,
|
||||||
|
forcePlain: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.options = simpleMerge(defaultOptions, options)
|
this.options = simpleMerge(defaultOptions, options)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,8 @@ export function inlineRemoteCss(
|
||||||
sendPlain?: boolean,
|
sendPlain?: boolean,
|
||||||
onPlain?: (cssText: string, id: number) => void,
|
onPlain?: (cssText: string, id: number) => void,
|
||||||
) {
|
) {
|
||||||
const sheetId = sendPlain ? null : getNextID();
|
const sheetId = getNextID();
|
||||||
if (!sendPlain) {
|
addOwner(sheetId, id);
|
||||||
addOwner(sheetId!, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sheet = node.sheet;
|
const sheet = node.sheet;
|
||||||
|
|
||||||
|
|
@ -27,7 +25,7 @@ export function inlineRemoteCss(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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)
|
fetch(node.href)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`response status ${response.status}`);
|
throw new Error(`Failed to fetch CSS: ${response.status}`);
|
||||||
}
|
}
|
||||||
return response.text();
|
return response.text();
|
||||||
})
|
})
|
||||||
.then(cssText => {
|
.then(cssText => {
|
||||||
if (sendPlain && onPlain) {
|
if (sendPlain && onPlain) {
|
||||||
onPlain(cssText, fakeIdHolder++);
|
onPlain(cssText, fakeIdHolder++);
|
||||||
} else {
|
|
||||||
processCssText(cssText);
|
|
||||||
}
|
}
|
||||||
|
processCssText(cssText);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.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);
|
const ruleTexts = parseCSS(cssText);
|
||||||
|
|
||||||
for (let i = 0; i < ruleTexts.length; i++) {
|
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) => {
|
(cssText: string, fakeTextId: number) => {
|
||||||
this.app.send(CreateTextNode(fakeTextId, id, 0))
|
this.app.send(CreateTextNode(fakeTextId, id, 0))
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.app.send(SetCSSDataURLBased(fakeTextId, cssText, this.app.getBaseHref()))
|
this.app.send(SetNodeData(fakeTextId, cssText))
|
||||||
}, 10)
|
}, 10)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ export interface Options {
|
||||||
* @default false
|
* @default false
|
||||||
* */
|
* */
|
||||||
inlineRemoteCss: boolean
|
inlineRemoteCss: boolean
|
||||||
|
inlinerOptions?: {
|
||||||
|
forceFetch?: boolean,
|
||||||
|
forcePlain?: boolean,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Context = Window & typeof globalThis
|
type Context = Window & typeof globalThis
|
||||||
|
|
@ -95,7 +99,7 @@ export default class TopObserver extends Observer {
|
||||||
this.app.debug.info('doc already observed for', id)
|
this.app.debug.info('doc already observed for', id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const observer = new IFrameObserver(this.app)
|
const observer = new IFrameObserver(this.app, false, {})
|
||||||
this.iframeObservers.set(iframe, observer)
|
this.iframeObservers.set(iframe, observer)
|
||||||
this.docObservers.set(currentDoc, observer)
|
this.docObservers.set(currentDoc, observer)
|
||||||
this.iframeObserversArr.push(observer)
|
this.iframeObserversArr.push(observer)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ describe('inlineRemoteCss', () => {
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
expect(mockNextID).toHaveBeenCalled();
|
expect(mockNextID).toHaveBeenCalled();
|
||||||
expect(mockAdoptedSSAddOwner).toHaveBeenCalledWith(123, 456);
|
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();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -73,6 +73,12 @@ describe('inlineRemoteCss', () => {
|
||||||
test('should handle successful fetch and process CSS text', async () => {
|
test('should handle successful fetch and process CSS text', async () => {
|
||||||
mockNode.href = 'http://example.com/style.css';
|
mockNode.href = 'http://example.com/style.css';
|
||||||
const mockSheet = {};
|
const mockSheet = {};
|
||||||
|
global.fetch.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
text: () => Promise.resolve('body { color: red; }')
|
||||||
|
})
|
||||||
|
);
|
||||||
Object.defineProperty(mockSheet, 'cssRules', {
|
Object.defineProperty(mockSheet, 'cssRules', {
|
||||||
get: () => { throw new Error('CORS error'); }
|
get: () => { throw new Error('CORS error'); }
|
||||||
});
|
});
|
||||||
|
|
@ -80,18 +86,26 @@ describe('inlineRemoteCss', () => {
|
||||||
get: () => mockSheet
|
get: () => mockSheet
|
||||||
});
|
});
|
||||||
inlineRemoteCss(mockNode, 456, 'http://example.com',mockNextID,mockAdoptedSSInsertRuleURLBased, mockAdoptedSSAddOwner);
|
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(mockNextID).toHaveBeenCalled();
|
||||||
expect(mockAdoptedSSAddOwner).toHaveBeenCalledWith(123, 456);
|
expect(mockAdoptedSSAddOwner).toHaveBeenCalledWith(123, 456);
|
||||||
expect(mockAdoptedSSInsertRuleURLBased).toHaveBeenCalledWith(123, 'body { color: red; }', 0, 'http://example.com');
|
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', {
|
Object.defineProperty(mockNode, 'sheet', {
|
||||||
get: () => null
|
get: () => null
|
||||||
});
|
});
|
||||||
|
global.fetch.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
text: () => Promise.resolve('body { color: red; }')
|
||||||
|
})
|
||||||
|
);
|
||||||
mockNode.href = 'http://example.com/style.css';
|
mockNode.href = 'http://example.com/style.css';
|
||||||
inlineRemoteCss(mockNode, 456, 'http://example.com',mockNextID,mockAdoptedSSInsertRuleURLBased, mockAdoptedSSAddOwner);
|
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');
|
expect(global.fetch).toHaveBeenCalledWith('http://example.com/style.css');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue