Skip to content

Commit 98aa732

Browse files
authored
nested record iframe (#63)
* pick nested branch * iframe snapshot * temp: add bundle file to git * revert ignore file * refactor iframe impl 1. do callback one iframe is loaded, let rrweb handle the rest 2. handle iframe as normal element in rebuild * rename hook function
1 parent a3ff5e5 commit 98aa732

File tree

11 files changed

+343
-7
lines changed

11 files changed

+343
-7
lines changed

src/rebuild.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,20 @@ export function buildNodeWithSN(
198198
map: idNodeMap;
199199
skipChild?: boolean;
200200
hackCss: boolean;
201+
afterAppend?: (n: INode) => unknown;
201202
},
202203
): INode | null {
203-
const { doc, map, skipChild = false, hackCss = true } = options;
204+
const { doc, map, skipChild = false, hackCss = true, afterAppend } = options;
204205
let node = buildNode(n, { doc, hackCss });
205206
if (!node) {
206207
return null;
207208
}
209+
if (n.rootId) {
210+
console.assert(
211+
((map[n.rootId] as unknown) as Document) === doc,
212+
'Target document should has the same root id.',
213+
);
214+
}
208215
// use target document as root document
209216
if (n.type === NodeType.Document) {
210217
// close before open to make sure document was closed
@@ -215,6 +222,7 @@ export function buildNodeWithSN(
215222

216223
(node as INode).__sn = n;
217224
map[n.id] = node as INode;
225+
218226
if (
219227
(n.type === NodeType.Document || n.type === NodeType.Element) &&
220228
!skipChild
@@ -225,14 +233,20 @@ export function buildNodeWithSN(
225233
map,
226234
skipChild: false,
227235
hackCss,
236+
afterAppend,
228237
});
229238
if (!childNode) {
230239
console.warn('Failed to rebuild', childN);
231-
} else {
232-
node.appendChild(childNode);
240+
continue;
241+
}
242+
243+
node.appendChild(childNode);
244+
if (afterAppend) {
245+
afterAppend(childNode);
233246
}
234247
}
235248
}
249+
236250
return node as INode;
237251
}
238252

@@ -274,15 +288,17 @@ function rebuild(
274288
doc: Document;
275289
onVisit?: (node: INode) => unknown;
276290
hackCss?: boolean;
291+
afterAppend?: (n: INode) => unknown;
277292
},
278293
): [Node | null, idNodeMap] {
279-
const { doc, onVisit, hackCss = true } = options;
294+
const { doc, onVisit, hackCss = true, afterAppend } = options;
280295
const idNodeMap: idNodeMap = {};
281296
const node = buildNodeWithSN(n, {
282297
doc,
283298
map: idNodeMap,
284299
skipChild: false,
285300
hackCss,
301+
afterAppend,
286302
});
287303
visit(idNodeMap, (visitedNode) => {
288304
if (onVisit) {

src/snapshot.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,34 @@ export function _isBlockedElement(
196196
return false;
197197
}
198198

199+
// https://stackoverflow.com/a/36155560
200+
function onceIframeLoaded(
201+
iframeEl: HTMLIFrameElement,
202+
listener: () => unknown,
203+
) {
204+
const win = iframeEl.contentWindow;
205+
if (!win) {
206+
return;
207+
}
208+
// document is loading
209+
if (win.document.readyState !== 'complete') {
210+
iframeEl.addEventListener('load', listener);
211+
return;
212+
}
213+
// check blank frame for Chrome
214+
const blankUrl = 'about:blank';
215+
if (
216+
win.location.href !== blankUrl ||
217+
iframeEl.src === blankUrl ||
218+
iframeEl.src === ''
219+
) {
220+
listener();
221+
return;
222+
}
223+
// use default listener
224+
iframeEl.addEventListener('load', listener);
225+
}
226+
199227
function serializeNode(
200228
n: Node,
201229
options: {
@@ -215,18 +243,26 @@ function serializeNode(
215243
maskInputOptions = {},
216244
recordCanvas,
217245
} = options;
246+
// Only record root id when document object is not the base document
247+
let rootId: number | undefined;
248+
if (((doc as unknown) as INode).__sn) {
249+
const docId = ((doc as unknown) as INode).__sn.id;
250+
rootId = docId === 1 ? undefined : docId;
251+
}
218252
switch (n.nodeType) {
219253
case n.DOCUMENT_NODE:
220254
return {
221255
type: NodeType.Document,
222256
childNodes: [],
257+
rootId,
223258
};
224259
case n.DOCUMENT_TYPE_NODE:
225260
return {
226261
type: NodeType.DocumentType,
227262
name: (n as DocumentType).name,
228263
publicId: (n as DocumentType).publicId,
229264
systemId: (n as DocumentType).systemId,
265+
rootId,
230266
};
231267
case n.ELEMENT_NODE:
232268
const needBlock = _isBlockedElement(
@@ -318,6 +354,7 @@ function serializeNode(
318354
if ((n as HTMLElement).scrollTop) {
319355
attributes.rr_scrollTop = (n as HTMLElement).scrollTop;
320356
}
357+
// block element
321358
if (needBlock) {
322359
const { width, height } = (n as HTMLElement).getBoundingClientRect();
323360
attributes = {
@@ -326,13 +363,18 @@ function serializeNode(
326363
rr_height: `${height}px`,
327364
};
328365
}
366+
// iframe
367+
if (tagName === 'iframe') {
368+
delete attributes.src;
369+
}
329370
return {
330371
type: NodeType.Element,
331372
tagName,
332373
attributes,
333374
childNodes: [],
334375
isSVG: isSVGElement(n as Element) || undefined,
335376
needBlock,
377+
rootId,
336378
};
337379
case n.TEXT_NODE:
338380
// The parent node may not be a html element which has a tagName attribute.
@@ -351,16 +393,19 @@ function serializeNode(
351393
type: NodeType.Text,
352394
textContent: textContent || '',
353395
isStyle,
396+
rootId,
354397
};
355398
case n.CDATA_SECTION_NODE:
356399
return {
357400
type: NodeType.CDATA,
358401
textContent: '',
402+
rootId,
359403
};
360404
case n.COMMENT_NODE:
361405
return {
362406
type: NodeType.Comment,
363407
textContent: (n as Comment).textContent || '',
408+
rootId,
364409
};
365410
default:
366411
return false;
@@ -472,6 +517,8 @@ export function serializeNodeWithId(
472517
slimDOMOptions: SlimDOMOptions;
473518
recordCanvas?: boolean;
474519
preserveWhiteSpace?: boolean;
520+
onSerialize?: (n: INode) => unknown;
521+
onIframeLoad?: (iframeINode: INode, node: serializedNodeWithId) => unknown;
475522
},
476523
): serializedNodeWithId | null {
477524
const {
@@ -484,6 +531,8 @@ export function serializeNodeWithId(
484531
maskInputOptions = {},
485532
slimDOMOptions,
486533
recordCanvas = false,
534+
onSerialize,
535+
onIframeLoad,
487536
} = options;
488537
let { preserveWhiteSpace = true } = options;
489538
const _serializedNode = serializeNode(n, {
@@ -521,6 +570,9 @@ export function serializeNodeWithId(
521570
return null; // slimDOM
522571
}
523572
map[id] = n as INode;
573+
if (onSerialize) {
574+
onSerialize(n as INode);
575+
}
524576
let recordChild = !skipChild;
525577
if (serializedNode.type === NodeType.Element) {
526578
recordChild = recordChild && !serializedNode.needBlock;
@@ -552,12 +604,44 @@ export function serializeNodeWithId(
552604
slimDOMOptions,
553605
recordCanvas,
554606
preserveWhiteSpace,
607+
onSerialize,
608+
onIframeLoad,
555609
});
556610
if (serializedChildNode) {
557611
serializedNode.childNodes.push(serializedChildNode);
558612
}
559613
}
560614
}
615+
616+
if (
617+
serializedNode.type === NodeType.Element &&
618+
serializedNode.tagName === 'iframe'
619+
) {
620+
onceIframeLoaded(n as HTMLIFrameElement, () => {
621+
const iframeDoc = (n as HTMLIFrameElement).contentDocument;
622+
if (iframeDoc && onIframeLoad) {
623+
const serializedIframeNode = serializeNodeWithId(iframeDoc, {
624+
doc: iframeDoc,
625+
map,
626+
blockClass,
627+
blockSelector,
628+
skipChild: false,
629+
inlineStylesheet,
630+
maskInputOptions,
631+
slimDOMOptions,
632+
recordCanvas,
633+
preserveWhiteSpace,
634+
onSerialize,
635+
onIframeLoad,
636+
});
637+
638+
if (serializedIframeNode) {
639+
onIframeLoad(n as INode, serializedIframeNode);
640+
}
641+
}
642+
});
643+
}
644+
561645
return serializedNode;
562646
}
563647

@@ -570,6 +654,9 @@ function snapshot(
570654
slimDOM?: boolean | SlimDOMOptions;
571655
recordCanvas?: boolean;
572656
blockSelector?: string | null;
657+
preserveWhiteSpace?: boolean;
658+
onSerialize?: (n: INode) => unknown;
659+
onIframeLoad?: (iframeINode: INode, node: serializedNodeWithId) => unknown;
573660
},
574661
): [serializedNodeWithId | null, idNodeMap] {
575662
const {
@@ -579,6 +666,9 @@ function snapshot(
579666
blockSelector = null,
580667
maskAllInputs = false,
581668
slimDOM = false,
669+
preserveWhiteSpace,
670+
onSerialize,
671+
onIframeLoad,
582672
} = options || {};
583673
const idNodeMap: idNodeMap = {};
584674
const maskInputOptions: MaskInputOptions =
@@ -632,6 +722,9 @@ function snapshot(
632722
maskInputOptions,
633723
slimDOMOptions,
634724
recordCanvas,
725+
preserveWhiteSpace,
726+
onSerialize,
727+
onIframeLoad,
635728
}),
636729
idNodeMap,
637730
];

src/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,16 @@ export type commentNode = {
4747
textContent: string;
4848
};
4949

50-
export type serializedNode =
50+
export type serializedNode = (
5151
| documentNode
5252
| documentTypeNode
5353
| elementNode
5454
| textNode
5555
| cdataNode
56-
| commentNode;
56+
| commentNode
57+
) & {
58+
rootId?: number;
59+
};
5760

5861
export type serializedNodeWithId = serializedNode & { id: number };
5962

0 commit comments

Comments
 (0)