diff --git a/tracker/tracker/src/main/app/observer/top_observer.ts b/tracker/tracker/src/main/app/observer/top_observer.ts index 55ba5af7d..b3ab0e6db 100644 --- a/tracker/tracker/src/main/app/observer/top_observer.ts +++ b/tracker/tracker/src/main/app/observer/top_observer.ts @@ -14,6 +14,16 @@ export interface Options { type ContextCallback = (context: Window & typeof globalThis) => void +// Le truc - for defining an absolute offset for (nested) iframe documents. (To track mouse movments) +type Offset = { top: number; left: number } +type PatchedDocument = Document & { + __openreplay__getOffset: () => Offset +} +function isPatchedDocument(doc: Document): doc is PatchedDocument { + // @ts-ignore + return typeof doc.__openreplay__getOffset === 'function' +} + const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot() export default class TopObserver extends Observer { @@ -53,6 +63,14 @@ export default class TopObserver extends Observer { this.contextCallbacks.push(cb) } + // Le truc + getDocumentOffset(doc: Document): Offset { + if (isPatchedDocument(doc)) { + return doc.__openreplay__getOffset() + } + return { top: 0, left: 0 } + } + private iframeObservers: IFrameObserver[] = [] private handleIframe(iframe: HTMLIFrameElement): void { let doc: Document | null = null @@ -70,6 +88,15 @@ export default class TopObserver extends Observer { this.iframeObservers.push(observer) observer.observe(iframe) doc = currentDoc + + // Le truc + ;(doc as PatchedDocument).__openreplay__getOffset = () => { + const { top, left } = this.getDocumentOffset(iframe.ownerDocument) + return { + top: iframe.offsetTop + top, + left: iframe.offsetLeft + left, + } + } } if (currentWin && currentWin !== win) { //@ts-ignore https://github.com/microsoft/TypeScript/issues/41684 diff --git a/tracker/tracker/src/main/modules/console.ts b/tracker/tracker/src/main/modules/console.ts index 941dd761c..2a2b61c1d 100644 --- a/tracker/tracker/src/main/modules/console.ts +++ b/tracker/tracker/src/main/modules/console.ts @@ -122,7 +122,7 @@ export default function (app: App, opts: Partial): void { const patchConsole = (console: Console) => options.consoleMethods!.forEach((method) => { if (consoleMethods.indexOf(method) === -1) { - console.error(`OpenReplay: unsupported console method "${method}"`) + app.debug.error(`OpenReplay: unsupported console method "${method}"`) return } const fn = (console as any)[method] @@ -134,23 +134,8 @@ export default function (app: App, opts: Partial): void { sendConsoleLog(method, args) } }) - patchConsole(window.console) + const patchContext = app.safe((context: typeof globalThis) => patchConsole(context.console)) - app.nodes.attachNodeCallback( - app.safe((node) => { - if (hasTag(node, 'IFRAME')) { - // TODO: newContextCallback - let context = node.contentWindow - if (context) { - patchConsole((context as Window & typeof globalThis).console) - } - app.attachEventListener(node, 'load', () => { - if (node.contentWindow !== context) { - context = node.contentWindow - patchConsole((context as Window & typeof globalThis).console) - } - }) - } - }), - ) + patchContext(window) + app.observer.attachContextCallback(patchContext) } diff --git a/tracker/tracker/src/main/modules/cssrules.ts b/tracker/tracker/src/main/modules/cssrules.ts index 2c1176f99..b6b898788 100644 --- a/tracker/tracker/src/main/modules/cssrules.ts +++ b/tracker/tracker/src/main/modules/cssrules.ts @@ -27,16 +27,20 @@ export default function (app: App | null) { } // else error? }) - const { insertRule, deleteRule } = CSSStyleSheet.prototype + const patchContext = (context: typeof globalThis) => { + const { insertRule, deleteRule } = context.CSSStyleSheet.prototype + context.CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) { + processOperation(this, index, rule) + return insertRule.call(this, rule, index) + } + context.CSSStyleSheet.prototype.deleteRule = function (index: number) { + processOperation(this, index) + return deleteRule.call(this, index) + } + } - CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) { - processOperation(this, index, rule) - return insertRule.call(this, rule, index) - } - CSSStyleSheet.prototype.deleteRule = function (index: number) { - processOperation(this, index) - return deleteRule.call(this, index) - } + patchContext(window) + app.observer.attachContextCallback(patchContext) app.nodes.attachNodeCallback((node: Node): void => { if (!hasTag(node, 'STYLE') || !node.sheet) { diff --git a/tracker/tracker/src/main/modules/input.ts b/tracker/tracker/src/main/modules/input.ts index 8364d886c..319812509 100644 --- a/tracker/tracker/src/main/modules/input.ts +++ b/tracker/tracker/src/main/modules/input.ts @@ -49,7 +49,7 @@ const labelElementFor: (element: TextEditableElement) => HTMLLabelElement | unde } const id = node.id if (id) { - const labels = document.querySelectorAll('label[for="' + id + '"]') + const labels = node.ownerDocument.querySelectorAll('label[for="' + id + '"]') if (labels !== null && labels.length === 1) { return labels[0] as HTMLLabelElement } diff --git a/tracker/tracker/src/main/modules/mouse.ts b/tracker/tracker/src/main/modules/mouse.ts index 4704dc5be..415c1925f 100644 --- a/tracker/tracker/src/main/modules/mouse.ts +++ b/tracker/tracker/src/main/modules/mouse.ts @@ -1,10 +1,10 @@ import type App from '../app/index.js' -import { hasTag, isSVGElement } from '../app/guards.js' +import { hasTag, isSVGElement, isDocument } from '../app/guards.js' import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils.js' import { MouseMove, MouseClick } from '../app/messages.gen.js' import { getInputLabel } from './input.js' -function _getSelector(target: Element): string { +function _getSelector(target: Element, document: Document): string { let el: Element | null = target let selector: string | null = null do { @@ -37,18 +37,18 @@ function isClickable(element: Element): boolean { element.getAttribute('role') === 'button' ) //|| element.className.includes("btn") - // MBTODO: intersect addEventListener + // MBTODO: intersept addEventListener } -//TODO: fix (typescript doesn't allow work when the guard is inside the function) -function getTarget(target: EventTarget | null): Element | null { +//TODO: fix (typescript is not sure about target variable after assignation of svg) +function getTarget(target: EventTarget | null, document: Document): Element | null { if (target instanceof Element) { - return _getTarget(target) + return _getTarget(target, document) } return null } -function _getTarget(target: Element): Element | null { +function _getTarget(target: Element, document: Document): Element | null { let element: Element | null = target while (element !== null && element !== document.documentElement) { if (hasOpenreplayAttribute(element, 'masked')) { @@ -120,48 +120,58 @@ export default function (app: App): void { } } - const selectorMap: { [id: number]: string } = {} - function getSelector(id: number, target: Element): string { - return (selectorMap[id] = selectorMap[id] || _getSelector(target)) + const patchDocument = (document: Document) => { + const selectorMap: { [id: number]: string } = {} + function getSelector(id: number, target: Element): string { + return (selectorMap[id] = selectorMap[id] || _getSelector(target, document)) + } + + app.attachEventListener(document.documentElement, 'mouseover', (e: MouseEvent): void => { + const target = getTarget(e.target, document) + if (target !== mouseTarget) { + mouseTarget = target + mouseTargetTime = performance.now() + } + }) + app.attachEventListener( + document, + 'mousemove', + (e: MouseEvent): void => { + const { top, left } = app.observer.getDocumentOffset(document) + mousePositionX = e.clientX + left + mousePositionY = e.clientY + top + mousePositionChanged = true + }, + false, + ) + app.attachEventListener(document, 'click', (e: MouseEvent): void => { + const target = getTarget(e.target, document) + if ((!e.clientX && !e.clientY) || target === null) { + return + } + const id = app.nodes.getID(target) + if (id !== undefined) { + sendMouseMove() + app.send( + MouseClick( + id, + mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0, + getTargetLabel(target), + getSelector(id, target), + ), + true, + ) + } + mouseTarget = null + }) } - app.attachEventListener(document.documentElement, 'mouseover', (e: MouseEvent): void => { - const target = getTarget(e.target) - if (target !== mouseTarget) { - mouseTarget = target - mouseTargetTime = performance.now() + app.nodes.attachNodeCallback((node) => { + if (isDocument(node)) { + patchDocument(node) } }) - app.attachEventListener( - document, - 'mousemove', - (e: MouseEvent): void => { - mousePositionX = e.clientX - mousePositionY = e.clientY - mousePositionChanged = true - }, - false, - ) - app.attachEventListener(document, 'click', (e: MouseEvent): void => { - const target = getTarget(e.target) - if ((!e.clientX && !e.clientY) || target === null) { - return - } - const id = app.nodes.getID(target) - if (id !== undefined) { - sendMouseMove() - app.send( - MouseClick( - id, - mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0, - getTargetLabel(target), - getSelector(id, target), - ), - true, - ) - } - mouseTarget = null - }) + patchDocument(document) app.ticker.attach(sendMouseMove, 10) }