From 392a6f5bf3fc308bfdd69f58454e25a8966b18a4 Mon Sep 17 00:00:00 2001 From: Alexandru Capatina Date: Thu, 4 Jan 2024 16:59:39 +0200 Subject: [PATCH 1/5] OMS Adapter: add new adapter --- modules/omsBidAdapter.js | 321 ++++++++++++++++++ modules/omsBidAdapter.md | 46 +++ test/spec/modules/omsBidAdapter_spec.js | 411 ++++++++++++++++++++++++ 3 files changed, 778 insertions(+) create mode 100644 modules/omsBidAdapter.js create mode 100644 modules/omsBidAdapter.md create mode 100644 test/spec/modules/omsBidAdapter_spec.js diff --git a/modules/omsBidAdapter.js b/modules/omsBidAdapter.js new file mode 100644 index 00000000000..1f099372595 --- /dev/null +++ b/modules/omsBidAdapter.js @@ -0,0 +1,321 @@ +import { + isArray, + getWindowTop, + getUniqueIdentifierStr, + deepSetValue, + logError, + logWarn, + createTrackPixelHtml, + getWindowSelf, + isFn, + isPlainObject, getBidIdParameter, +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {ajax} from '../src/ajax.js'; + +const BIDDER_CODE = 'oms'; +const URL = 'https://rt.marphezis.com/hb'; +const TRACK_EVENT_URL = 'https://rt.marphezis.com/prebid' + +export const spec = { + code: BIDDER_CODE, + gvlid: 883, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onBidderError, + onTimeout, + onBidWon, + getUserSyncs, +}; + +function buildRequests(bidReqs, bidderRequest) { + try { + const impressions = bidReqs.map(bid => { + let bidSizes = bid?.mediaTypes?.banner?.sizes || bid.sizes; + bidSizes = ((isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]); + bidSizes = bidSizes.filter(size => isArray(size)); + const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})); + + const element = document.getElementById(bid.adUnitCode); + const minSize = _getMinSize(processedSizes); + const viewabilityAmount = _isViewabilityMeasurable(element) ? _getViewability(element, getWindowTop(), minSize) : 'na'; + const viewabilityAmountRounded = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount); + + const imp = { + id: bid.bidId, + banner: { + format: processedSizes, + ext: { + viewability: viewabilityAmountRounded + } + }, + tagid: String(bid.adUnitCode) + }; + + const bidFloor = _getBidFloor(bid); + + if (bidFloor) { + imp.bidfloor = bidFloor; + } + + return imp; + }) + + const referrer = bidderRequest?.refererInfo?.page || ''; + const publisherId = getBidIdParameter('publisherId', bidReqs[0].params); + + const payload = { + id: getUniqueIdentifierStr(), + imp: impressions, + site: { + domain: bidderRequest?.refererInfo?.domain || '', + page: referrer, + publisher: { + id: publisherId + } + }, + device: { + devicetype: _getDeviceType(), + w: screen.width, + h: screen.height + }, + tmax: bidderRequest?.timeout + }; + + if (bidderRequest?.gdprConsent) { + deepSetValue(payload, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies); + deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + } + + if (bidderRequest?.uspConsent) { + deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + if (config.getConfig('coppa') === true) { + deepSetValue(payload, 'regs.coppa', 1); + } + + if (bidReqs?.[0]?.schain) { + deepSetValue(payload, 'source.ext.schain', bidReqs[0].schain) + } + + if (bidReqs?.[0]?.userIdAsEids) { + deepSetValue(payload, 'user.ext.eids', bidReqs[0].userIdAsEids || []) + } + + if (bidReqs?.[0].userId) { + deepSetValue(payload, 'user.ext.ids', bidReqs[0].userId || []) + } + + return { + method: 'POST', + url: URL, + data: JSON.stringify(payload), + }; + } catch (e) { + logError(e, {bidReqs, bidderRequest}); + } +} + +function isBidRequestValid(bid) { + if (bid.bidder !== BIDDER_CODE || !bid.params || !bid.params.publisherId) { + return false; + } + + return true; +} + +function interpretResponse(serverResponse) { + let response = []; + if (!serverResponse.body || typeof serverResponse.body != 'object') { + logWarn('OMS server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); + return response; + } + + const {body: {id, seatbid}} = serverResponse; + + try { + if (id && seatbid && seatbid.length > 0 && seatbid[0].bid && seatbid[0].bid.length > 0) { + response = seatbid[0].bid.map(bid => { + return { + requestId: bid.impid, + cpm: parseFloat(bid.price), + width: parseInt(bid.w), + height: parseInt(bid.h), + creativeId: bid.crid || bid.id, + currency: 'USD', + netRevenue: true, + mediaType: BANNER, + ad: _getAdMarkup(bid), + ttl: 60, + meta: { + advertiserDomains: bid?.adomain || [] + } + }; + }); + } + } catch (e) { + logError(e, {id, seatbid}); + } + + return response; +} + +// Don't do user sync for now +function getUserSyncs(syncOptions, responses, gdprConsent) { + return []; +} + +function onTimeout(timeoutData) { + if (timeoutData === null) { + return; + } + + _trackEvent('timeout', timeoutData); +} + +function onBidderError(errorData) { + if (errorData === null || !errorData.bidderRequest) { + return; + } + + _trackEvent('error', errorData.bidderRequest) +} + +function onBidWon(bid) { + if (bid === null) { + return; + } + + _trackEvent('bidwon', bid) +} + +function _trackEvent(endpoint, data) { + ajax(`${TRACK_EVENT_URL}/${endpoint}`, null, JSON.stringify(data), { + method: 'POST', + withCredentials: false + }); +} + +function _isMobile() { + return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); +} + +function _isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); +} + +function _getDeviceType() { + return _isMobile() ? 1 : _isConnectedTV() ? 3 : 2; +} + +function _getAdMarkup(bid) { + let adm = bid.adm; + if ('nurl' in bid) { + adm += createTrackPixelHtml(bid.nurl); + } + return adm; +} + +function _isViewabilityMeasurable(element) { + return !_isIframe() && element !== null; +} + +function _getViewability(element, topWin, {w, h} = {}) { + return getWindowTop().document.visibilityState === 'visible' ? _getPercentInView(element, topWin, {w, h}) : 0; +} + +function _isIframe() { + try { + return getWindowSelf() !== getWindowTop(); + } catch (e) { + return true; + } +} + +function _getMinSize(sizes) { + return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); +} + +function _getBoundingBox(element, {w, h} = {}) { + let {width, height, left, top, right, bottom} = element.getBoundingClientRect(); + + if ((width === 0 || height === 0) && w && h) { + width = w; + height = h; + right = left + w; + bottom = top + h; + } + + return {width, height, left, top, right, bottom}; +} + +function _getIntersectionOfRects(rects) { + const bbox = { + left: rects[0].left, right: rects[0].right, top: rects[0].top, bottom: rects[0].bottom + }; + + for (let i = 1; i < rects.length; ++i) { + bbox.left = Math.max(bbox.left, rects[i].left); + bbox.right = Math.min(bbox.right, rects[i].right); + + if (bbox.left >= bbox.right) { + return null; + } + + bbox.top = Math.max(bbox.top, rects[i].top); + bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); + + if (bbox.top >= bbox.bottom) { + return null; + } + } + + bbox.width = bbox.right - bbox.left; + bbox.height = bbox.bottom - bbox.top; + + return bbox; +} + +function _getPercentInView(element, topWin, {w, h} = {}) { + const elementBoundingBox = _getBoundingBox(element, {w, h}); + + // Obtain the intersection of the element and the viewport + const elementInViewBoundingBox = _getIntersectionOfRects([{ + left: 0, top: 0, right: topWin.innerWidth, bottom: topWin.innerHeight + }, elementBoundingBox]); + + let elementInViewArea, elementTotalArea; + + if (elementInViewBoundingBox !== null) { + // Some or all of the element is in view + elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; + elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; + + return ((elementInViewArea / elementTotalArea) * 100); + } + + // No overlap between element and the viewport; therefore, the element + // lies completely out of view + return 0; +} + +function _getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return bid.params.bidFloor ? bid.params.bidFloor : null; + } + + let floor = bid.getFloor({ + currency: 'USD', mediaType: '*', size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + +registerBidder(spec); diff --git a/modules/omsBidAdapter.md b/modules/omsBidAdapter.md new file mode 100644 index 00000000000..f1e2d459eca --- /dev/null +++ b/modules/omsBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: OMS Bid Adapter +Module Type: Bidder Adapter +Maintainer: alexandruc@onlinemediasolutions.com +``` + +# Description + +Online media solutions adapter integration to the Prebid library. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'test-leaderboard', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bids: [{ + bidder: 'oms', + params: { + publisherId: 2141020, + bidFloor: 0.01 + } + }] + }, { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'oms', + params: { + publisherId: 2141020 + } + }] + } +] +``` diff --git a/test/spec/modules/omsBidAdapter_spec.js b/test/spec/modules/omsBidAdapter_spec.js new file mode 100644 index 00000000000..a40dee55950 --- /dev/null +++ b/test/spec/modules/omsBidAdapter_spec.js @@ -0,0 +1,411 @@ +import { expect } from 'chai'; +import * as utils from 'src/utils.js'; +import { spec } from 'modules/omsBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import {config} from '../../../src/config'; + +const URL = 'https://rt.marphezis.com/hb'; + +describe('omsBidAdapter', function() { + const adapter = newBidder(spec); + let element, win; + let bidRequests; + let sandbox; + + beforeEach(function() { + element = { + x: 0, + y: 0, + + width: 0, + height: 0, + + getBoundingClientRect: () => { + return { + width: element.width, + height: element.height, + + left: element.x, + top: element.y, + right: element.x + element.width, + bottom: element.y + element.height + }; + } + }; + win = { + document: { + visibilityState: 'visible' + }, + + innerWidth: 800, + innerHeight: 600 + }; + bidRequests = [{ + 'bidder': 'bcmssp', + 'params': { + 'publisherId': 1234567 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'bidId': '5fb26ac22bde4', + 'bidderRequestId': '4bf93aeb730cb9', + 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + } + ] + }, + }]; + + sandbox = sinon.sandbox.create(); + sandbox.stub(document, 'getElementById').withArgs('adunit-code').returns(element); + sandbox.stub(utils, 'getWindowTop').returns(win); + sandbox.stub(utils, 'getWindowSelf').returns(win); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'bcmssp', + 'params': { + 'publisherId': 1234567 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'bidId': '5fb26ac22bde4', + 'bidderRequestId': '4bf93aeb730cb9', + 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when publisherId not passed correctly', function () { + bid.params.publisherId = undefined; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when require params are not passed', function () { + let bid = Object.assign({}, bid); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('sends bid request to our endpoint via POST', function () { + const request = spec.buildRequests(bidRequests); + expect(request.method).to.equal('POST'); + }); + + it('request url should match our endpoint url', function () { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(URL); + }); + + it('sets the proper banner object', function() { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + }); + + it('accepts a single array as a size', function() { + bidRequests[0].mediaTypes.banner.sizes = [300, 250]; + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); + }); + + it('sends bidfloor param if present', function () { + bidRequests[0].params.bidFloor = 0.05; + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].bidfloor).to.equal(0.05); + }); + + it('sends tagid', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].tagid).to.equal('adunit-code'); + }); + + it('sends publisher id', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.site.publisher.id).to.equal(1234567); + }); + + it('sends gdpr info if exists', function () { + const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const bidderRequest = { + 'bidderCode': 'bcmssp', + 'auctionId': '1d1a030790a437', + 'bidderRequestId': '22edbae2744bf5', + 'timeout': 3000, + gdprConsent: { + consentString: consentString, + gdprApplies: true + }, + refererInfo: { + page: 'http://example.com/page.html', + domain: 'example.com', + } + }; + bidderRequest.bids = bidRequests; + + const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); + + expect(data.regs.ext.gdpr).to.exist.and.to.be.a('number'); + expect(data.regs.ext.gdpr).to.equal(1); + expect(data.user.ext.consent).to.exist.and.to.be.a('string'); + expect(data.user.ext.consent).to.equal(consentString); + }); + + it('sends us_privacy', function () { + const bidderRequest = { + uspConsent: '1YYY' + }; + const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data) + + expect(data.regs).to.not.equal(null); + expect(data.regs.ext).to.not.equal(null); + expect(data.regs.ext.us_privacy).to.equal('1YYY'); + }); + + it('sends coppa', function () { + sandbox.stub(config, 'getConfig').withArgs('coppa').returns(true); + + const data = JSON.parse(spec.buildRequests(bidRequests).data) + expect(data.regs).to.not.be.undefined; + expect(data.regs.coppa).to.equal(1); + }); + + it('sends schain', function () { + const data = JSON.parse(spec.buildRequests(bidRequests).data); + expect(data).to.not.be.undefined; + expect(data.source).to.not.be.undefined; + expect(data.source.ext).to.not.be.undefined; + expect(data.source.ext.schain).to.not.be.undefined; + expect(data.source.ext.schain.complete).to.equal(1); + expect(data.source.ext.schain.ver).to.equal('1.0'); + expect(data.source.ext.schain.nodes).to.not.be.undefined; + expect(data.source.ext.schain.nodes).to.lengthOf(1); + expect(data.source.ext.schain.nodes[0].asi).to.equal('exchange1.com'); + expect(data.source.ext.schain.nodes[0].sid).to.equal('1234'); + expect(data.source.ext.schain.nodes[0].hp).to.equal(1); + expect(data.source.ext.schain.nodes[0].rid).to.equal('bid-request-1'); + expect(data.source.ext.schain.nodes[0].name).to.equal('publisher'); + expect(data.source.ext.schain.nodes[0].domain).to.equal('publisher.com'); + }); + + it('sends user eid parameters', function () { + bidRequests[0].userIdAsEids = [{ + source: 'pubcid.org', + uids: [{ + id: 'userid_pubcid' + }] + }, { + source: 'adserver.org', + uids: [{ + id: 'userid_ttd', + ext: { + rtiPartner: 'TDID' + } + }] + } + ]; + + const data = JSON.parse(spec.buildRequests(bidRequests).data); + + expect(data.user).to.not.be.undefined; + expect(data.user.ext).to.not.be.undefined; + expect(data.user.ext.eids).to.not.be.undefined; + expect(data.user.ext.eids).to.deep.equal(bidRequests[0].userIdAsEids); + }); + + it('sends user id parameters', function () { + const userId = { + sharedid: { + id: '01*******', + third: '01E*******' + } + }; + + bidRequests[0].userId = userId; + + const data = JSON.parse(spec.buildRequests(bidRequests).data); + expect(data.user).to.not.be.undefined; + expect(data.user.ext).to.not.be.undefined; + expect(data.user.ext.ids).is.deep.equal(userId); + }); + + context('when element is fully in view', function() { + it('returns 100', function() { + Object.assign(element, { width: 600, height: 400 }); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(100); + }); + }); + + context('when element is out of view', function() { + it('returns 0', function() { + Object.assign(element, { x: -300, y: 0, width: 207, height: 320 }); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(0); + }); + }); + + context('when element is partially in view', function() { + it('returns percentage', function() { + Object.assign(element, { width: 800, height: 800 }); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(75); + }); + }); + + context('when width or height of the element is zero', function() { + it('try to use alternative values', function() { + Object.assign(element, { width: 0, height: 0 }); + bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(25); + }); + }); + + context('when nested iframes', function() { + it('returns \'na\'', function() { + Object.assign(element, { width: 600, height: 400 }); + + utils.getWindowTop.restore(); + utils.getWindowSelf.restore(); + sandbox.stub(utils, 'getWindowTop').returns(win); + sandbox.stub(utils, 'getWindowSelf').returns({}); + + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal('na'); + }); + }); + + context('when tab is inactive', function() { + it('returns 0', function() { + Object.assign(element, { width: 600, height: 400 }); + + utils.getWindowTop.restore(); + win.document.visibilityState = 'hidden'; + sandbox.stub(utils, 'getWindowTop').returns(win); + + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(0); + }); + }); + }); + + describe('interpretResponse', function () { + let response; + beforeEach(function () { + response = { + body: { + 'id': '37386aade21a71', + 'seatbid': [{ + 'bid': [{ + 'id': '376874781', + 'impid': '283a9f4cd2415d', + 'price': 0.35743275, + 'nurl': '', + 'adm': '', + 'w': 300, + 'h': 250, + 'adomain': ['example.com'] + }] + }] + } + }; + }); + + it('should get the correct bid response', function () { + let expectedResponse = [{ + 'requestId': '283a9f4cd2415d', + 'cpm': 0.35743275, + 'width': 300, + 'height': 250, + 'creativeId': '376874781', + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': `
`, + 'ttl': 60, + 'meta': { + 'advertiserDomains': ['example.com'] + } + }]; + + let result = spec.interpretResponse(response); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + + it('crid should default to the bid id if not on the response', function () { + let expectedResponse = [{ + 'requestId': '283a9f4cd2415d', + 'cpm': 0.35743275, + 'width': 300, + 'height': 250, + 'creativeId': response.body.seatbid[0].bid[0].id, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': `
`, + 'ttl': 60, + 'meta': { + 'advertiserDomains': ['example.com'] + } + }]; + + let result = spec.interpretResponse(response); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + + it('handles empty bid response', function () { + let response = { + body: '' + }; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + }); + + describe('getUserSyncs ', () => { + let syncOptions = {iframeEnabled: true, pixelEnabled: true}; + + it('should not return', () => { + let returnStatement = spec.getUserSyncs(syncOptions, []); + expect(returnStatement).to.be.empty; + }); + }); +}); From 96eb76959d653fb68dd11312fccf23023a0eeee9 Mon Sep 17 00:00:00 2001 From: Alexandru Capatina Date: Wed, 10 Jan 2024 12:10:29 +0200 Subject: [PATCH 2/5] OMS Adapter: fix tests --- test/spec/modules/omsBidAdapter_spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/spec/modules/omsBidAdapter_spec.js b/test/spec/modules/omsBidAdapter_spec.js index a40dee55950..1701e884627 100644 --- a/test/spec/modules/omsBidAdapter_spec.js +++ b/test/spec/modules/omsBidAdapter_spec.js @@ -41,7 +41,7 @@ describe('omsBidAdapter', function() { innerHeight: 600 }; bidRequests = [{ - 'bidder': 'bcmssp', + 'bidder': 'oms', 'params': { 'publisherId': 1234567 }, @@ -82,7 +82,7 @@ describe('omsBidAdapter', function() { describe('isBidRequestValid', function () { let bid = { - 'bidder': 'bcmssp', + 'bidder': 'oms', 'params': { 'publisherId': 1234567 }, @@ -159,7 +159,7 @@ describe('omsBidAdapter', function() { it('sends gdpr info if exists', function () { const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; const bidderRequest = { - 'bidderCode': 'bcmssp', + 'bidderCode': 'oms', 'auctionId': '1d1a030790a437', 'bidderRequestId': '22edbae2744bf5', 'timeout': 3000, From ae27bb3a9b0e44ba16524662f9b109034353ca64 Mon Sep 17 00:00:00 2001 From: Alexandru Capatina Date: Tue, 30 Jan 2024 19:33:59 +0200 Subject: [PATCH 3/5] OMS Adapter: required changes --- libraries/percentInView/percentInView.js | 66 +++++++++++++++++++++++ modules/omsBidAdapter.js | 68 ++---------------------- 2 files changed, 70 insertions(+), 64 deletions(-) create mode 100644 libraries/percentInView/percentInView.js diff --git a/libraries/percentInView/percentInView.js b/libraries/percentInView/percentInView.js new file mode 100644 index 00000000000..7941df5596f --- /dev/null +++ b/libraries/percentInView/percentInView.js @@ -0,0 +1,66 @@ + + +function getBoundingBox(element, {w, h} = {}) { + let {width, height, left, top, right, bottom} = element.getBoundingClientRect(); + + if ((width === 0 || height === 0) && w && h) { + width = w; + height = h; + right = left + w; + bottom = top + h; + } + + return {width, height, left, top, right, bottom}; +} + + + +function getIntersectionOfRects(rects) { + const bbox = { + left: rects[0].left, right: rects[0].right, top: rects[0].top, bottom: rects[0].bottom + }; + + for (let i = 1; i < rects.length; ++i) { + bbox.left = Math.max(bbox.left, rects[i].left); + bbox.right = Math.min(bbox.right, rects[i].right); + + if (bbox.left >= bbox.right) { + return null; + } + + bbox.top = Math.max(bbox.top, rects[i].top); + bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); + + if (bbox.top >= bbox.bottom) { + return null; + } + } + + bbox.width = bbox.right - bbox.left; + bbox.height = bbox.bottom - bbox.top; + + return bbox; +} + +export const percentInView = (element, topWin, {w, h} = {}) => { + const elementBoundingBox = getBoundingBox(element, {w, h}); + + // Obtain the intersection of the element and the viewport + const elementInViewBoundingBox = getIntersectionOfRects([{ + left: 0, top: 0, right: topWin.innerWidth, bottom: topWin.innerHeight + }, elementBoundingBox]); + + let elementInViewArea, elementTotalArea; + + if (elementInViewBoundingBox !== null) { + // Some or all of the element is in view + elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; + elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; + + return ((elementInViewArea / elementTotalArea) * 100); + } + + // No overlap between element and the viewport; therefore, the element + // lies completely out of view + return 0; +} diff --git a/modules/omsBidAdapter.js b/modules/omsBidAdapter.js index 1f099372595..1dab446d866 100644 --- a/modules/omsBidAdapter.js +++ b/modules/omsBidAdapter.js @@ -14,6 +14,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {ajax} from '../src/ajax.js'; +import {percentInView} from "../libraries/percentInView/percentInView"; const BIDDER_CODE = 'oms'; const URL = 'https://rt.marphezis.com/hb'; @@ -21,6 +22,7 @@ const TRACK_EVENT_URL = 'https://rt.marphezis.com/prebid' export const spec = { code: BIDDER_CODE, + aliases: ['brightcom', 'bcmssp'], gvlid: 883, supportedMediaTypes: [BANNER], isBidRequestValid, @@ -95,7 +97,7 @@ function buildRequests(bidReqs, bidderRequest) { deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); } - if (config.getConfig('coppa') === true) { + if (config.getConfig('coppa') === true || bidReqs[0].coppa) { deepSetValue(payload, 'regs.coppa', 1); } @@ -226,7 +228,7 @@ function _isViewabilityMeasurable(element) { } function _getViewability(element, topWin, {w, h} = {}) { - return getWindowTop().document.visibilityState === 'visible' ? _getPercentInView(element, topWin, {w, h}) : 0; + return getWindowTop().document.visibilityState === 'visible' ? percentInView(element, topWin, {w, h}) : 0; } function _isIframe() { @@ -241,68 +243,6 @@ function _getMinSize(sizes) { return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); } -function _getBoundingBox(element, {w, h} = {}) { - let {width, height, left, top, right, bottom} = element.getBoundingClientRect(); - - if ((width === 0 || height === 0) && w && h) { - width = w; - height = h; - right = left + w; - bottom = top + h; - } - - return {width, height, left, top, right, bottom}; -} - -function _getIntersectionOfRects(rects) { - const bbox = { - left: rects[0].left, right: rects[0].right, top: rects[0].top, bottom: rects[0].bottom - }; - - for (let i = 1; i < rects.length; ++i) { - bbox.left = Math.max(bbox.left, rects[i].left); - bbox.right = Math.min(bbox.right, rects[i].right); - - if (bbox.left >= bbox.right) { - return null; - } - - bbox.top = Math.max(bbox.top, rects[i].top); - bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); - - if (bbox.top >= bbox.bottom) { - return null; - } - } - - bbox.width = bbox.right - bbox.left; - bbox.height = bbox.bottom - bbox.top; - - return bbox; -} - -function _getPercentInView(element, topWin, {w, h} = {}) { - const elementBoundingBox = _getBoundingBox(element, {w, h}); - - // Obtain the intersection of the element and the viewport - const elementInViewBoundingBox = _getIntersectionOfRects([{ - left: 0, top: 0, right: topWin.innerWidth, bottom: topWin.innerHeight - }, elementBoundingBox]); - - let elementInViewArea, elementTotalArea; - - if (elementInViewBoundingBox !== null) { - // Some or all of the element is in view - elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; - elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; - - return ((elementInViewArea / elementTotalArea) * 100); - } - - // No overlap between element and the viewport; therefore, the element - // lies completely out of view - return 0; -} function _getBidFloor(bid) { if (!isFn(bid.getFloor)) { From 5dbcf2976b2f8c50ad966083e278d2c022a281c4 Mon Sep 17 00:00:00 2001 From: Alexandru Capatina Date: Tue, 30 Jan 2024 19:35:20 +0200 Subject: [PATCH 4/5] OMS Adapter: change ttl --- modules/omsBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/omsBidAdapter.js b/modules/omsBidAdapter.js index 1dab446d866..9fa98946749 100644 --- a/modules/omsBidAdapter.js +++ b/modules/omsBidAdapter.js @@ -153,7 +153,7 @@ function interpretResponse(serverResponse) { netRevenue: true, mediaType: BANNER, ad: _getAdMarkup(bid), - ttl: 60, + ttl: 300, meta: { advertiserDomains: bid?.adomain || [] } From 6886457178bcb2a52ce89e3097283282008e0ec3 Mon Sep 17 00:00:00 2001 From: Alexandru Capatina Date: Sun, 11 Feb 2024 15:27:15 +0200 Subject: [PATCH 5/5] OMS Adapter: required changes --- libraries/percentInView/percentInView.js | 3 - modules/omsBidAdapter.js | 59 +++++++++++--------- test/spec/modules/omsBidAdapter_spec.js | 71 ++++++++++-------------- 3 files changed, 63 insertions(+), 70 deletions(-) diff --git a/libraries/percentInView/percentInView.js b/libraries/percentInView/percentInView.js index 7941df5596f..13381c5c541 100644 --- a/libraries/percentInView/percentInView.js +++ b/libraries/percentInView/percentInView.js @@ -1,5 +1,4 @@ - function getBoundingBox(element, {w, h} = {}) { let {width, height, left, top, right, bottom} = element.getBoundingClientRect(); @@ -13,8 +12,6 @@ function getBoundingBox(element, {w, h} = {}) { return {width, height, left, top, right, bottom}; } - - function getIntersectionOfRects(rects) { const bbox = { left: rects[0].left, right: rects[0].right, top: rects[0].top, bottom: rects[0].bottom diff --git a/modules/omsBidAdapter.js b/modules/omsBidAdapter.js index 9fa98946749..bef9a43749f 100644 --- a/modules/omsBidAdapter.js +++ b/modules/omsBidAdapter.js @@ -1,20 +1,20 @@ import { isArray, getWindowTop, - getUniqueIdentifierStr, deepSetValue, logError, logWarn, createTrackPixelHtml, getWindowSelf, isFn, - isPlainObject, getBidIdParameter, + isPlainObject, + getBidIdParameter, + getUniqueIdentifierStr, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; import {ajax} from '../src/ajax.js'; -import {percentInView} from "../libraries/percentInView/percentInView"; +import {percentInView} from '../libraries/percentInView/percentInView.js'; const BIDDER_CODE = 'oms'; const URL = 'https://rt.marphezis.com/hb'; @@ -29,7 +29,6 @@ export const spec = { buildRequests, interpretResponse, onBidderError, - onTimeout, onBidWon, getUserSyncs, }; @@ -81,7 +80,7 @@ function buildRequests(bidReqs, bidderRequest) { } }, device: { - devicetype: _getDeviceType(), + devicetype: _getDeviceType(navigator.userAgent, bidderRequest?.ortb2?.device?.sua), w: screen.width, h: screen.height }, @@ -93,11 +92,12 @@ function buildRequests(bidReqs, bidderRequest) { deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); } - if (bidderRequest?.uspConsent) { - deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); + const gpp = _getGpp(bidderRequest) + if (gpp) { + deepSetValue(payload, 'regs.ext.gpp', gpp); } - if (config.getConfig('coppa') === true || bidReqs[0].coppa) { + if (bidderRequest?.ortb2?.regs?.coppa) { deepSetValue(payload, 'regs.coppa', 1); } @@ -105,6 +105,10 @@ function buildRequests(bidReqs, bidderRequest) { deepSetValue(payload, 'source.ext.schain', bidReqs[0].schain) } + if (bidderRequest?.ortb2?.user) { + deepSetValue(payload, 'user', bidderRequest.ortb2.user) + } + if (bidReqs?.[0]?.userIdAsEids) { deepSetValue(payload, 'user.ext.eids', bidReqs[0].userIdAsEids || []) } @@ -113,6 +117,10 @@ function buildRequests(bidReqs, bidderRequest) { deepSetValue(payload, 'user.ext.ids', bidReqs[0].userId || []) } + if (bidderRequest?.ortb2?.site?.content) { + deepSetValue(payload, 'site.content', bidderRequest.ortb2.site.content) + } + return { method: 'POST', url: URL, @@ -172,14 +180,6 @@ function getUserSyncs(syncOptions, responses, gdprConsent) { return []; } -function onTimeout(timeoutData) { - if (timeoutData === null) { - return; - } - - _trackEvent('timeout', timeoutData); -} - function onBidderError(errorData) { if (errorData === null || !errorData.bidderRequest) { return; @@ -203,16 +203,26 @@ function _trackEvent(endpoint, data) { }); } -function _isMobile() { - return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); -} +function _getDeviceType(ua, sua) { + if (sua?.mobile || (/(ios|ipod|ipad|iphone|android)/i).test(ua)) { + return 1 + } -function _isConnectedTV() { - return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); + if ((/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(ua)) { + return 3 + } + + return 2 } -function _getDeviceType() { - return _isMobile() ? 1 : _isConnectedTV() ? 3 : 2; +function _getGpp(bidderRequest) { + if (bidderRequest?.gppConsent != null) { + return bidderRequest.gppConsent; + } + + return ( + bidderRequest?.ortb2?.regs?.gpp ?? { gppString: '', applicableSections: '' } + ); } function _getAdMarkup(bid) { @@ -243,7 +253,6 @@ function _getMinSize(sizes) { return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); } - function _getBidFloor(bid) { if (!isFn(bid.getFloor)) { return bid.params.bidFloor ? bid.params.bidFloor : null; diff --git a/test/spec/modules/omsBidAdapter_spec.js b/test/spec/modules/omsBidAdapter_spec.js index 1701e884627..a7b7ba09113 100644 --- a/test/spec/modules/omsBidAdapter_spec.js +++ b/test/spec/modules/omsBidAdapter_spec.js @@ -1,18 +1,18 @@ -import { expect } from 'chai'; +import {expect} from 'chai'; import * as utils from 'src/utils.js'; -import { spec } from 'modules/omsBidAdapter'; -import { newBidder } from 'src/adapters/bidderFactory.js'; +import {spec} from 'modules/omsBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from '../../../src/config'; const URL = 'https://rt.marphezis.com/hb'; -describe('omsBidAdapter', function() { +describe('omsBidAdapter', function () { const adapter = newBidder(spec); let element, win; let bidRequests; let sandbox; - beforeEach(function() { + beforeEach(function () { element = { x: 0, y: 0, @@ -76,7 +76,7 @@ describe('omsBidAdapter', function() { sandbox.stub(utils, 'getWindowSelf').returns(win); }); - afterEach(function() { + afterEach(function () { sandbox.restore(); }); @@ -124,13 +124,13 @@ describe('omsBidAdapter', function() { expect(request.url).to.equal(URL); }); - it('sets the proper banner object', function() { + it('sets the proper banner object', function () { const request = spec.buildRequests(bidRequests); const payload = JSON.parse(request.data); expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); }); - it('accepts a single array as a size', function() { + it('accepts a single array as a size', function () { bidRequests[0].mediaTypes.banner.sizes = [300, 250]; const request = spec.buildRequests(bidRequests); const payload = JSON.parse(request.data); @@ -182,21 +182,8 @@ describe('omsBidAdapter', function() { expect(data.user.ext.consent).to.equal(consentString); }); - it('sends us_privacy', function () { - const bidderRequest = { - uspConsent: '1YYY' - }; - const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data) - - expect(data.regs).to.not.equal(null); - expect(data.regs.ext).to.not.equal(null); - expect(data.regs.ext.us_privacy).to.equal('1YYY'); - }); - it('sends coppa', function () { - sandbox.stub(config, 'getConfig').withArgs('coppa').returns(true); - - const data = JSON.parse(spec.buildRequests(bidRequests).data) + const data = JSON.parse(spec.buildRequests(bidRequests, {ortb2: {regs: {coppa: 1}}}).data) expect(data.regs).to.not.be.undefined; expect(data.regs.coppa).to.equal(1); }); @@ -260,36 +247,36 @@ describe('omsBidAdapter', function() { expect(data.user.ext.ids).is.deep.equal(userId); }); - context('when element is fully in view', function() { - it('returns 100', function() { - Object.assign(element, { width: 600, height: 400 }); + context('when element is fully in view', function () { + it('returns 100', function () { + Object.assign(element, {width: 600, height: 400}); const request = spec.buildRequests(bidRequests); const payload = JSON.parse(request.data); expect(payload.imp[0].banner.ext.viewability).to.equal(100); }); }); - context('when element is out of view', function() { - it('returns 0', function() { - Object.assign(element, { x: -300, y: 0, width: 207, height: 320 }); + context('when element is out of view', function () { + it('returns 0', function () { + Object.assign(element, {x: -300, y: 0, width: 207, height: 320}); const request = spec.buildRequests(bidRequests); const payload = JSON.parse(request.data); expect(payload.imp[0].banner.ext.viewability).to.equal(0); }); }); - context('when element is partially in view', function() { - it('returns percentage', function() { - Object.assign(element, { width: 800, height: 800 }); + context('when element is partially in view', function () { + it('returns percentage', function () { + Object.assign(element, {width: 800, height: 800}); const request = spec.buildRequests(bidRequests); const payload = JSON.parse(request.data); expect(payload.imp[0].banner.ext.viewability).to.equal(75); }); }); - context('when width or height of the element is zero', function() { - it('try to use alternative values', function() { - Object.assign(element, { width: 0, height: 0 }); + context('when width or height of the element is zero', function () { + it('try to use alternative values', function () { + Object.assign(element, {width: 0, height: 0}); bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; const request = spec.buildRequests(bidRequests); const payload = JSON.parse(request.data); @@ -297,9 +284,9 @@ describe('omsBidAdapter', function() { }); }); - context('when nested iframes', function() { - it('returns \'na\'', function() { - Object.assign(element, { width: 600, height: 400 }); + context('when nested iframes', function () { + it('returns \'na\'', function () { + Object.assign(element, {width: 600, height: 400}); utils.getWindowTop.restore(); utils.getWindowSelf.restore(); @@ -312,9 +299,9 @@ describe('omsBidAdapter', function() { }); }); - context('when tab is inactive', function() { - it('returns 0', function() { - Object.assign(element, { width: 600, height: 400 }); + context('when tab is inactive', function () { + it('returns 0', function () { + Object.assign(element, {width: 600, height: 400}); utils.getWindowTop.restore(); win.document.visibilityState = 'hidden'; @@ -360,7 +347,7 @@ describe('omsBidAdapter', function() { 'netRevenue': true, 'mediaType': 'banner', 'ad': `
`, - 'ttl': 60, + 'ttl': 300, 'meta': { 'advertiserDomains': ['example.com'] } @@ -381,7 +368,7 @@ describe('omsBidAdapter', function() { 'netRevenue': true, 'mediaType': 'banner', 'ad': `
`, - 'ttl': 60, + 'ttl': 300, 'meta': { 'advertiserDomains': ['example.com'] }