From ebf7aec953584f3d208be6ddb3f1cc8c5b7f5c85 Mon Sep 17 00:00:00 2001 From: james huffaker Date: Thu, 25 Apr 2024 15:52:05 -0700 Subject: [PATCH] Add config to allow excluding iframes from the ttvc measurement, include by default --- src/inViewportImageObserver.ts | 6 +++++- src/inViewportMutationObserver.ts | 5 +++++ src/util/constants.ts | 4 ++++ test/e2e/iframe4/index.html | 18 ++++++++++++++++++ test/e2e/iframe4/index.spec.ts | 20 ++++++++++++++++++++ test/server/public/analytics-no-iframe.js | 23 +++++++++++++++++++++++ 6 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 test/e2e/iframe4/index.html create mode 100644 test/e2e/iframe4/index.spec.ts create mode 100644 test/server/public/analytics-no-iframe.js diff --git a/src/inViewportImageObserver.ts b/src/inViewportImageObserver.ts index f87564d..0b30bb9 100644 --- a/src/inViewportImageObserver.ts +++ b/src/inViewportImageObserver.ts @@ -1,4 +1,5 @@ import {Logger} from './util/logger'; +import {CONFIG} from './util/constants'; /** * Modeled after IntersectionObserver and MutationObserver, this Image observer @@ -33,6 +34,9 @@ export class InViewportImageObserver { private intersectionObserverCallback = (entries: IntersectionObserverEntry[]) => { entries.forEach((entry) => { const img = entry.target as HTMLImageElement | HTMLIFrameElement; + if (CONFIG.EXCLUDE_IFRAME && entry.target instanceof HTMLIFrameElement) { + return; + } const timestamp = this.imageLoadTimes.get(img); if (entry.isIntersecting && timestamp != null) { Logger.info('InViewportImageObserver.callback()', '::', 'timestamp =', timestamp); @@ -44,7 +48,7 @@ export class InViewportImageObserver { }; private handleLoadOrErrorEvent = (event: Event) => { - if (event.target instanceof HTMLImageElement || event.target instanceof HTMLIFrameElement) { + if (event.target instanceof HTMLImageElement || (!CONFIG.EXCLUDE_IFRAME && event.target instanceof HTMLIFrameElement)) { Logger.debug('InViewportImageObserver.handleLoadOrErrorEvent()', '::', 'event =', event); this.imageLoadTimes.set(event.target, event.timeStamp); this.intersectionObserver.observe(event.target); diff --git a/src/inViewportMutationObserver.ts b/src/inViewportMutationObserver.ts index 44b2d31..7255a48 100644 --- a/src/inViewportMutationObserver.ts +++ b/src/inViewportMutationObserver.ts @@ -1,4 +1,5 @@ import {Logger} from './util/logger'; +import {CONFIG} from './util/constants'; export type InViewportMutationObserverCallback = (mutation: TimestampedMutationRecord) => void; export type TimestampedMutationRecord = MutationRecord & {timestamp?: number}; @@ -69,6 +70,10 @@ export class InViewportMutationObserver { return; } + if (CONFIG.EXCLUDE_IFRAME && target instanceof HTMLIFrameElement) { + return; + } + switch (mutation.type) { case 'childList': mutation.addedNodes.forEach((node) => { diff --git a/src/util/constants.ts b/src/util/constants.ts index 90f18aa..daaebab 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -2,6 +2,7 @@ export type TtvcOptions = { debug?: boolean; idleTimeout?: number; networkTimeout?: number; + excludeIframe?: boolean; }; /** ttvc configuration values set during initialization */ @@ -19,10 +20,13 @@ export const CONFIG = { * If NETWORK_TIMEOUT is set to 0, disable this feature. */ NETWORK_TIMEOUT: 60000, + + EXCLUDE_IFRAME: false, }; export const setConfig = (options?: TtvcOptions) => { if (options?.debug) CONFIG.DEBUG = options.debug; if (options?.idleTimeout) CONFIG.IDLE_TIMEOUT = options.idleTimeout; if (options?.networkTimeout) CONFIG.NETWORK_TIMEOUT = options.networkTimeout; + if (options?.excludeIframe) CONFIG.EXCLUDE_IFRAME = options.excludeIframe; }; diff --git a/test/e2e/iframe4/index.html b/test/e2e/iframe4/index.html new file mode 100644 index 0000000..5708641 --- /dev/null +++ b/test/e2e/iframe4/index.html @@ -0,0 +1,18 @@ + + + + + + +

Hello world!

+ + + \ No newline at end of file diff --git a/test/e2e/iframe4/index.spec.ts b/test/e2e/iframe4/index.spec.ts new file mode 100644 index 0000000..2322928 --- /dev/null +++ b/test/e2e/iframe4/index.spec.ts @@ -0,0 +1,20 @@ +import {test, expect} from '@playwright/test'; + +import {getEntriesAndErrors} from '../../util/entries'; + +const PAGELOAD_DELAY = 200; +const IFRAME_DELAY = 500; + +test.describe('TTVC', () => { + test('a static document with an iframe', async ({page}) => { + await page.goto(`/test/iframe4?delay=${PAGELOAD_DELAY}`, { + waitUntil: 'networkidle', + }); + + const {entries} = await getEntriesAndErrors(page); + + expect(entries.length).toBe(1); + expect(entries[0].duration).toBeGreaterThanOrEqual(PAGELOAD_DELAY); + expect(entries[0].duration).toBeLessThan(PAGELOAD_DELAY + IFRAME_DELAY); + }); +}); diff --git a/test/server/public/analytics-no-iframe.js b/test/server/public/analytics-no-iframe.js new file mode 100644 index 0000000..b6e718c --- /dev/null +++ b/test/server/public/analytics-no-iframe.js @@ -0,0 +1,23 @@ +// use window.entries and window.errors to communicate between browser and test runner processes +window.entries = []; +window.errors = []; + +// patch window.fetch +const old2Fetch = window.fetch; +window.fetch = (...args) => { + TTVC.incrementAjaxCount(); + return old2Fetch(...args).finally(TTVC.decrementAjaxCount); +}; + +TTVC.init({debug: true, networkTimeout: window.NETWORK_TIMEOUT ?? 3000, excludeIframe: true}); + +TTVC.onTTVC( + (measurement) => { + console.log('TTVC:SUCCESS', measurement); + window.entries.push(measurement); + }, + (error) => { + console.log('TTVC:ERROR', error); + window.errors.push(error); + } +);