Impl record iframe (#481)
* Impl record iframe * iframe observe * temp: add bundle file to git * update bundle * update with pick * update bundle * fix fragment map remove * feat: add an option to determine whether to pause CSS animation when playback is paused (#428) set pauseAnimation to true by default * fix: elements would lose some states like scroll position because of "virtual parent" optimization (#427) * fix: elements would lose some state like scroll position because of "virtual parent" optimization * refactor: the bugfix code bug: elements would lose some state like scroll position because of "virtual parent" optimization * fix: an error occured at applyMutation(remove nodes part) error message: Uncaught (in promise) DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node * pick fixes * revert ignore file * re-impl iframe record * re-impl iframe replay * code housekeeping * move multi layer dimension calculation to replay side * update test cases * teardown test server * upgrade rrweb-snapshot with iframe load timeout Co-authored-by: Lucky Feng <yun.feng@smartx.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
MouseInteractions,
|
||||
playerConfig,
|
||||
playerMetaData,
|
||||
viewportResizeDimention,
|
||||
viewportResizeDimension,
|
||||
missingNodeMap,
|
||||
addedNodeMutation,
|
||||
missingNode,
|
||||
@@ -37,6 +37,9 @@ import {
|
||||
TreeIndex,
|
||||
queueToResolveTrees,
|
||||
iterateResolveTree,
|
||||
AppendedIframe,
|
||||
isIframeINode,
|
||||
getBaseDimension,
|
||||
} from '../utils';
|
||||
import getInjectStyleRules from './styles/inject-style';
|
||||
import './styles/style.css';
|
||||
@@ -49,6 +52,7 @@ const SKIP_TIME_INTERVAL = 5 * 1000;
|
||||
const mitt = (mittProxy as any).default || mittProxy;
|
||||
|
||||
const REPLAY_CONSOLE_PREFIX = '[replayer]';
|
||||
const SCROLL_ATTRIBUTE_NAME = '__rrweb_scroll__';
|
||||
|
||||
const defaultMouseTailConfig = {
|
||||
duration: 500,
|
||||
@@ -111,6 +115,8 @@ export class Replayer {
|
||||
|
||||
private imageMap: Map<eventWithTime, HTMLImageElement> = new Map();
|
||||
|
||||
private newDocumentQueue: addedNodeMutation[] = [];
|
||||
|
||||
constructor(
|
||||
events: Array<eventWithTime | string>,
|
||||
config?: Partial<playerConfig>,
|
||||
@@ -233,6 +239,10 @@ export class Replayer {
|
||||
}
|
||||
if (firstFullsnapshot) {
|
||||
setTimeout(() => {
|
||||
// when something has been played, there is no need to rebuild poster
|
||||
if (this.timer.timeOffset > 0) {
|
||||
return;
|
||||
}
|
||||
this.rebuildFullSnapshot(
|
||||
firstFullsnapshot as fullSnapshotEvent & { timestamp: number },
|
||||
);
|
||||
@@ -406,7 +416,7 @@ export class Replayer {
|
||||
}
|
||||
}
|
||||
|
||||
private handleResize(dimension: viewportResizeDimention) {
|
||||
private handleResize(dimension: viewportResizeDimension) {
|
||||
this.iframe.style.display = 'inherit';
|
||||
for (const el of [this.mouseTail, this.iframe]) {
|
||||
if (!el) {
|
||||
@@ -534,11 +544,44 @@ export class Replayer {
|
||||
);
|
||||
}
|
||||
this.legacy_missingNodeRetryMap = {};
|
||||
const collected: AppendedIframe[] = [];
|
||||
mirror.map = rebuild(event.data.node, {
|
||||
doc: this.iframe.contentDocument,
|
||||
afterAppend: (builtNode) => {
|
||||
this.collectIframeAndAttachDocument(collected, builtNode);
|
||||
},
|
||||
})[1];
|
||||
const styleEl = document.createElement('style');
|
||||
for (const { mutationInQueue, builtNode } of collected) {
|
||||
this.attachDocumentToIframe(mutationInQueue, builtNode);
|
||||
this.newDocumentQueue = this.newDocumentQueue.filter(
|
||||
(m) => m !== mutationInQueue,
|
||||
);
|
||||
if (builtNode.contentDocument) {
|
||||
const { documentElement, head } = builtNode.contentDocument;
|
||||
this.insertStyleRules(documentElement, head);
|
||||
}
|
||||
}
|
||||
const { documentElement, head } = this.iframe.contentDocument;
|
||||
this.insertStyleRules(documentElement, head);
|
||||
if (!this.service.state.matches('playing')) {
|
||||
this.iframe.contentDocument
|
||||
.getElementsByTagName('html')[0]
|
||||
.classList.add('rrweb-paused');
|
||||
}
|
||||
this.emitter.emit(ReplayerEvents.FullsnapshotRebuilded, event);
|
||||
if (!isSync) {
|
||||
this.waitForStylesheetLoad();
|
||||
}
|
||||
if (this.config.UNSAFE_replayCanvas) {
|
||||
this.preloadAllImages();
|
||||
}
|
||||
}
|
||||
|
||||
private insertStyleRules(
|
||||
documentElement: HTMLElement,
|
||||
head: HTMLHeadElement,
|
||||
) {
|
||||
const styleEl = document.createElement('style');
|
||||
documentElement!.insertBefore(styleEl, head);
|
||||
const injectStylesRules = getInjectStyleRules(
|
||||
this.config.blockClass,
|
||||
@@ -548,20 +591,48 @@ export class Replayer {
|
||||
'html.rrweb-paused * { animation-play-state: paused !important; }',
|
||||
);
|
||||
}
|
||||
if (!this.service.state.matches('playing')) {
|
||||
this.iframe.contentDocument
|
||||
.getElementsByTagName('html')[0]
|
||||
.classList.add('rrweb-paused');
|
||||
}
|
||||
for (let idx = 0; idx < injectStylesRules.length; idx++) {
|
||||
(styleEl.sheet! as CSSStyleSheet).insertRule(injectStylesRules[idx], idx);
|
||||
}
|
||||
this.emitter.emit(ReplayerEvents.FullsnapshotRebuilded, event);
|
||||
if (!isSync) {
|
||||
this.waitForStylesheetLoad();
|
||||
}
|
||||
|
||||
private attachDocumentToIframe(
|
||||
mutation: addedNodeMutation,
|
||||
iframeEl: HTMLIFrameElement,
|
||||
) {
|
||||
const collected: AppendedIframe[] = [];
|
||||
buildNodeWithSN(mutation.node, {
|
||||
doc: iframeEl.contentDocument!,
|
||||
map: mirror.map,
|
||||
hackCss: true,
|
||||
skipChild: false,
|
||||
afterAppend: (builtNode) => {
|
||||
this.collectIframeAndAttachDocument(collected, builtNode);
|
||||
},
|
||||
});
|
||||
for (const { mutationInQueue, builtNode } of collected) {
|
||||
this.attachDocumentToIframe(mutationInQueue, builtNode);
|
||||
this.newDocumentQueue = this.newDocumentQueue.filter(
|
||||
(m) => m !== mutationInQueue,
|
||||
);
|
||||
if (builtNode.contentDocument) {
|
||||
const { documentElement, head } = builtNode.contentDocument;
|
||||
this.insertStyleRules(documentElement, head);
|
||||
}
|
||||
}
|
||||
if (this.config.UNSAFE_replayCanvas) {
|
||||
this.preloadAllImages();
|
||||
}
|
||||
|
||||
private collectIframeAndAttachDocument(
|
||||
collected: AppendedIframe[],
|
||||
builtNode: INode,
|
||||
) {
|
||||
if (isIframeINode(builtNode)) {
|
||||
const mutationInQueue = this.newDocumentQueue.find(
|
||||
(m) => m.parentId === builtNode.__sn.id,
|
||||
);
|
||||
if (mutationInQueue) {
|
||||
collected.push({ mutationInQueue, builtNode });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,6 +1092,10 @@ export class Replayer {
|
||||
}
|
||||
let parent = mirror.getNode(mutation.parentId);
|
||||
if (!parent) {
|
||||
if (mutation.node.type === NodeType.Document) {
|
||||
// is newly added document, maybe the document node of an iframe
|
||||
return this.newDocumentQueue.push(mutation);
|
||||
}
|
||||
return queue.push(mutation);
|
||||
}
|
||||
|
||||
@@ -1059,12 +1134,23 @@ export class Replayer {
|
||||
return queue.push(mutation);
|
||||
}
|
||||
|
||||
if (mutation.node.rootId && !mirror.getNode(mutation.node.rootId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetDoc = mutation.node.rootId
|
||||
? mirror.getNode(mutation.node.rootId)
|
||||
: this.iframe.contentDocument;
|
||||
if (isIframeINode(parent)) {
|
||||
this.attachDocumentToIframe(mutation, parent);
|
||||
return;
|
||||
}
|
||||
const target = buildNodeWithSN(mutation.node, {
|
||||
doc: this.iframe.contentDocument,
|
||||
doc: targetDoc as Document,
|
||||
map: mirror.map,
|
||||
skipChild: true,
|
||||
hackCss: true,
|
||||
}) as Node;
|
||||
}) as INode;
|
||||
|
||||
// legacy data, we should not have -1 siblings any more
|
||||
if (mutation.previousId === -1 || mutation.nextId === -1) {
|
||||
@@ -1087,6 +1173,22 @@ export class Replayer {
|
||||
parent.appendChild(target);
|
||||
}
|
||||
|
||||
if (isIframeINode(target)) {
|
||||
const mutationInQueue = this.newDocumentQueue.find(
|
||||
(m) => m.parentId === target.__sn.id,
|
||||
);
|
||||
if (mutationInQueue) {
|
||||
this.attachDocumentToIframe(mutationInQueue, target);
|
||||
this.newDocumentQueue = this.newDocumentQueue.filter(
|
||||
(m) => m !== mutationInQueue,
|
||||
);
|
||||
}
|
||||
if (target.contentDocument) {
|
||||
const { documentElement, head } = target.contentDocument;
|
||||
this.insertStyleRules(documentElement, head);
|
||||
}
|
||||
}
|
||||
|
||||
if (mutation.previousId || mutation.nextId) {
|
||||
this.legacy_resolveMissingNode(
|
||||
legacy_missingNodeMap,
|
||||
@@ -1228,7 +1330,7 @@ export class Replayer {
|
||||
* generate a console log replayer which implement the interface ReplayLogger
|
||||
*/
|
||||
private getConsoleLogger(): ReplayLogger {
|
||||
const rrwebOriginal = '__rrweb_original__';
|
||||
const rrwebOriginal = SCROLL_ATTRIBUTE_NAME;
|
||||
const replayLogger: ReplayLogger = {};
|
||||
for (const level of this.config.logConfig.level!)
|
||||
if (level === 'trace')
|
||||
@@ -1284,14 +1386,18 @@ export class Replayer {
|
||||
}
|
||||
|
||||
private moveAndHover(d: incrementalData, x: number, y: number, id: number) {
|
||||
this.mouse.style.left = `${x}px`;
|
||||
this.mouse.style.top = `${y}px`;
|
||||
this.drawMouseTail({ x, y });
|
||||
|
||||
const target = mirror.getNode(id);
|
||||
if (!target) {
|
||||
return this.debugNodeNotFound(d, id);
|
||||
}
|
||||
|
||||
const base = getBaseDimension(target);
|
||||
const _x = x + base.x;
|
||||
const _y = y + base.y;
|
||||
|
||||
this.mouse.style.left = `${_x}px`;
|
||||
this.mouse.style.top = `${_y}px`;
|
||||
this.drawMouseTail({ x: _x, y: _y });
|
||||
this.hoverElements((target as Node) as Element);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const rules: (blockClass: string) => string[] = (blockClass: string) => [
|
||||
`iframe, .${blockClass} { background: #ccc }`,
|
||||
`.${blockClass} { background: #ccc }`,
|
||||
'noscript { display: none !important; }',
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user