Skip to content

Commit

Permalink
feat(replay): Check blockSelector for blocking elements as well (#49)
Browse files Browse the repository at this point in the history
This fixes an issue with dynamically added elements where a parent is
blocked according to a selector defined in `blockSelector`, but its
children are not.
  • Loading branch information
billyvg authored Feb 13, 2023
1 parent 779d7a9 commit b19ed89
Show file tree
Hide file tree
Showing 20 changed files with 1,252 additions and 237 deletions.
1 change: 1 addition & 0 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ function serializeNode(
}
delete attributes.src; // prevent auto loading
}

return {
type: NodeType.Element,
tagName,
Expand Down
2 changes: 2 additions & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ function record<T = eventWithTime>(
mutationCb: wrappedCanvasMutationEmit,
win: window,
blockClass,
blockSelector,
unblockSelector,
mirror,
});

Expand Down
10 changes: 5 additions & 5 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ export default class MutationBuffer {
switch (m.type) {
case 'characterData': {
const value = m.target.textContent;
if (!isBlocked(m.target, this.blockClass) && value !== m.oldValue) {
if (!isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector) && value !== m.oldValue) {
this.texts.push({
value:
needMaskingText(
Expand Down Expand Up @@ -489,7 +489,7 @@ export default class MutationBuffer {
maskInputFn: this.maskInputFn,
});
}
if (isBlocked(m.target, this.blockClass) || value === m.oldValue) {
if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector) || value === m.oldValue) {
return;
}
let item: attributeCursor | undefined = this.attributes.find(
Expand Down Expand Up @@ -561,7 +561,7 @@ export default class MutationBuffer {
const parentId = isShadowRoot(m.target)
? this.mirror.getId((m.target.host as unknown) as INode)
: this.mirror.getId(m.target as INode);
if (isBlocked(m.target, this.blockClass) || isIgnored(n)) {
if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector) || isIgnored(n)) {
return;
}
// removed node has not been serialized yet, just remove it from the Set
Expand Down Expand Up @@ -606,7 +606,7 @@ export default class MutationBuffer {

private genAdds = (n: Node | INode, target?: Node | INode) => {
// parent was blocked, so we can ignore this node
if (target && isBlocked(target, this.blockClass)) {
if (target && isBlocked(target, this.blockClass, this.blockSelector, this.unblockSelector)) {
return;
}
if (isINode(n)) {
Expand All @@ -628,7 +628,7 @@ export default class MutationBuffer {

// if this node is blocked `serializeNode` will turn it into a placeholder element
// but we have to remove it's children otherwise they will be added as placeholders too
if (!isBlocked(n, this.blockClass))
if (!isBlocked(n, this.blockClass, this.blockSelector, this.unblockSelector))
n.childNodes.forEach((childN) => this.genAdds(childN));
};
}
Expand Down
18 changes: 13 additions & 5 deletions packages/rrweb/src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ function initMouseInteractionObserver({
doc,
mirror,
blockClass,
blockSelector,
unblockSelector,
sampling,
}: observerParam): listenerHandler {
if (sampling.mouseInteraction === false) {
Expand All @@ -222,7 +224,7 @@ function initMouseInteractionObserver({
const getHandler = (eventKey: keyof typeof MouseInteractions) => {
return (event: MouseEvent | TouchEvent) => {
const target = getEventTarget(event) as Node;
if (isBlocked(target as Node, blockClass)) {
if (isBlocked(target as Node, blockClass, blockSelector, unblockSelector)) {
return;
}
const e = isTouchEvent(event) ? event.changedTouches[0] : event;
Expand Down Expand Up @@ -261,14 +263,16 @@ export function initScrollObserver({
doc,
mirror,
blockClass,
blockSelector,
unblockSelector,
sampling,
}: Pick<
observerParam,
'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'sampling'
'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'blockSelector' | 'unblockSelector' | 'sampling'
>): listenerHandler {
const updatePosition = throttle<UIEvent>((evt) => {
const target = getEventTarget(evt);
if (!target || isBlocked(target as Node, blockClass)) {
if (!target || isBlocked(target as Node, blockClass, blockSelector, unblockSelector)) {
return;
}
const id = mirror.getId(target as INode);
Expand Down Expand Up @@ -326,6 +330,8 @@ function initInputObserver({
doc,
mirror,
blockClass,
blockSelector,
unblockSelector,
ignoreClass,
ignoreSelector,
maskInputSelector,
Expand All @@ -348,7 +354,7 @@ function initInputObserver({
!target ||
!(target as Element).tagName ||
INPUT_TAGS.indexOf((target as Element).tagName) < 0 ||
isBlocked(target as Node, blockClass)
isBlocked(target as Node, blockClass, blockSelector, unblockSelector)
) {
return;
}
Expand Down Expand Up @@ -724,14 +730,16 @@ function initStyleDeclarationObserver(
function initMediaInteractionObserver({
mediaInteractionCb,
blockClass,
blockSelector,
unblockSelector,
mirror,
sampling,
}: observerParam): listenerHandler {
const handler = (type: MediaInteractions) =>
throttle(
callbackWrapper((event: Event) => {
const target = getEventTarget(event);
if (!target || isBlocked(target as Node, blockClass)) {
if (!target || isBlocked(target as Node, blockClass, blockSelector, unblockSelector)) {
return;
}
const { currentTime, volume, muted } = target as HTMLMediaElement;
Expand Down
12 changes: 11 additions & 1 deletion packages/rrweb/src/record/observers/canvas/2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import {
IWindow,
listenerHandler,
Mirror,
blockSelector,
} from '../../../types';
import { hookSetter, isBlocked, patch } from '../../../utils';

export default function initCanvas2DMutationObserver(
cb: canvasManagerMutationCallback,
win: IWindow,
blockClass: blockClass,
unblockSelector: blockSelector,
blockSelector: blockSelector,
mirror: Mirror,
): listenerHandler {
const handlers: listenerHandler[] = [];
Expand All @@ -36,7 +39,14 @@ export default function initCanvas2DMutationObserver(
this: CanvasRenderingContext2D,
...args: Array<unknown>
) {
if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
if (
!isBlocked(
(this.canvas as unknown) as INode,
blockClass,
blockSelector,
unblockSelector,
)
) {
// Using setTimeout as getImageData + JSON.stringify can be heavy
// and we'd rather not block the main thread
setTimeout(() => {
Expand Down
23 changes: 21 additions & 2 deletions packages/rrweb/src/record/observers/canvas/canvas-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IWindow,
listenerHandler,
Mirror,
blockSelector,
} from '../../../types';
import initCanvas2DMutationObserver from './2d';
import initCanvasContextObserver from './canvas';
Expand Down Expand Up @@ -56,13 +57,20 @@ export class CanvasManager {
mutationCb: canvasMutationCallback;
win: IWindow;
blockClass: blockClass;
blockSelector: blockSelector;
unblockSelector: blockSelector;
mirror: Mirror;
}) {
this.mutationCb = options.mutationCb;
this.mirror = options.mirror;

if (options.recordCanvas === true)
this.initCanvasMutationObserver(options.win, options.blockClass);
this.initCanvasMutationObserver(
options.win,
options.blockClass,
options.blockSelector,
options.unblockSelector,
);
}

private processMutation: canvasManagerMutationCallback = function (
Expand All @@ -85,22 +93,33 @@ export class CanvasManager {
private initCanvasMutationObserver(
win: IWindow,
blockClass: blockClass,
unblockSelector: blockSelector,
blockSelector: blockSelector,
): void {
this.startRAFTimestamping();
this.startPendingCanvasMutationFlusher();

const canvasContextReset = initCanvasContextObserver(win, blockClass);
const canvasContextReset = initCanvasContextObserver(
win,
blockClass,
blockSelector,
unblockSelector,
);
const canvas2DReset = initCanvas2DMutationObserver(
this.processMutation.bind(this),
win,
blockClass,
blockSelector,
unblockSelector,
this.mirror,
);

const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(
this.processMutation.bind(this),
win,
blockClass,
blockSelector,
unblockSelector,
this.mirror,
);

Expand Down
18 changes: 16 additions & 2 deletions packages/rrweb/src/record/observers/canvas/canvas.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { INode, ICanvas } from '@sentry-internal/rrweb-snapshot';
import { blockClass, IWindow, listenerHandler } from '../../../types';
import {
blockClass,
IWindow,
listenerHandler,
blockSelector,
} from '../../../types';
import { isBlocked, patch } from '../../../utils';

export default function initCanvasContextObserver(
win: IWindow,
blockClass: blockClass,
blockSelector: blockSelector,
unblockSelector: blockSelector,
): listenerHandler {
const handlers: listenerHandler[] = [];
try {
Expand All @@ -17,7 +24,14 @@ export default function initCanvasContextObserver(
contextType: string,
...args: Array<unknown>
) {
if (!isBlocked((this as unknown) as INode, blockClass)) {
if (
!isBlocked(
(this as unknown) as INode,
blockClass,
blockSelector,
unblockSelector,
)
) {
if (!('__context' in this))
(this as ICanvas).__context = contextType;
}
Expand Down
18 changes: 17 additions & 1 deletion packages/rrweb/src/record/observers/canvas/webgl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
IWindow,
listenerHandler,
Mirror,
blockSelector,
} from '../../../types';
import { hookSetter, isBlocked, patch } from '../../../utils';
import { saveWebGLVar, serializeArgs } from './serialize-args';
Expand All @@ -16,6 +17,8 @@ function patchGLPrototype(
type: CanvasContext,
cb: canvasManagerMutationCallback,
blockClass: blockClass,
unblockSelector: blockSelector,
blockSelector: blockSelector,
mirror: Mirror,
win: IWindow,
): listenerHandler[] {
Expand All @@ -32,7 +35,14 @@ function patchGLPrototype(
return function (this: typeof prototype, ...args: Array<unknown>) {
const result = original.apply(this, args);
saveWebGLVar(result, win, prototype);
if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
if (
!isBlocked(
(this.canvas as unknown) as INode,
blockClass,
blockSelector,
unblockSelector,
)
) {
const id = mirror.getId((this.canvas as unknown) as INode);

const recordArgs = serializeArgs([...args], win, prototype);
Expand Down Expand Up @@ -72,6 +82,8 @@ export default function initCanvasWebGLMutationObserver(
cb: canvasManagerMutationCallback,
win: IWindow,
blockClass: blockClass,
blockSelector: blockSelector,
unblockSelector: blockSelector,
mirror: Mirror,
): listenerHandler {
const handlers: listenerHandler[] = [];
Expand All @@ -82,6 +94,8 @@ export default function initCanvasWebGLMutationObserver(
CanvasContext.WebGL,
cb,
blockClass,
blockSelector,
unblockSelector,
mirror,
win,
),
Expand All @@ -94,6 +108,8 @@ export default function initCanvasWebGLMutationObserver(
CanvasContext.WebGL2,
cb,
blockClass,
blockSelector,
unblockSelector,
mirror,
win,
),
Expand Down
2 changes: 2 additions & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ export type eventWithTime = event & {

export type blockClass = string | RegExp;

export type blockSelector = string | null;

export type maskTextClass = string | RegExp;

export type SamplingStrategy = Partial<{
Expand Down
Loading

0 comments on commit b19ed89

Please sign in to comment.