fix (tracker): 3.5.4: shadowDOM fix - extended 'inDocument' check

This commit is contained in:
ShiKhu 2022-03-18 20:04:31 +01:00
parent 18a1071060
commit be58d4e754
4 changed files with 58 additions and 34 deletions

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "3.5.3",
"version": "3.5.4",
"keywords": [
"logging",
"replay"

View file

@ -41,32 +41,57 @@ export function isInstance<T extends WindowConstructor>(node: Node, constr: Cons
// @ts-ignore (for EI, Safary)
doc.parentWindow ||
doc.defaultView; // TODO: smart global typing for Window object
while(context.parent && context.parent !== context) {
while((context.parent || context.top) && context.parent !== context) {
// @ts-ignore
if (node instanceof context[constr.name]) {
return true
}
// @ts-ignore
context = context.parent
context = context.parent || context.top
}
// @ts-ignore
return node instanceof context[constr.name]
}
export function inDocument(node: Node): boolean {
// TODO: ensure 1. it works in every cases (iframes/detached nodes) and 2. the most efficient
export function inDocument(node: Node) {
const doc = node.ownerDocument
if (!doc) { return false }
if (doc.contains(node)) { return true }
let context: Window =
// @ts-ignore (for EI, Safary)
doc.parentWindow ||
doc.defaultView;
while(context.parent && context.parent !== context) {
if (context.document.contains(node)) {
if (!doc) { return true } // Document
let current: Node | null = node
while(current) {
if (current === doc) {
return true
} else if(isInstance(current, ShadowRoot)) {
current = current.host
} else {
current = current.parentNode
}
// @ts-ignore
context = context.parent
}
return false;
return false
}
// export function inDocument(node: Node): boolean {
// // @ts-ignore compatability
// if (node.getRootNode) {
// let root: Node
// while ((root = node.getRootNode()) !== node) {
// ////
// }
// }
// const doc = node.ownerDocument
// if (!doc) { return false }
// if (doc.contains(node)) { return true }
// let context: Window =
// // @ts-ignore (for EI, Safary)
// doc.parentWindow ||
// doc.defaultView;
// while(context.parent && context.parent !== context) {
// if (context.document.contains(node)) {
// return true
// }
// // @ts-ignore
// context = context.parent
// }
// return false;
// }

View file

@ -1,4 +1,3 @@
import { hasOpenreplayAttribute } from "../../utils.js";
import {
RemoveNodeAttribute,
SetNodeAttribute,
@ -59,9 +58,7 @@ export default abstract class Observer {
private readonly indexes: Array<number> = [];
private readonly attributesList: Array<Set<string> | undefined> = [];
private readonly textSet: Set<number> = new Set();
private readonly inUpperContext: boolean;
constructor(protected readonly app: App, protected readonly context: Window = window) {
this.inUpperContext = context.parent === context //TODO: get rid of context here
constructor(protected readonly app: App, protected readonly isTopContext = false) {
this.observer = new MutationObserver(
this.app.safe((mutations) => {
for (const mutation of mutations) {
@ -226,7 +223,7 @@ export default abstract class Observer {
// Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before)
// TODO: get rid of "special" cases (there is an issue with CreateDocument altered behaviour though)
// TODO: Clean the logic (though now it workd fine)
if (!isInstance(node, HTMLHtmlElement) || !this.inUpperContext) {
if (!isInstance(node, HTMLHtmlElement) || !this.isTopContext) {
if (parent === null) {
this.unbindNode(node);
return false;
@ -321,6 +318,8 @@ export default abstract class Observer {
for (let id = 0; id < this.recents.length; id++) {
// TODO: make things/logic nice here.
// commit required in any case if recents[id] true or false (in case of unbinding) or undefined (in case of attr change).
// Possible solution: separate new node commit (recents) and new attribute/move node commit
// Otherwise commitNode is called on each node, which might be a lot
if (!this.myNodes[id]) { continue }
this.commitNode(id);
if (this.recents[id] === true && (node = this.app.nodes.getNode(id))) {

View file

@ -6,7 +6,7 @@ import ShadowRootObserver from "./shadow_root_observer.js";
import { CreateDocument } from "../../../messages/index.js";
import App from "../index.js";
import { IN_BROWSER } from '../../utils.js'
import { IN_BROWSER, hasOpenreplayAttribute } from '../../utils.js'
export interface Options {
captureIFrames: boolean
@ -17,15 +17,16 @@ const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : ()=>n
export default class TopObserver extends Observer {
private readonly options: Options;
constructor(app: App, options: Partial<Options>) {
super(app);
super(app, true);
this.options = Object.assign({
captureIFrames: false
captureIFrames: true
}, options);
// IFrames
this.app.nodes.attachNodeCallback(node => {
if (isInstance(node, HTMLIFrameElement) &&
(this.options.captureIFrames || node.getAttribute("data-openreplay-capture"))
((this.options.captureIFrames && !hasOpenreplayAttribute(node, "obscured"))
|| hasOpenreplayAttribute(node, "capture"))
) {
this.handleIframe(node)
}
@ -42,26 +43,25 @@ export default class TopObserver extends Observer {
private iframeObservers: IFrameObserver[] = [];
private handleIframe(iframe: HTMLIFrameElement): void {
let context: Window | null = null
let doc: Document | null = null
const handle = this.app.safe(() => {
const id = this.app.nodes.getID(iframe)
if (id === undefined) { return } //log
if (iframe.contentWindow === context) { return } //Does this happen frequently?
context = iframe.contentWindow as Window | null;
if (!context) { return }
const observer = new IFrameObserver(this.app, context)
if (iframe.contentDocument === doc) { return } // How frequently can it happen?
doc = iframe.contentDocument
if (!doc || !iframe.contentWindow) { return }
const observer = new IFrameObserver(this.app)
this.iframeObservers.push(observer)
observer.observe(iframe)
})
this.app.attachEventListener(iframe, "load", handle)
iframe.addEventListener("load", handle) // why app.attachEventListener not working?
handle()
}
private shadowRootObservers: ShadowRootObserver[] = []
private handleShadowRoot(shRoot: ShadowRoot) {
const observer = new ShadowRootObserver(this.app, this.context)
const observer = new ShadowRootObserver(this.app)
this.shadowRootObservers.push(observer)
observer.observe(shRoot.host)
}
@ -81,9 +81,9 @@ export default class TopObserver extends Observer {
// the change in the re-player behaviour caused by CreateDocument message:
// the 0-node ("fRoot") will become #document rather than documentElement as it is now.
// Alternatively - observe(#document) then bindNode(documentElement)
this.observeRoot(this.context.document, () => {
this.observeRoot(window.document, () => {
this.app.send(new CreateDocument())
}, this.context.document.documentElement);
}, window.document.documentElement);
}
disconnect() {