From 1cf1137c7d5dfb023a6b5fcb1ac9630518bfcdec Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 2 Oct 2024 09:47:36 +0200 Subject: [PATCH] fixing iframe issues... --- tracker/tracker/src/main/app/canvas.ts | 6 +- tracker/tracker/src/main/app/index.ts | 232 +++++++++++++++---------- 2 files changed, 142 insertions(+), 96 deletions(-) diff --git a/tracker/tracker/src/main/app/canvas.ts b/tracker/tracker/src/main/app/canvas.ts index e96b80c72..f8e7d2e89 100644 --- a/tracker/tracker/src/main/app/canvas.ts +++ b/tracker/tracker/src/main/app/canvas.ts @@ -35,10 +35,8 @@ class CanvasRecorder { startTracking() { setTimeout(() => { this.app.nodes.scanTree(this.captureCanvas) - this.app.nodes.attachNodeCallback((node: Node): void => { - this.captureCanvas(node) - }) - }, 500) + this.app.nodes.attachNodeCallback(this.captureCanvas) + }, 250) } restartTracking = () => { diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index ab755b9ab..47193440a 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -191,6 +191,8 @@ const proto = { iframeId: 'never-gonna-say-goodbye', // batch of messages from an iframe window iframeBatch: 'never-gonna-tell-a-lie-and-hurt-you', + // signal that parent is live + parentAlive: 'i-dont-know-more-lines', } as const export default class App { @@ -351,7 +353,75 @@ export default class App { this.initWorker() const thisTab = this.session.getTabId() + const catchParentMessage = (event: MessageEvent) => { + const { data } = event + if (!data) return + if (data.line === proto.parentAlive) { + this.parentActive = true + } + if (data.line === proto.iframeId) { + this.parentActive = true + this.rootId = data.id + this.session.setSessionToken(data.token as string) + this.frameOderNumber = data.frameOrderNumber + this.debug.log('starting iframe tracking', data) + this.allowAppStart() + } + } + window.addEventListener('message', catchParentMessage) + this.attachStopCallback(() => { + window.removeEventListener('message', catchParentMessage) + }) + + if (this.bc !== null) { + this.bc.postMessage({ + line: proto.ask, + source: thisTab, + context: this.contextId, + }) + this.startTimeout = setTimeout(() => { + this.allowAppStart() + }, 500) + this.bc.onmessage = (ev: MessageEvent) => { + if (ev.data.context === this.contextId) { + return + } + if (ev.data.line === proto.resp) { + const sessionToken = ev.data.token + this.session.setSessionToken(sessionToken) + this.allowAppStart() + } + if (ev.data.line === proto.reg) { + const sessionToken = ev.data.token + this.session.regenerateTabId() + this.session.setSessionToken(sessionToken) + this.allowAppStart() + } + if (ev.data.line === proto.ask) { + const token = this.session.getSessionToken() + if (token && this.bc) { + this.bc.postMessage({ + line: ev.data.source === thisTab ? proto.reg : proto.resp, + token, + source: thisTab, + context: this.contextId, + }) + } + } + } + } + } + + /** used by child iframes for crossdomain only */ + parentActive = false + checkStatus = () => { + return this.parentActive + } + /** used by child iframes for crossdomain only */ + + /** track app instances in crossdomain child iframes */ + crossdomainIframesModule = () => { if (!this.insideIframe) { /** * if we get a signal from child iframes, we check for their node_id and send it back, @@ -362,36 +432,32 @@ export default class App { const { data } = event if (!data) return if (data.line === proto.iframeSignal) { - const childIframeDomain = data.domain + // @ts-ignore + event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*') + const childIframeDomain = data.domain as string const pageIframes = Array.from(document.querySelectorAll('iframe')) this.pageFrames = pageIframes const signalId = async () => { - let tries = 0 - while (tries < 10) { - const id = this.checkNodeId(pageIframes, childIframeDomain) - if (id) { - this.waitStarted() - .then(() => { - crossdomainFrameCount++ - const token = this.session.getSessionToken() - const iframeData = { - line: proto.iframeId, - context: this.contextId, - domain: childIframeDomain, - id, - token, - frameOrderNumber: crossdomainFrameCount, - } - this.debug.log('iframe_data', iframeData) - // @ts-ignore - event.source?.postMessage(iframeData, '*') - }) - .catch(console.error) - tries = 10 - break + const id = await this.checkNodeId(pageIframes, childIframeDomain) + if (id) { + try { + await this.waitStarted() + crossdomainFrameCount++ + const token = this.session.getSessionToken() + const iframeData = { + line: proto.iframeId, + context: this.contextId, + domain: childIframeDomain, + id, + token, + frameOrderNumber: crossdomainFrameCount, + } + this.debug.log('iframe_data', iframeData) + // @ts-ignore + event.source?.postMessage(iframeData, '*') + } catch (e) { + console.error(e) } - tries++ - await delay(100) } } void signalId() @@ -451,26 +517,13 @@ export default class App { this.attachStopCallback(() => { window.removeEventListener('message', catchIframeMessage) }) - } else { - const catchParentMessage = (event: MessageEvent) => { - const { data } = event - if (!data) return - if (data.line !== proto.iframeId) { - return - } - this.rootId = data.id - this.session.setSessionToken(data.token as string) - this.frameOderNumber = data.frameOrderNumber - this.debug.log('starting iframe tracking', data) - this.allowAppStart() - } - window.addEventListener('message', catchParentMessage) - this.attachStopCallback(() => { - window.removeEventListener('message', catchParentMessage) - }) - // communicating with parent window, - // even if its crossdomain is possible via postMessage api - const domain = this.initialHostName + } + } + + signalIframeTracker = () => { + const domain = this.initialHostName + const thisTab = this.session.getTabId() + const signalToParent = (n: number) => { window.parent.postMessage( { line: proto.iframeSignal, @@ -478,47 +531,15 @@ export default class App { context: this.contextId, domain, }, - '*', + this.options.crossdomain?.parentDomain ?? '*', ) + setTimeout(() => { + if (!this.checkStatus() && n < 100) { + void signalToParent(n + 1) + } + }, 250) } - - if (this.bc !== null) { - this.bc.postMessage({ - line: proto.ask, - source: thisTab, - context: this.contextId, - }) - this.startTimeout = setTimeout(() => { - this.allowAppStart() - }, 500) - this.bc.onmessage = (ev: MessageEvent) => { - if (ev.data.context === this.contextId) { - return - } - if (ev.data.line === proto.resp) { - const sessionToken = ev.data.token - this.session.setSessionToken(sessionToken) - this.allowAppStart() - } - if (ev.data.line === proto.reg) { - const sessionToken = ev.data.token - this.session.regenerateTabId() - this.session.setSessionToken(sessionToken) - this.allowAppStart() - } - if (ev.data.line === proto.ask) { - const token = this.session.getSessionToken() - if (token && this.bc) { - this.bc.postMessage({ - line: ev.data.source === thisTab ? proto.reg : proto.resp, - token, - source: thisTab, - context: this.contextId, - }) - } - } - } - } + void signalToParent(1) } startTimeout: ReturnType | null = null @@ -530,15 +551,35 @@ export default class App { } } - private checkNodeId(iframes: HTMLIFrameElement[], domain: string) { + private async checkNodeId(iframes: HTMLIFrameElement[], domain: string): Promise { for (const iframe of iframes) { if (iframe.dataset.domain === domain) { - // @ts-ignore - return iframe[this.options.node_id] as number | undefined + /** + * Here we're trying to get node id from the iframe (which is kept in observer) + * because of async nature of dom initialization, we give 100 retries with 100ms delay each + * which equals to 10 seconds. This way we have a period where we give app some time to load + * and tracker some time to parse the initial DOM tree even on slower devices + * */ + let tries = 0 + while (tries < 100) { + // @ts-ignore + const potentialId = iframe[this.options.node_id] + if (potentialId !== undefined) { + tries = 100 + return potentialId + } else { + tries++ + await delay(100) + } + } + + return null } } + return null } + private initWorker() { try { this.worker = new Worker( @@ -660,7 +701,7 @@ export default class App { messages: this.messages, domain: this.initialHostName, }, - '*', + this.options.crossdomain?.parentDomain ?? '*', ) this.commitCallbacks.forEach((cb) => cb(this.messages)) this.messages.length = 0 @@ -756,7 +797,6 @@ export default class App { this.stopCallbacks.push(cb) } - // Use app.nodes.attachNodeListener for registered nodes instead attachEventListener( target: EventTarget, type: string, @@ -1327,6 +1367,9 @@ export default class App { } await this.tagWatcher.fetchTags(this.options.ingestPoint, token) this.activityState = ActivityState.Active + if (this.options.crossdomain?.enabled || this.insideIframe) { + this.crossdomainIframesModule() + } if (canvasEnabled && !this.options.canvas.disableCanvas) { this.canvasRecorder = @@ -1338,7 +1381,6 @@ export default class App { fixedScaling: this.options.canvas.fixedCanvasScaling, useAnimationFrame: this.options.canvas.useAnimationFrame, }) - this.canvasRecorder.startTracking() } /** --------------- COLD START BUFFER ------------------*/ @@ -1361,9 +1403,12 @@ export default class App { } this.ticker.start() } + this.canvasRecorder?.startTracking() if (this.features['usability-test']) { - this.uxtManager = this.uxtManager ? this.uxtManager : new UserTestManager(this, uxtStorageKey) + this.uxtManager = this.uxtManager + ? this.uxtManager + : new UserTestManager(this, uxtStorageKey) let uxtId: number | undefined const savedUxtTag = this.localStorage.getItem(uxtStorageKey) if (savedUxtTag) { @@ -1471,6 +1516,9 @@ export default class App { * and here we just apply 10ms delay just in case * */ async start(...args: Parameters): Promise { + if (this.insideIframe) { + this.signalIframeTracker() + } if ( this.activityState === ActivityState.Active || this.activityState === ActivityState.Starting