Skip to content

Commit d1385b4

Browse files
authored
fix: replace blocked videos with labelled placeholders (#444)
1 parent 7a4aed4 commit d1385b4

3 files changed

Lines changed: 99 additions & 3 deletions

File tree

‎csr/csr-recorder/src/engine/rrweb-engine.test.ts‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,56 @@ describe('RrwebEngine', () => {
5555

5656
it('applies default blockSelector when blockSelectors is absent', () => {
5757
new RrwebEngine().start({}, () => {});
58-
expect(recordSpy.mock.calls[0][0].blockSelector).toBe('[data-csr-block]');
58+
expect(recordSpy.mock.calls[0][0].blockSelector).toBe('[data-csr-block],video');
59+
});
60+
61+
it('rebuilds blocked elements as labelled inert placeholders', () => {
62+
new RrwebEngine().start({}, () => {});
63+
const plugin = recordSpy.mock.calls[0][0].plugins.find(
64+
({ name }: { name: string }) => name === 'csr/blocked-element-labels@1',
65+
);
66+
const event = {
67+
type: 2,
68+
timestamp: 1,
69+
data: {
70+
node: {
71+
type: 0,
72+
id: 1,
73+
childNodes: [
74+
{
75+
type: 2,
76+
id: 2,
77+
tagName: 'video',
78+
attributes: { rr_width: '640px', rr_height: '360px' },
79+
childNodes: [],
80+
},
81+
],
82+
},
83+
},
84+
};
85+
86+
expect(plugin.eventProcessor(event)).toEqual({
87+
...event,
88+
data: {
89+
node: {
90+
type: 0,
91+
id: 1,
92+
childNodes: [
93+
{
94+
type: 2,
95+
id: 2,
96+
tagName: 'div',
97+
attributes: {
98+
rr_width: '640px',
99+
rr_height: '360px',
100+
'data-csr-blocked-element': 'video',
101+
},
102+
childNodes: [],
103+
},
104+
],
105+
},
106+
},
107+
});
59108
});
60109

61110
it('throttles mousemove to 100ms and records only last input value', () => {

‎csr/csr-recorder/src/engine/rrweb-engine.ts‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,53 @@ type RrwebPlugin = NonNullable<recordOptions<RecordingEvent>['plugins']>[number]
1010

1111
type ClickModifiers = Pick<MouseEvent, 'button' | 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey'>;
1212

13+
const BLOCKED_ELEMENT_ATTRIBUTE = 'data-csr-blocked-element';
14+
15+
type SerializedNode = {
16+
type: number;
17+
tagName?: string;
18+
attributes?: Record<string, unknown>;
19+
childNodes?: SerializedNode[];
20+
};
21+
22+
function labelBlockedElement(node: SerializedNode): void {
23+
const isBlockedElement =
24+
node.type === 2 &&
25+
node.tagName !== undefined &&
26+
node.attributes !== undefined &&
27+
typeof node.attributes.rr_width === 'string' &&
28+
typeof node.attributes.rr_height === 'string';
29+
30+
if (isBlockedElement) {
31+
node.attributes![BLOCKED_ELEMENT_ATTRIBUTE] = node.tagName;
32+
node.tagName = 'div';
33+
}
34+
35+
node.childNodes?.forEach(labelBlockedElement);
36+
}
37+
38+
/**
39+
* rrweb strips blocked elements down to their dimensions, but retains their
40+
* original tag name. Rebuild them as inert divs and keep the tag name as safe
41+
* metadata so players can render a useful placeholder label.
42+
*/
43+
function blockedElementLabelsPlugin(): RrwebPlugin {
44+
return {
45+
name: 'csr/blocked-element-labels@1',
46+
options: {},
47+
observer: () => () => {},
48+
eventProcessor: event => {
49+
if (event.type === EventType.FullSnapshot) {
50+
labelBlockedElement(event.data.node as SerializedNode);
51+
} else if (event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.Mutation) {
52+
event.data.adds.forEach(add => labelBlockedElement(add.node as SerializedNode));
53+
}
54+
55+
return event;
56+
},
57+
};
58+
}
59+
1360
type ClipboardAction = 'copy' | 'cut' | 'paste';
1461

1562
/**
@@ -102,7 +149,7 @@ export class RrwebEngine implements RecordingEngine {
102149
const maskSelectors = config.maskSelectors ?? DEFAULT_MASK_SELECTORS;
103150
const blockSelectors = config.blockSelectors ?? DEFAULT_BLOCK_SELECTORS;
104151

105-
const plugins: RrwebPlugin[] = [clickModifiersPlugin(), clipboardActionsPlugin()];
152+
const plugins: RrwebPlugin[] = [clickModifiersPlugin(), clipboardActionsPlugin(), blockedElementLabelsPlugin()];
106153
const { captureConsoleLogs } = config;
107154
if (captureConsoleLogs) {
108155
const levels = captureConsoleLogs === true ? ALL_CONSOLE_LEVELS : captureConsoleLogs.levels;

‎csr/csr-recorder/src/types.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface RecorderOptions {
66
}
77

88
export const DEFAULT_MASK_SELECTORS: string[] = ['[data-csr-mask]'];
9-
export const DEFAULT_BLOCK_SELECTORS: string[] = ['[data-csr-block]'];
9+
export const DEFAULT_BLOCK_SELECTORS: string[] = ['[data-csr-block]', 'video'];
1010

1111
export interface RecordingConfig {
1212
/**

0 commit comments

Comments
 (0)