Skip to content

Commit ec7d7e6

Browse files
authored
Merge pull request #28 from pendo-io/jg-merge-master-to-pendo-main
Merge master in to pendo-main
2 parents 0de8fe6 + acb3d53 commit ec7d7e6

14 files changed

+157
-82
lines changed

.changeset/chilled-penguins-sin.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"rrdom": patch
3+
---
4+
5+
Ignore invalid DOM attributes when diffing

.changeset/metal-mugs-mate.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.changeset/perfect-dolls-grab.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"rrweb-snapshot": patch
3+
---
4+
5+
fix dimensions for blocked element not being applied
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"rrweb-snapshot": patch
3+
"rrweb": patch
4+
---
5+
6+
Slight simplification to how we replace :hover after #1458

.changeset/swift-pots-search.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"rrweb": minor
3+
---
4+
5+
Optimize isParentRemoved check

docs/recipes/optimize-storage.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ rrweb.record({
4949
rrweb.record({
5050
emit(event) {},
5151
sampling: {
52-
// Configure which kins of mouse interaction should be recorded
52+
// Configure which kinds of mouse interaction should be recorded
5353
mouseInteraction: {
5454
MouseUp: false,
5555
MouseDown: false,
@@ -78,7 +78,7 @@ import { pack } from '@rrweb/packer';
7878

7979
rrweb.record({
8080
emit(event) {},
81-
packFn: rrweb.pack,
81+
packFn: pack,
8282
});
8383
```
8484

@@ -88,7 +88,7 @@ And you need to pass packer.unpack as the `unpackFn` in replaying.
8888
import { unpack } from '@rrweb/packer';
8989

9090
const replayer = new rrweb.Replayer(events, {
91-
unpackFn: rrweb.unpack,
91+
unpackFn: unpack,
9292
});
9393
```
9494

packages/rrdom/src/diff.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,16 @@ function diffProps(
354354
}
355355
};
356356
} else if (newTree.tagName === 'IFRAME' && name === 'srcdoc') continue;
357-
else oldTree.setAttribute(name, newValue);
357+
else {
358+
try {
359+
oldTree.setAttribute(name, newValue);
360+
} catch (err) {
361+
// We want to continue diffing so we quietly catch
362+
// this exception. Otherwise, this can throw and bubble up to
363+
// the `ReplayerEvents.Flush` listener and break rendering
364+
console.warn(err);
365+
}
366+
}
358367
}
359368

360369
for (const { name } of Array.from(oldAttributes))

packages/rrdom/test/diff.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,32 @@ describe('diff algorithm for rrdom', () => {
336336
expect((node as Node as HTMLElement).className).toBe('node');
337337
});
338338

339+
it('ignores invalid attributes', () => {
340+
const tagName = 'DIV';
341+
const node = document.createElement(tagName);
342+
const sn = Object.assign({}, elementSn, {
343+
attributes: { '@click': 'foo' },
344+
tagName,
345+
});
346+
mirror.add(node, sn);
347+
348+
const rrDocument = new RRDocument();
349+
const rrNode = rrDocument.createElement(tagName);
350+
const sn2 = Object.assign({}, elementSn, {
351+
attributes: { '@click': 'foo' },
352+
tagName,
353+
});
354+
rrDocument.mirror.add(rrNode, sn2);
355+
356+
rrNode.attributes = { id: 'node1', class: 'node', '@click': 'foo' };
357+
diff(node, rrNode, replayer);
358+
expect((node as Node as HTMLElement).id).toBe('node1');
359+
expect((node as Node as HTMLElement).className).toBe('node');
360+
expect('@click' in (node as Node as HTMLElement)).toBe(false);
361+
expect(warn).toHaveBeenCalledTimes(1);
362+
warn.mockClear();
363+
});
364+
339365
it('can update exist properties', () => {
340366
const tagName = 'DIV';
341367
const node = document.createElement(tagName);

packages/rrdom/test/virtual-dom.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as puppeteer from 'puppeteer';
77
import { vi } from 'vitest';
88
import { JSDOM } from 'jsdom';
99
import {
10+
buildNodeWithSN,
1011
cdataNode,
1112
commentNode,
1213
documentNode,
@@ -207,6 +208,33 @@ describe('RRDocument for browser environment', () => {
207208
expect((rrNode as RRElement).tagName).toEqual('SHADOWROOT');
208209
expect(rrNode).toBe(parentRRNode.shadowRoot);
209210
});
211+
212+
it('can rebuild blocked element with correct dimensions', () => {
213+
// @ts-expect-error Testing buildNodeWithSN with rr elements
214+
const node = buildNodeWithSN(
215+
{
216+
id: 1,
217+
tagName: 'svg',
218+
type: NodeType.Element,
219+
isSVG: true,
220+
attributes: {
221+
rr_width: '50px',
222+
rr_height: '50px',
223+
},
224+
childNodes: [],
225+
},
226+
{
227+
// @ts-expect-error
228+
doc: new RRDocument(),
229+
mirror,
230+
blockSelector: '*',
231+
slimDOMOptions: {},
232+
},
233+
) as RRElement;
234+
235+
expect(node.style.width).toBe('50px');
236+
expect(node.style.height).toBe('50px');
237+
});
210238
});
211239

212240
describe('create a RRDocument from a html document', () => {

packages/rrweb-snapshot/src/css.ts

+3-52
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const mediaSelectorPlugin: AcceptedPlugin = {
1717
},
1818
};
1919

20-
// Adapted from https://github.com/giuseppeg/postcss-pseudo-classes/blob/master/index.js
20+
// Simplified from https://github.com/giuseppeg/postcss-pseudo-classes/blob/master/index.js
2121
const pseudoClassPlugin: AcceptedPlugin = {
2222
postcssPlugin: 'postcss-hover-classes',
2323
prepare: function () {
@@ -28,58 +28,9 @@ const pseudoClassPlugin: AcceptedPlugin = {
2828
return;
2929
}
3030
fixed.push(rule);
31-
3231
rule.selectors.forEach(function (selector) {
33-
if (!selector.includes(':')) {
34-
return;
35-
}
36-
37-
const selectorParts = selector.replace(/\n/g, ' ').split(' ');
38-
const pseudoedSelectorParts: string[] = [];
39-
40-
selectorParts.forEach(function (selectorPart) {
41-
const pseudos = selectorPart.match(/::?([^:]+)/g);
42-
43-
if (!pseudos) {
44-
pseudoedSelectorParts.push(selectorPart);
45-
return;
46-
}
47-
48-
const baseSelector = selectorPart.substr(
49-
0,
50-
selectorPart.length - pseudos.join('').length,
51-
);
52-
53-
const classPseudos = pseudos.map(function (pseudo) {
54-
const pseudoToCheck = pseudo.replace(/\(.*/g, '');
55-
if (pseudoToCheck !== ':hover') {
56-
return pseudo;
57-
}
58-
59-
// Ignore pseudo-elements!
60-
if (pseudo.match(/^::/)) {
61-
return pseudo;
62-
}
63-
64-
// Kill the colon
65-
pseudo = pseudo.substr(1);
66-
67-
// Replace left and right parens
68-
pseudo = pseudo.replace(/\(/g, '\\(');
69-
pseudo = pseudo.replace(/\)/g, '\\)');
70-
71-
return '.' + '\\:' + pseudo;
72-
});
73-
74-
pseudoedSelectorParts.push(baseSelector + classPseudos.join(''));
75-
});
76-
77-
addSelector(pseudoedSelectorParts.join(' '));
78-
79-
function addSelector(newSelector: string) {
80-
if (newSelector && newSelector !== selector) {
81-
rule.selector += ',\n' + newSelector;
82-
}
32+
if (selector.includes(':hover')) {
33+
rule.selector += ',\n' + selector.replace(/:hover/g, '.\\:hover');
8334
}
8435
});
8536
},

packages/rrweb-snapshot/src/rebuild.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,9 @@ function buildNode(
328328
}
329329

330330
if (name === 'rr_width') {
331-
(node as HTMLElement).style.width = value.toString();
331+
(node as HTMLElement).style.setProperty('width', value.toString());
332332
} else if (name === 'rr_height') {
333-
(node as HTMLElement).style.height = value.toString();
333+
(node as HTMLElement).style.setProperty('height', value.toString());
334334
} else if (
335335
name === 'rr_mediaCurrentTime' &&
336336
typeof value === 'number'

packages/rrweb-snapshot/test/css.test.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,18 @@ describe('css parser', () => {
5050
describe('pseudoClassPlugin', () => {
5151
it('parses nested commas in selectors correctly', () => {
5252
const cssText =
53-
'body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a) {background: red;}';
53+
'body > ul :is(li:not(:first-of-type) a.current, li:not(:first-of-type).active a) {background: red;}';
5454
expect(parse(pseudoClassPlugin, cssText)).toEqual(cssText);
5555
});
5656

57+
it("doesn't ignore :hover within :is brackets", () => {
58+
const cssText =
59+
'body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a) {background: red;}';
60+
expect(parse(pseudoClassPlugin, cssText))
61+
.toEqual(`body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a),
62+
body > ul :is(li:not(:first-of-type) a.\\:hover, li:not(:first-of-type).active a) {background: red;}`);
63+
});
64+
5765
it('should parse selector with comma nested inside ()', () => {
5866
const cssText =
5967
'[_nghost-ng-c4172599085]:not(.fit-content).aim-select:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active) { border-color: rgb(84, 84, 84); }';

packages/rrweb-snapshot/test/rebuild.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,32 @@ describe('rebuild', function () {
7272
});
7373
});
7474

75+
describe('rr_width/rr_height', function () {
76+
it('rebuild blocked element with correct dimensions', function () {
77+
const node = buildNodeWithSN(
78+
{
79+
id: 1,
80+
tagName: 'svg',
81+
type: NodeType.Element,
82+
isSVG: true,
83+
attributes: {
84+
rr_width: '50px',
85+
rr_height: '50px',
86+
},
87+
childNodes: [],
88+
},
89+
{
90+
doc: document,
91+
mirror,
92+
hackCss: false,
93+
cache,
94+
},
95+
) as HTMLDivElement;
96+
expect(node.style.width).toBe('50px');
97+
expect(node.style.height).toBe('50px');
98+
});
99+
});
100+
75101
describe('shadowDom', function () {
76102
it('rebuild shadowRoot without siblings', function () {
77103
const node = buildNodeWithSN(

packages/rrweb/src/record/mutation.ts

+27-23
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ export default class MutationBuffer {
176176
private attributes: attributeCursor[] = [];
177177
private attributeMap = new WeakMap<Node, attributeCursor>();
178178
private removes: removedNodeMutation[] = [];
179-
private removesMap = new Map<number, number>();
180179
private mapRemoves: Node[] = [];
181180

182181
private movedMap: Record<string, true> = {};
@@ -201,6 +200,7 @@ export default class MutationBuffer {
201200
private addedSet = new Set<Node>();
202201
private movedSet = new Set<Node>();
203202
private droppedSet = new Set<Node>();
203+
private removesSubTreeCache = new Set<Node>();
204204

205205
private mutationCb: observerParam['mutationCb'];
206206
private blockClass: observerParam['blockClass'];
@@ -399,7 +399,7 @@ export default class MutationBuffer {
399399

400400
for (const n of this.movedSet) {
401401
if (
402-
isParentRemoved(this.removesMap, n, this.mirror) &&
402+
isParentRemoved(this.removesSubTreeCache, n, this.mirror) &&
403403
!this.movedSet.has(dom.parentNode(n)!)
404404
) {
405405
continue;
@@ -410,7 +410,7 @@ export default class MutationBuffer {
410410
for (const n of this.addedSet) {
411411
if (
412412
!isAncestorInSet(this.droppedSet, n) &&
413-
!isParentRemoved(this.removesMap, n, this.mirror)
413+
!isParentRemoved(this.removesSubTreeCache, n, this.mirror)
414414
) {
415415
pushAdd(n);
416416
} else if (isAncestorInSet(this.movedSet, n)) {
@@ -547,10 +547,10 @@ export default class MutationBuffer {
547547
this.attributes = [];
548548
this.attributeMap = new WeakMap<Node, attributeCursor>();
549549
this.removes = [];
550-
this.removesMap = new Map<number, number>();
551550
this.addedSet = new Set<Node>();
552551
this.movedSet = new Set<Node>();
553552
this.droppedSet = new Set<Node>();
553+
this.removesSubTreeCache = new Set<Node>();
554554
this.movedMap = {};
555555

556556
this.mutationCb(payload);
@@ -785,7 +785,7 @@ export default class MutationBuffer {
785785
? true
786786
: undefined,
787787
});
788-
this.removesMap.set(nodeId, this.removes.length - 1);
788+
processRemoves(n, this.removesSubTreeCache);
789789
}
790790
this.mapRemoves.push(n);
791791
});
@@ -849,29 +849,33 @@ function deepDelete(addsSet: Set<Node>, n: Node) {
849849
dom.childNodes(n).forEach((childN) => deepDelete(addsSet, childN));
850850
}
851851

852-
function isParentRemoved(
853-
removesMap: Map<number, number>,
854-
n: Node,
855-
mirror: Mirror,
856-
): boolean {
857-
if (removesMap.size === 0) return false;
858-
return _isParentRemoved(removesMap, n, mirror);
852+
function processRemoves(n: Node, cache: Set<Node>) {
853+
const queue = [n];
854+
855+
while (queue.length) {
856+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
857+
const next = queue.pop()!;
858+
if (cache.has(next)) continue;
859+
cache.add(next);
860+
dom.childNodes(next).forEach((n) => queue.push(n));
861+
}
862+
863+
return;
864+
}
865+
866+
function isParentRemoved(removes: Set<Node>, n: Node, mirror: Mirror): boolean {
867+
if (removes.size === 0) return false;
868+
return _isParentRemoved(removes, n, mirror);
859869
}
860870

861871
function _isParentRemoved(
862-
removesMap: Map<number, number>,
872+
removes: Set<Node>,
863873
n: Node,
864-
mirror: Mirror,
874+
_mirror: Mirror,
865875
): boolean {
866-
let node: ParentNode | null = n.parentNode;
867-
while (node) {
868-
const parentId = mirror.getId(node);
869-
if (removesMap.has(parentId)) {
870-
return true;
871-
}
872-
node = node.parentNode;
873-
}
874-
return false;
876+
const node: ParentNode | null = dom.parentNode(n);
877+
if (!node) return false;
878+
return removes.has(node);
875879
}
876880

877881
function isAncestorInSet(set: Set<Node>, n: Node): boolean {

0 commit comments

Comments
 (0)