shadow DOM recording GA
1. record shadow DOM event target by parsing composed path 2. nested record scroll event in shadow DOM
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
|||||||
listenerHandler,
|
listenerHandler,
|
||||||
LogRecordOptions,
|
LogRecordOptions,
|
||||||
mutationCallbackParam,
|
mutationCallbackParam,
|
||||||
|
scrollCallback,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { IframeManager } from './iframe-manager';
|
import { IframeManager } from './iframe-manager';
|
||||||
import { ShadowDomManager } from './shadow-dom-manager';
|
import { ShadowDomManager } from './shadow-dom-manager';
|
||||||
@@ -197,6 +198,16 @@ function record<T = eventWithTime>(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const wrappedScrollEmit: scrollCallback = (p) =>
|
||||||
|
wrappedEmit(
|
||||||
|
wrapEvent({
|
||||||
|
type: EventType.IncrementalSnapshot,
|
||||||
|
data: {
|
||||||
|
source: IncrementalSource.Scroll,
|
||||||
|
...p,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const iframeManager = new IframeManager({
|
const iframeManager = new IframeManager({
|
||||||
mutationCb: wrappedMutationEmit,
|
mutationCb: wrappedMutationEmit,
|
||||||
@@ -204,6 +215,7 @@ function record<T = eventWithTime>(
|
|||||||
|
|
||||||
const shadowDomManager = new ShadowDomManager({
|
const shadowDomManager = new ShadowDomManager({
|
||||||
mutationCb: wrappedMutationEmit,
|
mutationCb: wrappedMutationEmit,
|
||||||
|
scrollCb: wrappedScrollEmit,
|
||||||
bypassOptions: {
|
bypassOptions: {
|
||||||
blockClass,
|
blockClass,
|
||||||
blockSelector,
|
blockSelector,
|
||||||
@@ -213,6 +225,7 @@ function record<T = eventWithTime>(
|
|||||||
maskInputOptions,
|
maskInputOptions,
|
||||||
maskTextFn,
|
maskTextFn,
|
||||||
recordCanvas,
|
recordCanvas,
|
||||||
|
sampling,
|
||||||
slimDOMOptions,
|
slimDOMOptions,
|
||||||
iframeManager,
|
iframeManager,
|
||||||
},
|
},
|
||||||
@@ -325,16 +338,7 @@ function record<T = eventWithTime>(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
scrollCb: (p) =>
|
scrollCb: wrappedScrollEmit,
|
||||||
wrappedEmit(
|
|
||||||
wrapEvent({
|
|
||||||
type: EventType.IncrementalSnapshot,
|
|
||||||
data: {
|
|
||||||
source: IncrementalSource.Scroll,
|
|
||||||
...p,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
viewportResizeCb: (d) =>
|
viewportResizeCb: (d) =>
|
||||||
wrappedEmit(
|
wrappedEmit(
|
||||||
wrapEvent({
|
wrapEvent({
|
||||||
|
|||||||
@@ -60,6 +60,25 @@ type WindowWithAngularZone = Window & {
|
|||||||
|
|
||||||
export const mutationBuffers: MutationBuffer[] = [];
|
export const mutationBuffers: MutationBuffer[] = [];
|
||||||
|
|
||||||
|
function getEventTarget(event: Event): EventTarget | null {
|
||||||
|
try {
|
||||||
|
if ('composedPath' in event) {
|
||||||
|
const path = event.composedPath();
|
||||||
|
if (path.length) {
|
||||||
|
return path[0];
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
'path' in event &&
|
||||||
|
(event as { path: EventTarget[] }).path.length
|
||||||
|
) {
|
||||||
|
return (event as { path: EventTarget[] }).path[0];
|
||||||
|
}
|
||||||
|
return event.target;
|
||||||
|
} catch {
|
||||||
|
return event.target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function initMutationObserver(
|
export function initMutationObserver(
|
||||||
cb: mutationCallBack,
|
cb: mutationCallBack,
|
||||||
doc: Document,
|
doc: Document,
|
||||||
@@ -176,7 +195,7 @@ function initMoveObserver(
|
|||||||
);
|
);
|
||||||
const updatePosition = throttle<MouseEvent | TouchEvent | DragEvent>(
|
const updatePosition = throttle<MouseEvent | TouchEvent | DragEvent>(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
const { target } = evt;
|
const target = getEventTarget(evt);
|
||||||
const { clientX, clientY } = isTouchEvent(evt)
|
const { clientX, clientY } = isTouchEvent(evt)
|
||||||
? evt.changedTouches[0]
|
? evt.changedTouches[0]
|
||||||
: evt;
|
: evt;
|
||||||
@@ -231,14 +250,15 @@ function initMouseInteractionObserver(
|
|||||||
const handlers: listenerHandler[] = [];
|
const handlers: listenerHandler[] = [];
|
||||||
const getHandler = (eventKey: keyof typeof MouseInteractions) => {
|
const getHandler = (eventKey: keyof typeof MouseInteractions) => {
|
||||||
return (event: MouseEvent | TouchEvent) => {
|
return (event: MouseEvent | TouchEvent) => {
|
||||||
if (isBlocked(event.target as Node, blockClass)) {
|
const target = getEventTarget(event) as Node;
|
||||||
|
if (isBlocked(target as Node, blockClass)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const e = isTouchEvent(event) ? event.changedTouches[0] : event;
|
const e = isTouchEvent(event) ? event.changedTouches[0] : event;
|
||||||
if (!e) {
|
if (!e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = mirror.getId(event.target as INode);
|
const id = mirror.getId(target as INode);
|
||||||
const { clientX, clientY } = e;
|
const { clientX, clientY } = e;
|
||||||
cb({
|
cb({
|
||||||
type: MouseInteractions[eventKey],
|
type: MouseInteractions[eventKey],
|
||||||
@@ -265,7 +285,7 @@ function initMouseInteractionObserver(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function initScrollObserver(
|
export function initScrollObserver(
|
||||||
cb: scrollCallback,
|
cb: scrollCallback,
|
||||||
doc: Document,
|
doc: Document,
|
||||||
mirror: Mirror,
|
mirror: Mirror,
|
||||||
@@ -273,11 +293,12 @@ function initScrollObserver(
|
|||||||
sampling: SamplingStrategy,
|
sampling: SamplingStrategy,
|
||||||
): listenerHandler {
|
): listenerHandler {
|
||||||
const updatePosition = throttle<UIEvent>((evt) => {
|
const updatePosition = throttle<UIEvent>((evt) => {
|
||||||
if (!evt.target || isBlocked(evt.target as Node, blockClass)) {
|
const target = getEventTarget(evt);
|
||||||
|
if (!target || isBlocked(target as Node, blockClass)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = mirror.getId(evt.target as INode);
|
const id = mirror.getId(target as INode);
|
||||||
if (evt.target === doc) {
|
if (target === doc) {
|
||||||
const scrollEl = (doc.scrollingElement || doc.documentElement)!;
|
const scrollEl = (doc.scrollingElement || doc.documentElement)!;
|
||||||
cb({
|
cb({
|
||||||
id,
|
id,
|
||||||
@@ -287,12 +308,12 @@ function initScrollObserver(
|
|||||||
} else {
|
} else {
|
||||||
cb({
|
cb({
|
||||||
id,
|
id,
|
||||||
x: (evt.target as HTMLElement).scrollLeft,
|
x: (target as HTMLElement).scrollLeft,
|
||||||
y: (evt.target as HTMLElement).scrollTop,
|
y: (target as HTMLElement).scrollTop,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, sampling.scroll || 100);
|
}, sampling.scroll || 100);
|
||||||
return on('scroll', updatePosition);
|
return on('scroll', updatePosition, doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initViewportResizeObserver(
|
function initViewportResizeObserver(
|
||||||
@@ -328,7 +349,7 @@ function initInputObserver(
|
|||||||
sampling: SamplingStrategy,
|
sampling: SamplingStrategy,
|
||||||
): listenerHandler {
|
): listenerHandler {
|
||||||
function eventHandler(event: Event) {
|
function eventHandler(event: Event) {
|
||||||
const { target } = event;
|
const target = getEventTarget(event);
|
||||||
if (
|
if (
|
||||||
!target ||
|
!target ||
|
||||||
!(target as Element).tagName ||
|
!(target as Element).tagName ||
|
||||||
@@ -465,7 +486,7 @@ function initMediaInteractionObserver(
|
|||||||
mirror: Mirror,
|
mirror: Mirror,
|
||||||
): listenerHandler {
|
): listenerHandler {
|
||||||
const handler = (type: 'play' | 'pause') => (event: Event) => {
|
const handler = (type: 'play' | 'pause') => (event: Event) => {
|
||||||
const { target } = event;
|
const target = getEventTarget(event);
|
||||||
if (!target || isBlocked(target as Node, blockClass)) {
|
if (!target || isBlocked(target as Node, blockClass)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import {
|
|||||||
maskTextClass,
|
maskTextClass,
|
||||||
MaskTextFn,
|
MaskTextFn,
|
||||||
Mirror,
|
Mirror,
|
||||||
|
scrollCallback,
|
||||||
|
SamplingStrategy,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot';
|
import { MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot';
|
||||||
import { IframeManager } from './iframe-manager';
|
import { IframeManager } from './iframe-manager';
|
||||||
import { initMutationObserver } from './observer';
|
import { initMutationObserver, initScrollObserver } from './observer';
|
||||||
|
|
||||||
type BypassOptions = {
|
type BypassOptions = {
|
||||||
blockClass: blockClass;
|
blockClass: blockClass;
|
||||||
@@ -18,21 +20,25 @@ type BypassOptions = {
|
|||||||
maskInputOptions: MaskInputOptions;
|
maskInputOptions: MaskInputOptions;
|
||||||
maskTextFn: MaskTextFn | undefined;
|
maskTextFn: MaskTextFn | undefined;
|
||||||
recordCanvas: boolean;
|
recordCanvas: boolean;
|
||||||
|
sampling: SamplingStrategy;
|
||||||
slimDOMOptions: SlimDOMOptions;
|
slimDOMOptions: SlimDOMOptions;
|
||||||
iframeManager: IframeManager;
|
iframeManager: IframeManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ShadowDomManager {
|
export class ShadowDomManager {
|
||||||
private mutationCb: mutationCallBack;
|
private mutationCb: mutationCallBack;
|
||||||
|
private scrollCb: scrollCallback;
|
||||||
private bypassOptions: BypassOptions;
|
private bypassOptions: BypassOptions;
|
||||||
private mirror: Mirror;
|
private mirror: Mirror;
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
mutationCb: mutationCallBack;
|
mutationCb: mutationCallBack;
|
||||||
|
scrollCb: scrollCallback;
|
||||||
bypassOptions: BypassOptions;
|
bypassOptions: BypassOptions;
|
||||||
mirror: Mirror;
|
mirror: Mirror;
|
||||||
}) {
|
}) {
|
||||||
this.mutationCb = options.mutationCb;
|
this.mutationCb = options.mutationCb;
|
||||||
|
this.scrollCb = options.scrollCb;
|
||||||
this.bypassOptions = options.bypassOptions;
|
this.bypassOptions = options.bypassOptions;
|
||||||
this.mirror = options.mirror;
|
this.mirror = options.mirror;
|
||||||
}
|
}
|
||||||
@@ -55,5 +61,14 @@ export class ShadowDomManager {
|
|||||||
this,
|
this,
|
||||||
shadowRoot,
|
shadowRoot,
|
||||||
);
|
);
|
||||||
|
initScrollObserver(
|
||||||
|
this.scrollCb,
|
||||||
|
// https://gist.github.com/praveenpuglia/0832da687ed5a5d7a0907046c9ef1813
|
||||||
|
// scroll is not allowed to pass the boundary, so we need to listen the shadow document
|
||||||
|
(shadowRoot as unknown) as Document,
|
||||||
|
this.mirror,
|
||||||
|
this.bypassOptions.blockClass,
|
||||||
|
this.bypassOptions.sampling,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user