tracker: better crossdomain check; angularMode -> forceNgOff toggle

This commit is contained in:
nick-delirium 2024-10-15 17:12:17 +02:00
parent d39e9b8816
commit 5009491f63
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
6 changed files with 55 additions and 37 deletions

View file

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

View file

@ -168,11 +168,12 @@ type AppOptions = {
network?: NetworkOptions
/**
* use this flag if you're using Angular
* use this flag to force angular detection to be offline
*
* basically goes around window.Zone api changes to mutation observer
* and event listeners
* */
angularMode?: boolean
forceNgOff?: boolean
} & WebworkerOptions &
SessOptions
@ -318,7 +319,7 @@ export default class App {
__save_canvas_locally: false,
useAnimationFrame: false,
},
angularMode: false,
forceNgOff: false,
}
this.options = simpleMerge(defaultOptions, options)
@ -338,7 +339,7 @@ export default class App {
this.sanitizer = new Sanitizer({ app: this, options })
this.nodes = new Nodes({
node_id: this.options.node_id,
angularMode: Boolean(options.angularMode),
forceNgOff: Boolean(options.forceNgOff),
})
this.observer = new Observer({ app: this, options })
this.ticker = new Ticker(this)
@ -935,11 +936,11 @@ export default class App {
const createListener = () =>
target
? createEventListener(target, type, listener, useCapture, this.options.angularMode)
? createEventListener(target, type, listener, useCapture, this.options.forceNgOff)
: null
const deleteListener = () =>
target
? deleteEventListener(target, type, listener, useCapture, this.options.angularMode)
? deleteEventListener(target, type, listener, useCapture, this.options.forceNgOff)
: null
this.attachStartCallback(createListener, useSafe)

View file

@ -10,11 +10,11 @@ export default class Nodes {
private readonly elementListeners: Map<number, Array<ElementListener>> = new Map()
private nextNodeId = 0
private readonly node_id: string
private readonly angularMode: boolean
private readonly forceNgOff: boolean
constructor(params: { node_id: string; angularMode: boolean }) {
constructor(params: { node_id: string; forceNgOff: boolean }) {
this.node_id = params.node_id
this.angularMode = params.angularMode
this.forceNgOff = params.forceNgOff
}
syntheticMode(frameOrder: number) {
@ -48,7 +48,7 @@ export default class Nodes {
if (id === undefined) {
return
}
createEventListener(node, type, listener, useCapture, this.angularMode)
createEventListener(node, type, listener, useCapture, this.forceNgOff)
let listeners = this.elementListeners.get(id)
if (listeners === undefined) {
listeners = []
@ -80,7 +80,7 @@ export default class Nodes {
if (listeners !== undefined) {
this.elementListeners.delete(id)
listeners.forEach((listener) =>
deleteEventListener(node, listener[0], listener[1], listener[2], this.angularMode),
deleteEventListener(node, listener[0], listener[1], listener[2], this.forceNgOff),
)
}
this.totalNodeAmount--

View file

@ -119,7 +119,7 @@ export default abstract class Observer {
}
this.commitNodes()
}) as MutationCallback,
this.app.options.angularMode,
this.app.options.forceNgOff,
)
}

View file

@ -99,7 +99,7 @@ export default function (app: App): void {
}
}
}) as MutationCallback,
app.options.angularMode,
app.options.forceNgOff,
)
app.attachStopCallback(() => {

View file

@ -95,15 +95,27 @@ export function canAccessIframe(iframe: HTMLIFrameElement) {
}
}
export function canAccessTarget(target: any) {
export function canAccessTarget(target: EventTarget): boolean {
try {
if (target.contentWindow) {
// If this property is inaccessible, it will throw due to cross-origin restrictions
return Boolean(target.contentWindow.location)
if (target instanceof HTMLIFrameElement) {
void target.contentDocument
} else if (target instanceof Window) {
void target.document
} else if (target instanceof Document) {
void target.defaultView
} else if ('nodeType' in target) {
void (target as Node).nodeType
} else if ('addEventListener' in target) {
void (target as EventTarget).addEventListener
}
return true
} catch (e) {
return false
if (e instanceof DOMException && e.name === 'SecurityError') {
return false
}
}
return true
}
function dec2hex(dec: number) {
@ -143,8 +155,8 @@ export function ngSafeBrowserMethod(method: string): string {
: method
}
export function createMutationObserver(cb: MutationCallback, angularMode?: boolean) {
if (angularMode) {
export function createMutationObserver(cb: MutationCallback, forceNgOff?: boolean) {
if (!forceNgOff) {
const mObserver = ngSafeBrowserMethod('MutationObserver') as 'MutationObserver'
return new window[mObserver](cb)
} else {
@ -157,21 +169,24 @@ export function createEventListener(
event: string,
cb: EventListenerOrEventListenerObject,
capture?: boolean,
angularMode?: boolean,
forceNgOff?: boolean,
) {
// we need to check if target is crossorigin frame or no and if we can access it
if (target instanceof HTMLIFrameElement && !canAccessIframe(target)) {
if (!canAccessTarget(target)) {
return
}
let safeAddEventListener: 'addEventListener'
if (angularMode) {
let safeAddEventListener = 'addEventListener' as unknown as 'addEventListener'
if (!forceNgOff) {
safeAddEventListener = ngSafeBrowserMethod('addEventListener') as 'addEventListener'
} else {
safeAddEventListener = 'addEventListener'
}
try {
target[safeAddEventListener](event, cb, capture)
target.addEventListener(event, cb, capture)
// parent has angular, but child frame don't
if (target[safeAddEventListener]) {
target[safeAddEventListener](event, cb, capture)
} else {
// @ts-ignore
target.addEventListener(event, cb, capture)
}
} catch (e) {
const msg = e.message
console.error(
@ -188,20 +203,22 @@ export function deleteEventListener(
event: string,
cb: EventListenerOrEventListenerObject,
capture?: boolean,
angularMode?: boolean,
forceNgOff?: boolean,
) {
if (target instanceof HTMLIFrameElement && !canAccessIframe(target)) {
if (!canAccessTarget(target)) {
return
}
let safeRemoveEventListener: 'removeEventListener'
if (angularMode) {
let safeRemoveEventListener = 'removeEventListener' as unknown as 'removeEventListener'
if (!forceNgOff) {
safeRemoveEventListener = ngSafeBrowserMethod('removeEventListener') as 'removeEventListener'
} else {
safeRemoveEventListener = 'removeEventListener'
}
try {
target[safeRemoveEventListener](event, cb, capture)
target.removeEventListener(event, cb, capture)
if (target[safeRemoveEventListener]) {
target[safeRemoveEventListener](event, cb, capture)
} else {
// @ts-ignore
target.removeEventListener(event, cb, capture)
}
} catch (e) {
const msg = e.message
console.error(