From 4d9c15ab5fb9bf13982d825d1ebc1b36957137b7 Mon Sep 17 00:00:00 2001 From: Rares Mihai Preda <54801398+rares-mihai-preda@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:44:43 +0300 Subject: [PATCH] Connatix Bid Adapter: support viewability (#12122) * implement viewability support openxBidAdapter remove PAF, bugfix ortbConverter response (#12105) Appnexus Bid Adapter: fix parse of the encoded string to check for ast_override_div argument (#12106) Richaudience Bid Adapter : add compability with DSA (#12099) * Update richaudienceBidAdapter.md Update maintainer e-mail to integrations@richaudience.com * Add richaudienceBidAdapter.js file * Add richaudienceBidAdapter_spec.js * Update richaudienceBidAdapter.js * RichaudienceBidAdapter add compability with DSA * RichaudienceBidAdapter add compability with DSA * RichaudienceBidAdapter add compability with DSA --------- Co-authored-by: Patrick McCann Co-authored-by: sergigimenez finish unit testing added container identifier for viewability on bid params Add new TGM adapter (#12100) Co-authored-by: apykhteyev Prebid 9.9.0 release Increment version to 9.10.0-pre deprecate old copper6 alias (#12112) LimelightDigital Adapter: Add support of ortb2 and ortb2Imp objects (#12078) * Add support of ortb2 and ortb2Imp objects * Fix tests * Fix tests --------- Co-authored-by: apykhteyev update configuration example (#12109) FPD Enrichment: Replace device values `w` and `h` with screen size; add `ext.vpw` and `ext.vph` (#12108) Core: fix broken native resizing (#12096) * Core: fix broken native resizing * fix test Weborama RTD Module: BUGFIX on user-centric profile validation (#12095) * update unit tests * fix code and doc * improve integration example * add extra test * improve logging * fix lint issues hotfix - after this I will fix all tests fix fix width and height fix iframe detection change detected viewability between 0 and 1 to be consistent with declared viewability improve the case case where the publisher doesn't send us a container id to check viewability fully covered by tests fix test that works local re-use percentInView method fix unit tests removed test Revert "removed test" This reverts commit 15dcab71e4ae7b863e49f384d368e067759b62b4. Anyclip Bid Adapter : refactor bid adapter (#12030) * update anyclip adapter * remove coppa * fix jsdoc warnings * create bidderUtils * delete duplicate code * refactor --------- Co-authored-by: Chucky-choo Digital Matter Bid Adapter: initial release (#12114) * update anyclip adapter * remove coppa * fix jsdoc warnings * create bidderUtils * delete duplicate code * refactor * add digitalMatter Bid Adapter --------- Co-authored-by: Chucky-choo IX Bid Adapter: Remove client FT pbjs_allow_all_eids (#12117) Co-authored-by: Sajid Mahmood SeedingAlliance Adapter: rework to properly use openRTB standard internally (#12101) * rework adapter to only use openRTB data internally * added comments * fix lint errors saambaaBidAdapter.js: make alias of advangelist (#11992) * saambaaBidAdapter.js: reuse repeated code block * Update saambaaBidAdapter.js * Update advangelistsBidAdapter.js * Update saambaaBidAdapter.js * Update advangelistsBidAdapter.js * Update saambaaBidAdapter.js * Update advangelistsBidAdapter.js * Create index.js * Update saambaaBidAdapter.js * Update advangelistsBidAdapter.js * Update index.js * Update advangelistsBidAdapter.js * Update saambaaBidAdapter.js * Update index.js * Update advangelistsBidAdapter.js * Update saambaaBidAdapter.js * Update index.js * Update advangelistsBidAdapter.js * Update saambaaBidAdapter.js * Update advangelistsBidAdapter.js * Delete modules/saambaaBidAdapter.js * Update advangelistsBidAdapter.js * Update beachfrontBidAdapter.js * Update beachfrontBidAdapter.js * Update beachfrontBidAdapter.js * Update nextrollBidAdapter.js * Update beachfrontBidAdapter.js * Update index.js * Update advangelistsBidAdapter.js * Update beachfrontBidAdapter.js * Create saambaaBidAdapter.js * fix file name --------- Co-authored-by: Chris Huie Prebid 9.10.0 release Increment version to 9.11.0-pre Core: make sure adUnitCodes are unique in auction events (#12127) Mobian RTD provider: update API endpoint (#12121) * update API endpoint for mobian RTD provider * lint mobian RTD provider Initial Commit for Symitri Analytics Adapter (#12132) Co-authored-by: Manan Djax Bid Adapter : initial release (#12120) * Djax bid adapter files added * fix linting issue * Linting issue fixed * Update djaxBidAdapter.js * Update djaxBidAdapter_spec.js * Update djaxBidAdapter_spec.js --------- Co-authored-by: Chris Huie Co-authored-by: Patrick McCann Preciso : Added new library to remove code duplication in bid adapter (#11868) * Preciso : Added new library to remove code duplication in bid adapter * modified bidfloor mapping logic * error fix * error fix * import bidUtils.js in IdxBidadapter to reduce the code duplicataion * import bidUtils.js in IdxBidadapter to reduce the code duplicataion * Error fix * Imported bidUtils library into redtram bid adapter * import common library in mediabramaBidAdapter * import common library in loganBidAdapter to remove code duplication * removed storageManaser from library * renderer.js changes reverted 33across - allow aliasing (#12138) Updating isBidRequestValid logic to account for aliasing of bidder names (#12136) Digitalmatter Bid Adapter : add dichange alias (#12133) * add alias * fix alias --------- Co-authored-by: Chucky-choo add media consortium adapter (#11892) Co-authored-by: Maxime Lequain UserID: merge EIDs with first party data (#12110) * ortbConverter: do not override EIDS provided as first party data * update tests * Allow bidder-specific FPD enrichments * Refactor eid generation, add primaryIds * EIDs as FPD * Fix tests * PBS tests, fix multiple hook registration * Fix more test cleanup * remove eidPermissions * refactor oderByPriority * refactor initializedSubmodules * fix lint * update id5 primaryIds * simplify eid source filtering * clean up PBS userId logic * Revert "Allow bidder-specific FPD enrichments" This reverts commit 2fb74525a5d527e378b48e966769283e2256a9ea. * undo bidder-specific enrichments * use startAuction instead of requestBids * add test on userIdAsEids * Fix lint Bump ws, @wdio/browserstack-service, @wdio/cli and @wdio/local-runner (#12148) Bumps [ws](https://github.com/websockets/ws) to 8.17.1 and updates ancestor dependencies [ws](https://github.com/websockets/ws), [@wdio/browserstack-service](https://github.com/webdriverio/webdriverio/tree/HEAD/packages/wdio-browserstack-service), [@wdio/cli](https://github.com/webdriverio/webdriverio/tree/HEAD/packages/wdio-cli) and [@wdio/local-runner](https://github.com/webdriverio/webdriverio/tree/HEAD/packages/wdio-local-runner). These dependencies need to be updated together. Updates `ws` from 8.13.0 to 8.17.1 - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.13.0...8.17.1) Updates `@wdio/browserstack-service` from 8.39.0 to 9.0.5 - [Release notes](https://github.com/webdriverio/webdriverio/releases) - [Changelog](https://github.com/webdriverio/webdriverio/blob/main/CHANGELOG.md) - [Commits](https://github.com/webdriverio/webdriverio/commits/v9.0.5/packages/wdio-browserstack-service) Updates `@wdio/cli` from 8.39.0 to 9.0.5 - [Release notes](https://github.com/webdriverio/webdriverio/releases) - [Changelog](https://github.com/webdriverio/webdriverio/blob/main/CHANGELOG.md) - [Commits](https://github.com/webdriverio/webdriverio/commits/v9.0.5/packages/wdio-cli) Updates `@wdio/local-runner` from 8.39.0 to 9.0.5 - [Release notes](https://github.com/webdriverio/webdriverio/releases) - [Changelog](https://github.com/webdriverio/webdriverio/blob/main/CHANGELOG.md) - [Commits](https://github.com/webdriverio/webdriverio/commits/v9.0.5/packages/wdio-local-runner) --- updated-dependencies: - dependency-name: ws dependency-type: indirect - dependency-name: "@wdio/browserstack-service" dependency-type: direct:development - dependency-name: "@wdio/cli" dependency-type: direct:development - dependency-name: "@wdio/local-runner" dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Smartadserver Bid Adapter : add DSA support (#12141) * add support of dsa * restore topics * DSA fix for UT Change consent default param name (#12154) Co-authored-by: Danijel Ristic Freedom Ad Network Bid Adapter: initial release (#12153) * Create fanAdapter.md * Create fanAdapter.js * Create fanAdapter_spec.js * Update fanAdapter_spec.js * Update fanAdapter.js * Update fanAdapter.js * Update fanAdapter_spec.js * Update fanAdapter.js * Update fanAdapter_spec.js * Update fanAdapter.js * Update fanAdapter_spec.js * Update fanAdapter.js * Update fanAdapter_spec.js * Update fanAdapter.js * Update fanAdapter.js deleted bidder name check (#12157) Co-authored-by: Octavia Suceava Connatix Bid Adapter : support eids (#12142) * add eids on request * change naming --------- Co-authored-by: Darian Avasan Co-authored-by: Octavia Suceava PrebidServer Bid Adapter : update to use gloablly defined alias or s2sConfig defined alias (#12159) * update to use gloablly defined alias or s2sConfig defined alias * Add test case --------- Co-authored-by: Demetrio Girardi * fix build * test if can fix * Revert "test if can fix" This reverts commit 221f612e2aebe18228b91407957190e58b25d617. * remove test * refactoring --------- Co-authored-by: Demetrio Girardi --- modules/connatixBidAdapter.js | 86 +++++- test/spec/modules/connatixBidAdapter_spec.js | 285 ++++++++++++++++++- 2 files changed, 369 insertions(+), 2 deletions(-) diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js index 925ddebe099..7a110a59107 100644 --- a/modules/connatixBidAdapter.js +++ b/modules/connatixBidAdapter.js @@ -2,12 +2,17 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { percentInView } from '../libraries/percentInView/percentInView.js'; + import { deepAccess, isFn, logError, isArray, formatQS, + getWindowTop, + isNumber, + isStr, deepSetValue } from '../src/utils.js'; @@ -62,6 +67,76 @@ export function validateVideo(mediaTypes) { return video.context !== ADPOD; } +export function _getMinSize(sizes) { + if (!sizes || sizes.length === 0) return undefined; + return sizes.reduce((minSize, currentSize) => { + const minArea = minSize.w * minSize.h; + const currentArea = currentSize.w * currentSize.h; + return currentArea < minArea ? currentSize : minSize; + }); +} + +export function _canSelectViewabilityContainer() { + try { + window.top.document.querySelector('#viewability-container'); + return true; + } catch (e) { + return false; + } +} + +export function _isViewabilityMeasurable(element) { + if (!element) return false; + return _canSelectViewabilityContainer(element); +} + +export function _getViewability(element, topWin, { w, h } = {}) { + return topWin.document.visibilityState === 'visible' + ? percentInView(element, topWin, { w, h }) + : 0; +} + +export function detectViewability(bid) { + const { params, adUnitCode } = bid; + + const viewabilityContainerIdentifier = params.viewabilityContainerIdentifier; + + let element = null; + let bidParamSizes = null; + let minSize = []; + + if (isStr(viewabilityContainerIdentifier)) { + try { + element = document.querySelector(viewabilityContainerIdentifier) || window.top.document.querySelector(viewabilityContainerIdentifier); + if (element) { + bidParamSizes = [element.offsetWidth, element.offsetHeight]; + minSize = _getMinSize(bidParamSizes) + } + } catch (e) { + logError(`Error while trying to find viewability container element: ${viewabilityContainerIdentifier}`); + } + } + + if (!element) { + // Get the sizes from the mediaTypes object if it exists, otherwise use the sizes array from the bid object + bidParamSizes = bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes ? bid.mediaTypes.banner.sizes : bid.sizes; + bidParamSizes = typeof bidParamSizes === 'undefined' && bid.mediaType && bid.mediaType.video && bid.mediaType.video.playerSize ? bid.mediaType.video.playerSize : bidParamSizes; + bidParamSizes = typeof bidParamSizes === 'undefined' && bid.mediaType && bid.mediaType.video && isNumber(bid.mediaType.video.w) && isNumber(bid.mediaType.h) ? [bid.mediaType.video.w, bid.mediaType.video.h] : bidParamSizes; + minSize = _getMinSize(bidParamSizes ?? []) + element = document.getElementById(adUnitCode); + } + + if (_isViewabilityMeasurable(element)) { + const minSizeObj = { + w: minSize[0], + h: minSize[1] + } + return Math.round(_getViewability(element, getWindowTop(), minSizeObj)) + } + + return null; +} + /** * Get ids from Prebid User ID Modules and add them to the payload */ @@ -91,9 +166,10 @@ export const spec = { const hasMediaTypes = Boolean(mediaTypes) && (Boolean(mediaTypes[BANNER]) || Boolean(mediaTypes[VIDEO])); const isValidBanner = validateBanner(mediaTypes); const isValidVideo = validateVideo(mediaTypes); + const isValidViewability = typeof params.viewabilityPercentage === 'undefined' || (isNumber(params.viewabilityPercentage) && params.viewabilityPercentage >= 0 && params.viewabilityPercentage <= 1); const hasRequiredBidParams = Boolean(params.placementId); - const isValid = hasBidId && hasMediaTypes && isValidBanner && isValidVideo && hasRequiredBidParams; + const isValid = hasBidId && hasMediaTypes && isValidBanner && isValidVideo && hasRequiredBidParams && isValidViewability; if (!isValid) { logError( `Invalid bid request: @@ -120,10 +196,18 @@ export const spec = { params, sizes, } = bid; + + let detectedViewabilityPercentage = detectViewability(bid); + if (isNumber(detectedViewabilityPercentage)) { + detectedViewabilityPercentage = detectedViewabilityPercentage / 100; + } + return { bidId, mediaTypes, sizes, + detectedViewabilityPercentage, + declaredViewabilityPercentage: bid.params.viewabilityPercentage ?? null, placementId: params.placementId, floor: getBidFloor(bid), }; diff --git a/test/spec/modules/connatixBidAdapter_spec.js b/test/spec/modules/connatixBidAdapter_spec.js index d2e8ea63cbe..e9718136052 100644 --- a/test/spec/modules/connatixBidAdapter_spec.js +++ b/test/spec/modules/connatixBidAdapter_spec.js @@ -1,7 +1,13 @@ import { expect } from 'chai'; +import sinon from 'sinon'; import { spec, - getBidFloor as connatixGetBidFloor + detectViewability as connatixDetectViewability, + getBidFloor as connatixGetBidFloor, + _getMinSize as connatixGetMinSize, + _isViewabilityMeasurable as connatixIsViewabilityMeasurable, + _canSelectViewabilityContainer as connatixCanSelectViewabilityContainer, + _getViewability as connatixGetViewability, } from '../../../modules/connatixBidAdapter.js'; import { ADPOD, BANNER, VIDEO } from '../../../src/mediaTypes.js'; @@ -44,6 +50,283 @@ describe('connatixBidAdapter', function () { bid.mediaTypes = mediaTypes; } + describe('connatixGetMinSize', () => { + it('should return the smallest size based on area', () => { + const sizes = [ + { w: 300, h: 250 }, + { w: 728, h: 90 }, + { w: 160, h: 600 } + ]; + const result = connatixGetMinSize(sizes); + expect(result).to.deep.equal({ w: 728, h: 90 }); + }); + + it('should handle an array with one size', () => { + const sizes = [{ w: 300, h: 250 }]; + const result = connatixGetMinSize(sizes); + expect(result).to.deep.equal({ w: 300, h: 250 }); + }); + + it('should handle empty array', () => { + const sizes = []; + const result = connatixGetMinSize(sizes); + expect(result).to.be.undefined; + }); + }); + + describe('_isIframe', () => { + let querySelectorStub; + + beforeEach(() => { + querySelectorStub = sinon.stub(window.top.document, 'querySelector'); + }); + + afterEach(() => { + querySelectorStub.restore(); + }); + + it('should return true when window.top.document.querySelector does not throw an error', () => { + querySelectorStub.returns({}); + expect(connatixCanSelectViewabilityContainer()).to.be.true; + }); + + it('should return false when window.top.document.querySelector throws an error', () => { + querySelectorStub.throws(new Error('test error')); + expect(connatixCanSelectViewabilityContainer()).to.be.false; + }); + }); + + describe('_isViewabilityMeasurable', () => { + let querySelectorStub; + + beforeEach(() => { + querySelectorStub = sinon.stub(window.top.document, 'querySelector'); + }); + + afterEach(() => { + querySelectorStub.restore(); + }); + + it('should return false if the element is null or undefined', () => { + expect(connatixIsViewabilityMeasurable(null)).to.be.false; + expect(connatixIsViewabilityMeasurable(undefined)).to.be.false; + }); + + it('should return false if _isIframe returns true', () => { + querySelectorStub.throws(new Error('test error')); + + const element = document.createElement('div'); + expect(connatixIsViewabilityMeasurable(element)).to.be.false; + }); + + it('should return true if _isIframe returns false', () => { + querySelectorStub.returns(document.createElement('div')) + + const element = document.createElement('div'); + expect(connatixIsViewabilityMeasurable(element)).to.be.true; + }); + }); + + describe('_getViewability', () => { + let element; + let getBoundingClientRectStub; + let topWinMock; + + beforeEach(() => { + element = document.createElement('div'); + getBoundingClientRectStub = sinon.stub(element, 'getBoundingClientRect'); + + topWinMock = { + document: { + visibilityState: 'visible' + }, + innerWidth: 800, + innerHeight: 600 + }; + }); + + afterEach(() => { + getBoundingClientRectStub.restore(); + }); + + it('should return 0 if the document is not visible', () => { + topWinMock.document.visibilityState = 'hidden'; + + const viewability = connatixGetViewability(element, topWinMock); + + expect(viewability).to.equal(0); + }); + + it('should return 100% if the element is fully in view', () => { + const boundingBox = { left: 100, top: 100, right: 300, bottom: 300, width: 200, height: 200 }; + getBoundingClientRectStub.returns(boundingBox); + + const viewability = connatixGetViewability(element, topWinMock); + + expect(viewability).to.equal(100); + }); + + it('should return the correct percentage if the element is partially in view', () => { + const boundingBox = { left: 700, top: 500, right: 900, bottom: 700, width: 200, height: 200 }; + getBoundingClientRectStub.returns(boundingBox); + + const viewability = connatixGetViewability(element, topWinMock); + + expect(viewability).to.equal(25); // 100x100 / 200x200 = 0.25 -> 25% + }); + + it('should return 0% if the element is not in view', () => { + const boundingBox = { left: 900, top: 700, right: 1100, bottom: 900, width: 200, height: 200 }; + getBoundingClientRectStub.returns(boundingBox); + + const viewability = connatixGetViewability(element, topWinMock); + + expect(viewability).to.equal(0); + }); + + it('should use provided width and height if element dimensions are zero', () => { + const boundingBox = { left: 100, top: 100, right: 100, bottom: 100, width: 0, height: 0 }; + getBoundingClientRectStub.returns(boundingBox); + + const dimensions = { w: 200, h: 200 }; + const viewability = connatixGetViewability(element, topWinMock, dimensions); + + expect(viewability).to.equal(100); // Element fully in view with provided dimensions + }); + }); + + describe('detectViewability', () => { + let element; + let getBoundingClientRectStub; + let topWinMock; + let querySelectorStub; + let getElementByIdStub; + + beforeEach(() => { + element = document.createElement('div'); + getBoundingClientRectStub = sinon.stub(element, 'getBoundingClientRect'); + + topWinMock = { + document: { + visibilityState: 'visible' + }, + innerWidth: 800, + innerHeight: 600 + }; + + querySelectorStub = sinon.stub(window.top.document, 'querySelector'); + getElementByIdStub = sinon.stub(document, 'getElementById'); + }); + + afterEach(() => { + getBoundingClientRectStub.restore(); + querySelectorStub.restore(); + getElementByIdStub.restore(); + }); + + it('should return 100% viewability when the element is fully within view and has a valid viewabilityContainerIdentifier', () => { + const bid = { + params: { viewabilityContainerIdentifier: '#validElement' }, + adUnitCode: 'adUnitCode123', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + sizes: [[300, 250]] + }; + + getBoundingClientRectStub.returns({ + left: 100, + top: 100, + right: 400, + bottom: 350, + width: 300, + height: 250 + }); + + querySelectorStub.withArgs('#validElement').returns(element); + getElementByIdStub.returns(null); + + const result = connatixDetectViewability(bid); + + // Expected calculation: the element is fully in view, so 100% viewability + expect(result).to.equal(100); + }); + + it('should fall back to using bid sizes and adUnitCode when the viewabilityContainerIdentifier is invalid or was not provided', () => { + const bid = { + params: { viewabilityContainerIdentifier: '#invalidElement' }, + adUnitCode: 'adUnitCode123', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + sizes: [[300, 250]] + }; + + getBoundingClientRectStub.returns({ + left: 200, + top: 100, + right: 500, + bottom: 350, + width: 300, + height: 250 + }); + + querySelectorStub.withArgs('#invalidElement').returns(null); + getElementByIdStub.withArgs('adUnitCode123').returns(element); + + const result = connatixDetectViewability(bid); + + expect(result).to.equal(100); // Full viewability + }); + + it('should use the adUnitCode as a fallback when querying an element fails due to a browser error, and return 100% viewability because adUnitCode container is fully in view', () => { + const bid = { + params: { viewabilityContainerIdentifier: '#invalidElement' }, + adUnitCode: 'adUnitCode123', + sizes: [[300, 250]] + }; + + // Simulate an error when querying the element + querySelectorStub.withArgs('#invalidElement').throws(new Error('Query failed')); + + getBoundingClientRectStub.returns({ + left: 100, + top: 100, + right: 400, + bottom: 350, + width: 300, + height: 250 + }); + + // The fallback should use the adUnitCode to find the element + getElementByIdStub.withArgs('adUnitCode123').returns(element); + + const result = connatixDetectViewability(bid); + + expect(result).to.equal(100); // Expect the fallback to work and return 100% viewability + }); + + it('should return null when querying the element by the provided identifier fails and the adUnitCode viewability container is unavailable', () => { + const bid = { + params: { viewabilityContainerIdentifier: '#invalidElement' }, + adUnitCode: 'adUnitCode123', + sizes: [[300, 250]] + }; + + // Simulate an error when querying the element + querySelectorStub.withArgs('#invalidElement').throws(new Error('Query failed')); + + getBoundingClientRectStub.returns({ + left: 100, + top: 100, + right: 400, + bottom: 350, + width: 300, + height: 250 + }); + + const result = connatixDetectViewability(bid); + + expect(result).to.equal(null); + }); + }); + describe('isBidRequestValid', function () { this.beforeEach(function () { bid = mockBidRequest();