From 79c7611ef8a1896cc63bbd2272513ab5cedfb9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20G?= Date: Wed, 13 Nov 2024 12:00:48 +0100 Subject: [PATCH 1/7] Greenbids Bidder adapter --- modules/greenbidsBidAdapter.js | 324 +++++ modules/greenbidsBidAdapter.md | 32 + .../spec/modules/greenbidsBidAdapter_specs.js | 1045 +++++++++++++++++ 3 files changed, 1401 insertions(+) create mode 100644 modules/greenbidsBidAdapter.js create mode 100644 modules/greenbidsBidAdapter.md create mode 100644 test/spec/modules/greenbidsBidAdapter_specs.js diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js new file mode 100644 index 00000000000..7f5d7612411 --- /dev/null +++ b/modules/greenbidsBidAdapter.js @@ -0,0 +1,324 @@ +import { getValue, logError, deepAccess, parseSizesInput, isArray, getBidIdParameter } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { getDM, getHC, getHLen } from '../libraries/navigatorData/navigatorData.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + +const BIDDER_CODE = 'greenbids'; +const GVL_ID = 1232; +const ENDPOINT_URL = 'https://d.greenbids.ai/hb/bid-request'; +const gdprStatus = { + GDPR_APPLIES_PUBLISHER: 12, + GDPR_APPLIES_GLOBAL: 11, + GDPR_DOESNT_APPLY: 0, + CMP_NOT_FOUND_OR_ERROR: 22 +}; +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: ['banner'], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + let isValid = false; + if (typeof bid.params !== 'undefined') { + let isValidgbPlacementId = _validateId(getValue(bid.params, 'gbPlacementId')); + isValid = isValidgbPlacementId; + } + + if (!isValid) { + logError(' parameters are required. Bid aborted.'); + } + return isValid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const bids = validBidRequests.map(buildRequestObject); + const topWindow = window.top; + + const payload = { + referrer: getReferrerInfo(bidderRequest), + pageReferrer: document.referrer, + pageTitle: getPageTitle().slice(0, 300), + pageDescription: getPageDescription().slice(0, 300), + networkBandwidth: getConnectionDownLink(window.navigator), + timeToFirstByte: getTimeToFirstByte(window), + data: bids, + device: bidderRequest?.ortb2?.device || {}, + deviceWidth: screen.width, + deviceHeight: screen.height, + devicePixelRatio: topWindow.devicePixelRatio, + screenOrientation: screen.orientation?.type, + historyLength: getHLen(), + viewportHeight: topWindow.visualViewport?.height, + viewportWidth: topWindow.visualViewport?.width, + hardwareConcurrency: getHC(), + deviceMemory: getDM(), + hb_version: '$prebid.version$', + ...getSharedViewerIdParameters(validBidRequests), + }; + + const firstBidRequest = validBidRequests[0]; + + if (firstBidRequest.schain) { + payload.schain = firstBidRequest.schain; + } + + let gpp = bidderRequest.gppConsent; + if (bidderRequest && gpp) { + let isValidConsentString = typeof gpp.gppString === 'string'; + let validateApplicableSections = + Array.isArray(gpp.applicableSections) && + gpp.applicableSections.every((section) => typeof (section) === 'number') + payload.gpp = { + consentString: isValidConsentString ? gpp.gppString : '', + applicableSectionIds: validateApplicableSections ? gpp.applicableSections : [], + }; + } + + let gdpr = bidderRequest.gdprConsent; + if (bidderRequest && gdpr) { + let isCmp = typeof gdpr.gdprApplies === 'boolean'; + let isConsentString = typeof gdpr.consentString === 'string'; + let status = isCmp + ? findGdprStatus(gdpr.gdprApplies, gdpr.vendorData) + : gdprStatus.CMP_NOT_FOUND_OR_ERROR; + payload.gdpr_iab = { + consent: isConsentString ? gdpr.consentString : '', + status: status, + apiVersion: gdpr.apiVersion + }; + } + + if (bidderRequest && bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + + const userAgentClientHints = deepAccess(firstBidRequest, 'ortb2.device.sua'); + if (userAgentClientHints) { + payload.userAgentClientHints = userAgentClientHints; + } + + const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); + if (dsa) { + payload.dsa = dsa; + } + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidderRequest) { + serverResponse = serverResponse.body; + + if (!serverResponse.responses) { + return []; + } + + return serverResponse.responses.map((bid) => { + const bidResponse = { + cpm: bid.cpm, + width: bid.width, + height: bid.height, + currency: bid.currency, + netRevenue: true, + ttl: bid.ttl, + meta: { + advertiserDomains: bid && bid.adomain ? bid.adomain : [], + }, + ad: bid.ad, + requestId: bid.bidId, + creativeId: bid.creativeId, + gbPlacementId: bid.gbPlacementId, + }; + if (bid.dealId) { + bidResponse.dealId = bid.dealId + } + if (bid?.ext?.dsa) { + bidResponse.meta.dsa = bid.ext.dsa; + } + return bidResponse; + }); + } +}; + +/** + * + * @param validBidRequests an array of bids + * @returns {{sharedViewerIdKey : 'sharedViewerIdValue'}} object with all sharedviewerids + */ +function getSharedViewerIdParameters(validBidRequests) { + const sharedViewerIdMapping = { + unifiedId2: 'uid2.id', // uid2IdSystem + liveRampId: 'idl_env', // identityLinkIdSystem + lotamePanoramaId: 'lotamePanoramaId', // lotamePanoramaIdSystem + id5Id: 'id5id.uid', // id5IdSystem + criteoId: 'criteoId', // criteoIdSystem + yahooConnectId: 'connectId', // connectIdSystem + quantcastId: 'quantcastId', // quantcastIdSystem + epsilonPublisherLinkId: 'publinkId', // publinkIdSystem + publisherFirstPartyViewerId: 'pubcid', // sharedIdSystem + merkleId: 'merkleId.id', // merkleIdSystem + kinessoId: 'kpuid' // kinessoIdSystem + } + + let sharedViewerIdObject = {}; + for (const sharedViewerId in sharedViewerIdMapping) { + const key = sharedViewerIdMapping[sharedViewerId]; + const value = deepAccess(validBidRequests, `0.userId.${key}`); + if (value) { + sharedViewerIdObject[sharedViewerId] = value; + } + } + return sharedViewerIdObject; +} + +function getReferrerInfo(bidderRequest) { + let ref = ''; + if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) { + ref = bidderRequest.refererInfo.page; + } + return ref; +} + +function getPageTitle() { + try { + const ogTitle = window.top.document.querySelector('meta[property="og:title"]') + + return window.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]') + + return document.title || (ogTitle && ogTitle.content) || ''; + } +} + +function getPageDescription() { + let element; + + try { + element = window.top.document.querySelector('meta[name="description"]') || + window.top.document.querySelector('meta[property="og:description"]') + } catch (e) { + element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]') + } + + return (element && element.content) || ''; +} + +function getConnectionDownLink(nav) { + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; +} + +function getTimeToFirstByte(win) { + const performance = win.performance || win.webkitPerformance || win.msPerformance || win.mozPerformance; + + const ttfbWithTimingV2 = performance && + typeof performance.getEntriesByType === 'function' && + Object.prototype.toString.call(performance.getEntriesByType) === '[object Function]' && + performance.getEntriesByType('navigation')[0] && + performance.getEntriesByType('navigation')[0].responseStart && + performance.getEntriesByType('navigation')[0].requestStart && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && + Math.round( + performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart + ); + + if (ttfbWithTimingV2) { + return ttfbWithTimingV2.toString(); + } + + const ttfbWithTimingV1 = performance && + performance.timing.responseStart && + performance.timing.requestStart && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && + performance.timing.responseStart - performance.timing.requestStart; + + return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; +} + +function findGdprStatus(gdprApplies, gdprData) { + let status = gdprStatus.GDPR_APPLIES_PUBLISHER; + if (gdprApplies) { + if (gdprData && !gdprData.isServiceSpecific) { + status = gdprStatus.GDPR_APPLIES_GLOBAL; + } + } else { + status = gdprStatus.GDPR_DOESNT_APPLY; + } + return status; +} + +function buildRequestObject(bid) { + const reqObj = {}; + let gbPlacementId = getValue(bid.params, 'gbPlacementId'); + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); + reqObj.sizes = getSizes(bid); + reqObj.bidId = getBidIdParameter('bidId', bid); + reqObj.bidderRequestId = getBidIdParameter('bidderRequestId', bid); + reqObj.gbPlacementId = parseInt(gbPlacementId, 10); + reqObj.adUnitCode = getBidIdParameter('adUnitCode', bid); + reqObj.transactionId = bid.ortb2Imp?.ext?.tid || ''; + if (gpid) { reqObj.gpid = gpid; } + return reqObj; +} + +function getSizes(bid) { + return parseSizesInput(concatSizes(bid)); +} + +function concatSizes(bid) { + let bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); + + if (isArray(bannerSizes)) { + let mediaTypesSizes = [bannerSizes]; + return mediaTypesSizes + .reduce(function (acc, currSize) { + if (isArray(currSize)) { + if (isArray(currSize[0])) { + currSize.forEach(function (childSize) { + acc.push(childSize); + }) + } else { + acc.push(currSize); + } + } + return acc; + }, []); + } else { + return bid.sizes; + } +} + +function _validateId(id) { + return (parseInt(id) > 0); +} + +registerBidder(spec); diff --git a/modules/greenbidsBidAdapter.md b/modules/greenbidsBidAdapter.md new file mode 100644 index 00000000000..18e9840fe44 --- /dev/null +++ b/modules/greenbidsBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +**Module Name**: Greenbids Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: tech@greenbids.ai + +# Description + +Use `greenbids` as bidder. + +## AdUnits configuration example +``` + var adUnits = [{ + code: 'your-slot_1-div', //use exactly the same code as your slot div id. + sizes: [[300, 250]], + bids: [{ + bidder: 'greenbids', + params: { + gbPlacementId: 12345, + } + }] + },{ + code: 'your-slot_2-div', //use exactly the same code as your slot div id. + sizes: [[600, 800]], + bids: [{ + bidder: 'greenbids', + params: { + gbPlacementId: 12345, + } + }] + }]; +``` diff --git a/test/spec/modules/greenbidsBidAdapter_specs.js b/test/spec/modules/greenbidsBidAdapter_specs.js new file mode 100644 index 00000000000..6bfa1b96135 --- /dev/null +++ b/test/spec/modules/greenbidsBidAdapter_specs.js @@ -0,0 +1,1045 @@ +import { expect } from 'chai'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from 'modules/greenbidsBidAdapter.js'; +const ENDPOINT = 'https://d.greenbids.ai/hb/bid-request'; +const AD_SCRIPT = '"'; + +describe('greenbidsBidAdapter', () => { + const adapter = newBidder(spec); + let sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'greenbids', + 'params': { + 'gbPlacementId': 4242 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }) + describe('buildRequests', function () { + it('should send bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should not send auctionId in bid request ', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].auctionId).to.not.exist + }); + + it('should send US Privacy to endpoint', function () { + let usPrivacy = 'OHHHFCP1' + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': usPrivacy + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.us_privacy).to.exist; + expect(payload.us_privacy).to.equal(usPrivacy); + }); + + it('should send GPP values to endpoint when available and valid', function () { + let consentString = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'; + let applicableSectionIds = [7, 8]; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': consentString, + 'applicableSections': applicableSectionIds + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(consentString); + expect(payload.gpp.applicableSectionIds).to.have.members(applicableSectionIds); + }); + + it('should send default GPP values to endpoint when available but invalid', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': undefined, + 'applicableSections': ['a'] + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(''); + expect(payload.gpp.applicableSectionIds).to.have.members([]); + }); + + it('should not set the GPP object in the request sent to the endpoint when not present', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.not.exist; + }); + + it('should send GDPR to endpoint', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should add referer info to payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequest = { + refererInfo: { + page: 'https://example.com/page.html', + reachedTop: true, + numIframes: 2 + } + } + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.referrer).to.exist; + expect(payload.referrer).to.deep.equal('https://example.com/page.html') + }); + + it('should add networkBandwidth info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + const bandwidth = window.navigator && window.navigator.connection && window.navigator.connection.downlink; + + expect(payload.networkBandwidth).to.exist; + + if (bandwidth) { + expect(payload.networkBandwidth).to.deep.equal(bandwidth.toString()); + } else { + expect(payload.networkBandwidth).to.deep.equal(''); + } + }); + + it('should add pageReferrer info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageReferrer).to.exist; + expect(payload.pageReferrer).to.deep.equal(document.referrer); + }); + + it('should add width info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceWidth = screen.width + + expect(payload.deviceWidth).to.exist; + expect(payload.deviceWidth).to.deep.equal(deviceWidth); + }); + + it('should add height info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceHeight = screen.height + + expect(payload.deviceHeight).to.exist; + expect(payload.deviceHeight).to.deep.equal(deviceHeight); + }); + + it('should add pixelRatio info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const pixelRatio = window.top.devicePixelRatio + + expect(payload.devicePixelRatio).to.exist; + expect(payload.devicePixelRatio).to.deep.equal(pixelRatio); + }); + + it('should add screenOrientation info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const screenOrientation = window.top.screen.orientation?.type + + if (screenOrientation) { + expect(payload.screenOrientation).to.exist; + expect(payload.screenOrientation).to.deep.equal(screenOrientation); + } else expect(payload.screenOrientation).to.not.exist; + }); + + it('should add historyLength info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.historyLength).to.exist; + expect(payload.historyLength).to.deep.equal(window.top.history.length); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add viewportWidth info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportWidth).to.exist; + expect(payload.viewportWidth).to.deep.equal(window.top.visualViewport.width); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add ortb2 device data to payload', function () { + const ortb2DeviceBidderRequest = { + ...bidderRequestDefault, + ...{ + ortb2: { + device: { + w: 980, + h: 1720, + dnt: 0, + ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', + language: 'en', + devicetype: 1, + make: 'Apple', + model: 'iPhone 12 Pro Max', + os: 'iOS', + osv: '17.4', + ext: { fiftyonedegrees_deviceId: '17595-133085-133468-18092' }, + }, + }, + }, + }; + const request = spec.buildRequests(bidRequests, ortb2DeviceBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.device).to.deep.equal(ortb2DeviceBidderRequest.ortb2.device); + }); + + it('should add hardwareConcurrency info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const hardwareConcurrency = window.top.navigator?.hardwareConcurrency + + if (hardwareConcurrency) { + expect(payload.hardwareConcurrency).to.exist; + expect(payload.hardwareConcurrency).to.deep.equal(hardwareConcurrency); + } else expect(payload.hardwareConcurrency).to.not.exist + }); + + it('should add deviceMemory info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceMemory = window.top.navigator.deviceMemory + + if (deviceMemory) { + expect(payload.deviceMemory).to.exist; + expect(payload.deviceMemory).to.deep.equal(deviceMemory); + } else expect(payload.deviceMemory).to.not.exist; + }); + }); + describe('pageTitle', function () { + it('should add pageTitle info to payload based on document title', function () { + const testText = 'This is a title'; + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload based on open-graph title', function () { + const testText = 'This is a title from open-graph'; + sandbox.stub(window.top.document, 'title').value(''); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.have.length(300); + }); + + it('should add pageTitle info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback title'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + }); + + describe('pageDescription', function () { + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description from open-graph'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.have.length(300); + }); + + it('should add pageDescription info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback description'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add timeToFirstByte info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance; + + const ttfbExpectedV2 = performance && + typeof performance.getEntriesByType === 'function' && + Object.prototype.toString.call(performance.getEntriesByType) === '[object Function]' && + performance.getEntriesByType('navigation')[0] && + performance.getEntriesByType('navigation')[0].responseStart && + performance.getEntriesByType('navigation')[0].requestStart && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && + Math.round( + performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart + ); + + expect(payload.timeToFirstByte).to.exist; + + if (ttfbExpectedV2) { + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV2.toString()); + } else { + const ttfbWithTimingV1 = performance && + performance.timing.responseStart && + performance.timing.requestStart && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && + performance.timing.responseStart - performance.timing.requestStart; + const ttfbExpectedV1 = ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; + + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV1); + } + }); + + it('should send GDPR to endpoint with 11 status', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': false, + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(11); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR TCF2 to endpoint with 12 status', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 22 status', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': undefined, + 'gdprApplies': undefined, + 'vendorData': undefined, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(''); + expect(payload.gdpr_iab.status).to.equal(22); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 0 status', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': false, + 'vendorData': { + 'hasGlobalScope': false + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(0); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 0 status when gdprApplies = false (vendorData = undefined)', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': undefined, + 'gdprApplies': false, + 'vendorData': undefined, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(''); + expect(payload.gdpr_iab.status).to.equal(0); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 12 status when apiVersion = 0', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 0 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(0); + }); + + it('should add schain info to payload if available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + } + }); + + const request = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.schain).to.exist; + expect(payload.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + }); + }); + + it('should add userAgentClientHints info to payload if available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + ortb2: { + device: { + sua: { + source: 2, + platform: { + brand: 'macOS', + version: ['12', '4', '0'] + }, + browsers: [ + { + brand: 'Chromium', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Google Chrome', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Not;A=Brand', + version: ['99', '0', '0', '0'] + } + ], + mobile: 0, + model: '', + bitness: '64', + architecture: 'x86' + } + } + } + }); + + const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(requestWithUserAgentClientHints.data); + + expect(payload.userAgentClientHints).to.exist; + expect(payload.userAgentClientHints).to.deep.equal({ + source: 2, + platform: { + brand: 'macOS', + version: ['12', '4', '0'] + }, + browsers: [ + { + brand: 'Chromium', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Google Chrome', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Not;A=Brand', + version: ['99', '0', '0', '0'] + } + ], + mobile: 0, + model: '', + bitness: '64', + architecture: 'x86' + } + ); + + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); + expect(JSON.parse(defaultRequest.data).userAgentClientHints).to.not.exist; + }); + + it('should use good mediaTypes banner sizes', function () { + const mediaTypesBannerSize = { + 'mediaTypes': { + 'banner': { + 'sizes': [46, 48] + } + } + }; + checkMediaTypesSizes(mediaTypesBannerSize, '46x48'); + }); + }); + + describe('User IDs', function () { + const baseBidRequest = { + 'bidder': 'teads', + 'params': { + 'placementId': 10433394, + 'pageId': 1234 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + }; + + const userIdModules = { + unifiedId2: { uid2: { id: 'unifiedId2-id' } }, + liveRampId: { idl_env: 'liveRampId-id' }, + lotamePanoramaId: { lotamePanoramaId: 'lotamePanoramaId-id' }, + id5Id: { id5id: { uid: 'id5Id-id' } }, + criteoId: { criteoId: 'criteoId-id' }, + yahooConnectId: { connectId: 'yahooConnectId-id' }, + quantcastId: { quantcastId: 'quantcastId-id' }, + epsilonPublisherLinkId: { publinkId: 'epsilonPublisherLinkId-id' }, + publisherFirstPartyViewerId: { pubcid: 'publisherFirstPartyViewerId-id' }, + merkleId: { merkleId: { id: 'merkleId-id' } }, + kinessoId: { kpuid: 'kinessoId-id' } + }; + + describe('User Id Modules', function () { + it(`should not add param to payload if user id system is not enabled`, function () { + const bidRequest = { + ...baseBidRequest, + userId: {} // no property -> assumption that the system is disabled + }; + + const request = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(request.data); + + for (const userId in userIdModules) { + expect(payload, userId).not.to.have.property(userId); + } + }); + + it(`should not add param to payload if user id field is absent`, function () { + const request = spec.buildRequests([baseBidRequest], bidderRequestDefault); + const payload = JSON.parse(request.data); + + for (const userId in userIdModules) { + expect(payload, userId).not.to.have.property(userId); + } + }); + + it(`should not add param to payload if user id is enabled but there is no value`, function () { + const bidRequest = { + ...baseBidRequest, + userId: { + idl_env: '', + pubcid: 'publisherFirstPartyViewerId-id' + } + }; + + const request = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload).not.to.have.property('liveRampId'); + expect(payload['publisherFirstPartyViewerId']).to.equal('publisherFirstPartyViewerId-id'); + }); + + it(`should add userId param to payload for each enabled user id system`, function () { + let userIdObject = {}; + for (const userId in userIdModules) { + userIdObject = { + ...userIdObject, + ...userIdModules[userId] + } + } + const bidRequest = { + ...baseBidRequest, + userId: userIdObject + }; + + const request = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload['unifiedId2']).to.equal('unifiedId2-id'); + expect(payload['liveRampId']).to.equal('liveRampId-id'); + expect(payload['lotamePanoramaId']).to.equal('lotamePanoramaId-id'); + expect(payload['id5Id']).to.equal('id5Id-id'); + expect(payload['criteoId']).to.equal('criteoId-id'); + expect(payload['yahooConnectId']).to.equal('yahooConnectId-id'); + expect(payload['quantcastId']).to.equal('quantcastId-id'); + expect(payload['epsilonPublisherLinkId']).to.equal('epsilonPublisherLinkId-id'); + expect(payload['publisherFirstPartyViewerId']).to.equal('publisherFirstPartyViewerId-id'); + expect(payload['merkleId']).to.equal('merkleId-id'); + expect(payload['kinessoId']).to.equal('kinessoId-id'); + }); + }) + }); + describe('Global Placement Id', function () { + let bidRequests = [ + { + 'bidder': 'greenbids', + 'params': { + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + }, + { + 'bidder': 'greenbids', + 'params': { + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1f', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ef', + 'deviceWidth': 1680 + } + ]; + + it('should add gpid if ortb2Imp.ext.gpid is present and is non empty', function () { + const updatedBidRequests = bidRequests.map(function (bidRequest, index) { + return { + ...bidRequest, + ortb2Imp: { + ext: { + gpid: '1111/home-left-' + index + } + } + }; + } + ); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].gpid).to.equal('1111/home-left-0'); + expect(payload.data[1].gpid).to.equal('1111/home-left-1'); + }); + + it('should not add gpid if ortb2Imp.ext.gpid is present but empty', function () { + const updatedBidRequests = bidRequests.map(bidRequest => ({ + ...bidRequest, + ortb2Imp: { + ext: { + gpid: '' + } + } + })); + + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + return payload.data.forEach(bid => { + expect(bid).not.to.have.property('gpid'); + }); + }); + + it('should not add gpid if ortb2Imp.ext.gpid is not present', function () { + const updatedBidRequests = bidRequests.map(bidRequest => ({ + ...bidRequest, + ortb2Imp: { + ext: { + } + } + })); + + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + return payload.data.forEach(bid => { + expect(bid).not.to.have.property('gpid'); + }); + }); + + it('should add dsa info to payload if available', function () { + const bidRequestWithDsa = Object.assign({}, bidderRequestDefault, { + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + } + } + } + }); + + const requestWithDsa = spec.buildRequests(bidRequests, bidRequestWithDsa); + const payload = JSON.parse(requestWithDsa.data); + + expect(payload.dsa).to.exist; + expect(payload.dsa).to.deep.equal( + { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + ); + + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); + expect(JSON.parse(defaultRequest.data).dsa).to.not.exist; + }); + }); + + describe('interpretResponse', function () { + it('should get correct bid responses', function () { + let bids = { + 'body': { + 'responses': [{ + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 250, + 'bidId': '3ede2a3fa0db94', + 'ttl': 360, + 'width': 300, + 'creativeId': 'er2ee', + 'gbPlacementId': 4242 + }, { + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 200, + 'bidId': '4fef3b4gb1ec15', + 'ttl': 360, + 'width': 350, + 'creativeId': 'fs3ff', + 'gbPlacementId': 4242, + 'dealId': 'ABC_123', + 'ext': { + 'dsa': { + 'behalf': 'some-behalf', + 'paid': 'some-paid', + 'transparency': [{ + 'domain': 'test.com', + 'dsaparams': [1, 2, 3] + }], + 'adrender': 1 + } + } + }] + } + }; + let expectedResponse = [ + { + 'cpm': 0.5, + 'width': 300, + 'height': 250, + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [] + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '3ede2a3fa0db94', + 'creativeId': 'er2ee', + 'gbPlacementId': 4242 + }, { + 'cpm': 0.5, + 'width': 350, + 'height': 200, + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [], + dsa: { + behalf: 'some-behalf', + paid: 'some-paid', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }], + adrender: 1 + } + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '4fef3b4gb1ec15', + 'creativeId': 'fs3ff', + 'gbPlacementId': 4242, + 'dealId': 'ABC_123' + } + ] + ; + + let result = spec.interpretResponse(bids); + expect(result).to.eql(expectedResponse); + }); + + it('handles nobid responses', function () { + let bids = { + 'body': { + 'responses': [] + } + }; + + let result = spec.interpretResponse(bids); + expect(result.length).to.equal(0); + }); + }); +}); + +let bidderRequestDefault = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 +}; + +let bidRequests = [ + { + 'bidder': 'greenbids', + 'params': { + 'gbPlacementId': 4242 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + } +]; + +function checkMediaTypesSizes(mediaTypes, expectedSizes) { + const bidRequestWithBannerSizes = Object.assign(bidRequests[0], mediaTypes); + const requestWithBannerSizes = spec.buildRequests([bidRequestWithBannerSizes], bidderRequestDefault); + const payloadWithBannerSizes = JSON.parse(requestWithBannerSizes.data); + + return payloadWithBannerSizes.data.forEach(bid => { + if (Array.isArray(expectedSizes)) { + expect(JSON.stringify(bid.sizes)).to.equal(JSON.stringify(expectedSizes)); + } else { + expect(bid.sizes[0]).to.equal(expectedSizes); + } + }); +} From 49050934dc60822d330e9c6059196145b5c4578b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20G?= Date: Wed, 13 Nov 2024 15:40:36 +0100 Subject: [PATCH 2/7] refacto to make the code easier and clearer --- modules/greenbidsBidAdapter.js | 218 +++++------- modules/tapnativeBidAdapter.js | 41 +++ modules/tapnativeBidAdapter.md | 61 ++++ src/utils/privacy.js | 53 +++ .../spec/modules/greenbidsBidAdapter_specs.js | 123 ++----- test/spec/modules/tapnativeBidAdapter_spec.js | 322 ++++++++++++++++++ 6 files changed, 581 insertions(+), 237 deletions(-) create mode 100644 modules/tapnativeBidAdapter.js create mode 100644 modules/tapnativeBidAdapter.md create mode 100644 src/utils/privacy.js create mode 100644 test/spec/modules/tapnativeBidAdapter_spec.js diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js index 7f5d7612411..1f21d0e1e29 100644 --- a/modules/greenbidsBidAdapter.js +++ b/modules/greenbidsBidAdapter.js @@ -1,4 +1,4 @@ -import { getValue, logError, deepAccess, parseSizesInput, isArray, getBidIdParameter } from '../src/utils.js'; +import { getValue, logError, deepAccess, parseSizesInput, getBidIdParameter } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { getDM, getHC, getHLen } from '../libraries/navigatorData/navigatorData.js'; @@ -11,18 +11,12 @@ import { getDM, getHC, getHLen } from '../libraries/navigatorData/navigatorData. const BIDDER_CODE = 'greenbids'; const GVL_ID = 1232; const ENDPOINT_URL = 'https://d.greenbids.ai/hb/bid-request'; -const gdprStatus = { - GDPR_APPLIES_PUBLISHER: 12, - GDPR_APPLIES_GLOBAL: 11, - GDPR_DOESNT_APPLY: 0, - CMP_NOT_FOUND_OR_ERROR: 22 -}; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const spec = { code: BIDDER_CODE, gvlid: GVL_ID, - supportedMediaTypes: ['banner'], + supportedMediaTypes: ['banner', 'video'], /** * Determines whether or not the given bid request is valid. * @@ -30,25 +24,22 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - let isValid = false; - if (typeof bid.params !== 'undefined') { - let isValidgbPlacementId = _validateId(getValue(bid.params, 'gbPlacementId')); - isValid = isValidgbPlacementId; - } - - if (!isValid) { - logError(' parameters are required. Bid aborted.'); + if (typeof bid.params !== 'undefined' && parseInt(getValue(bid.params, 'gbPlacementId')) > 0) { + return true; + } else { + logError('Greenbids bidder adapter requires gbPlacementId to be defined and a positive number'); + return false; } - return isValid; }, /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} an array of bids + * @param {validBidRequests[]} validBidRequests array of bids + * @param bidderRequest bidder request object * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - const bids = validBidRequests.map(buildRequestObject); + const bids = validBidRequests.map(cleanBidsInfo); const topWindow = window.top; const payload = { @@ -69,8 +60,7 @@ export const spec = { viewportWidth: topWindow.visualViewport?.width, hardwareConcurrency: getHC(), deviceMemory: getDM(), - hb_version: '$prebid.version$', - ...getSharedViewerIdParameters(validBidRequests), + prebid_version: '$prebid.version$', }; const firstBidRequest = validBidRequests[0]; @@ -79,35 +69,9 @@ export const spec = { payload.schain = firstBidRequest.schain; } - let gpp = bidderRequest.gppConsent; - if (bidderRequest && gpp) { - let isValidConsentString = typeof gpp.gppString === 'string'; - let validateApplicableSections = - Array.isArray(gpp.applicableSections) && - gpp.applicableSections.every((section) => typeof (section) === 'number') - payload.gpp = { - consentString: isValidConsentString ? gpp.gppString : '', - applicableSectionIds: validateApplicableSections ? gpp.applicableSections : [], - }; - } - - let gdpr = bidderRequest.gdprConsent; - if (bidderRequest && gdpr) { - let isCmp = typeof gdpr.gdprApplies === 'boolean'; - let isConsentString = typeof gdpr.consentString === 'string'; - let status = isCmp - ? findGdprStatus(gdpr.gdprApplies, gdpr.vendorData) - : gdprStatus.CMP_NOT_FOUND_OR_ERROR; - payload.gdpr_iab = { - consent: isConsentString ? gdpr.consentString : '', - status: status, - apiVersion: gdpr.apiVersion - }; - } - - if (bidderRequest && bidderRequest.uspConsent) { - payload.us_privacy = bidderRequest.uspConsent; - } + hydratePayloadWithGPPData(payload, bidderRequest.gppConsent); + hydratePayloadWithGDPRData(payload, bidderRequest.gdprConsent); + hydratePayloadWithUspConsentData(payload, bidderRequest.uspConsent); const userAgentClientHints = deepAccess(firstBidRequest, 'ortb2.device.sua'); if (userAgentClientHints) { @@ -147,7 +111,7 @@ export const spec = { currency: bid.currency, netRevenue: true, ttl: bid.ttl, - meta: { + meta: { advertiserDomains: bid && bid.adomain ? bid.adomain : [], }, ad: bid.ad, @@ -166,36 +130,9 @@ export const spec = { } }; -/** - * - * @param validBidRequests an array of bids - * @returns {{sharedViewerIdKey : 'sharedViewerIdValue'}} object with all sharedviewerids - */ -function getSharedViewerIdParameters(validBidRequests) { - const sharedViewerIdMapping = { - unifiedId2: 'uid2.id', // uid2IdSystem - liveRampId: 'idl_env', // identityLinkIdSystem - lotamePanoramaId: 'lotamePanoramaId', // lotamePanoramaIdSystem - id5Id: 'id5id.uid', // id5IdSystem - criteoId: 'criteoId', // criteoIdSystem - yahooConnectId: 'connectId', // connectIdSystem - quantcastId: 'quantcastId', // quantcastIdSystem - epsilonPublisherLinkId: 'publinkId', // publinkIdSystem - publisherFirstPartyViewerId: 'pubcid', // sharedIdSystem - merkleId: 'merkleId.id', // merkleIdSystem - kinessoId: 'kpuid' // kinessoIdSystem - } +registerBidder(spec); - let sharedViewerIdObject = {}; - for (const sharedViewerId in sharedViewerIdMapping) { - const key = sharedViewerIdMapping[sharedViewerId]; - const value = deepAccess(validBidRequests, `0.userId.${key}`); - if (value) { - sharedViewerIdObject[sharedViewerId] = value; - } - } - return sharedViewerIdObject; -} +// Page info retrival function getReferrerInfo(bidderRequest) { let ref = ''; @@ -207,28 +144,24 @@ function getReferrerInfo(bidderRequest) { function getPageTitle() { try { - const ogTitle = window.top.document.querySelector('meta[property="og:title"]') - + const ogTitle = window.top.document.querySelector('meta[property="og:title"]'); return window.top.document.title || (ogTitle && ogTitle.content) || ''; } catch (e) { - const ogTitle = document.querySelector('meta[property="og:title"]') - + const ogTitle = document.querySelector('meta[property="og:title"]'); return document.title || (ogTitle && ogTitle.content) || ''; } } function getPageDescription() { - let element; - try { - element = window.top.document.querySelector('meta[name="description"]') || - window.top.document.querySelector('meta[property="og:description"]') + const element = window.top.document.querySelector('meta[name="description"]') || + window.top.document.querySelector('meta[property="og:description"]'); + return (element && element.content) || ''; } catch (e) { - element = document.querySelector('meta[name="description"]') || - document.querySelector('meta[property="og:description"]') + const element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]'); + return (element && element.content) || ''; } - - return (element && element.content) || ''; } function getConnectionDownLink(nav) { @@ -264,61 +197,76 @@ function getTimeToFirstByte(win) { return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; } -function findGdprStatus(gdprApplies, gdprData) { - let status = gdprStatus.GDPR_APPLIES_PUBLISHER; - if (gdprApplies) { - if (gdprData && !gdprData.isServiceSpecific) { - status = gdprStatus.GDPR_APPLIES_GLOBAL; - } - } else { - status = gdprStatus.GDPR_DOESNT_APPLY; - } - return status; -} - -function buildRequestObject(bid) { +function cleanBidsInfo(bids) { const reqObj = {}; - let gbPlacementId = getValue(bid.params, 'gbPlacementId'); - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); - reqObj.sizes = getSizes(bid); - reqObj.bidId = getBidIdParameter('bidId', bid); - reqObj.bidderRequestId = getBidIdParameter('bidderRequestId', bid); + let gbPlacementId = getValue(bids.params, 'gbPlacementId'); + const gpid = deepAccess(bids, 'ortb2Imp.ext.gpid'); + reqObj.sizes = getSizes(bids); + reqObj.bidId = getBidIdParameter('bidId', bids); + reqObj.bidderRequestId = getBidIdParameter('bidderRequestId', bids); reqObj.gbPlacementId = parseInt(gbPlacementId, 10); - reqObj.adUnitCode = getBidIdParameter('adUnitCode', bid); - reqObj.transactionId = bid.ortb2Imp?.ext?.tid || ''; + reqObj.adUnitCode = getBidIdParameter('adUnitCode', bids); + reqObj.transactionId = bids.ortb2Imp?.ext?.tid || ''; if (gpid) { reqObj.gpid = gpid; } return reqObj; } function getSizes(bid) { - return parseSizesInput(concatSizes(bid)); + return parseSizesInput(bid.sizes); } -function concatSizes(bid) { - let bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); +// Privacy handling + +export function hydratePayloadWithGPPData(payload, gppData) { + if (gppData) { + let isValidConsentString = typeof gppData.gppString === 'string'; + let validateApplicableSections = + Array.isArray(gppData.applicableSections) && + gppData.applicableSections.every((section) => typeof (section) === 'number') + payload.gpp = { + consentString: isValidConsentString ? gppData.gppString : '', + applicableSectionIds: validateApplicableSections ? gppData.applicableSections : [], + }; + } + return payload; +} - if (isArray(bannerSizes)) { - let mediaTypesSizes = [bannerSizes]; - return mediaTypesSizes - .reduce(function (acc, currSize) { - if (isArray(currSize)) { - if (isArray(currSize[0])) { - currSize.forEach(function (childSize) { - acc.push(childSize); - }) - } else { - acc.push(currSize); - } - } - return acc; - }, []); - } else { - return bid.sizes; +export function hydratePayloadWithGDPRData(payload, gdprData) { + if (gdprData) { + let isCmp = typeof gdprData.gdprApplies === 'boolean'; + let isConsentString = typeof gdprData.consentString === 'string'; + let status = isCmp + ? findGdprStatus(gdprData.gdprApplies, gdprData.vendorData) + : gdprStatus.CMP_NOT_FOUND_OR_ERROR; + payload.gdpr_iab = { + consent: isConsentString ? gdprData.consentString : '', + status: status, + apiVersion: gdprData.apiVersion + }; } } -function _validateId(id) { - return (parseInt(id) > 0); +export function hydratePayloadWithUspConsentData(payload, uspConsentData) { + if (uspConsentData) { + payload.us_privacy = uspConsentData; + } } -registerBidder(spec); +const gdprStatus = { + GDPR_APPLIES_PUBLISHER: 12, + GDPR_APPLIES_GLOBAL: 11, + GDPR_DOESNT_APPLY: 0, + CMP_NOT_FOUND_OR_ERROR: 22 +}; + +function findGdprStatus(gdprApplies, gdprData) { + let status = gdprStatus.GDPR_APPLIES_PUBLISHER; + if (gdprApplies) { + if (gdprData && !gdprData.isServiceSpecific) { + status = gdprStatus.GDPR_APPLIES_GLOBAL; + } + } else { + status = gdprStatus.GDPR_DOESNT_APPLY; + } + return status; +} diff --git a/modules/tapnativeBidAdapter.js b/modules/tapnativeBidAdapter.js new file mode 100644 index 00000000000..a09c746c960 --- /dev/null +++ b/modules/tapnativeBidAdapter.js @@ -0,0 +1,41 @@ +import { + BANNER, + NATIVE +} from '../src/mediaTypes.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { + getBannerRequest, + getBannerResponse, + getNativeResponse, +} from '../libraries/audUtils/bidderUtils.js'; + +const ENDPOINT = 'https://rtb-east.tapnative.com/hb'; +// Export const spec +export const spec = { + code: 'tapnative', + supportedMediaTypes: [BANNER, NATIVE], + // Determines whether or not the given bid request is valid + isBidRequestValid: function(bidParam) { + return !!(bidParam.params.placement_id); + }, + // Make a server request from the list of BidRequests + buildRequests: function(bidRequests, serverRequest) { + // Get Requests based on media types + return getBannerRequest(bidRequests, serverRequest, ENDPOINT); + }, + // Unpack the response from the server into a list of bids. + interpretResponse: function(serverResponse, serverRequest) { + let bidderResponse = {}; + const mType = JSON.parse(serverRequest.data)[0].MediaType; + if (mType == BANNER) { + bidderResponse = getBannerResponse(serverResponse, BANNER); + } else if (mType == NATIVE) { + bidderResponse = getNativeResponse(serverResponse, serverRequest, NATIVE); + } + return bidderResponse; + } +} + +registerBidder(spec); diff --git a/modules/tapnativeBidAdapter.md b/modules/tapnativeBidAdapter.md new file mode 100644 index 00000000000..2cc39475dcb --- /dev/null +++ b/modules/tapnativeBidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +``` +Module Name: Tapnative Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@tapnative.com +``` + +# Description + +Tapnative currently supports the BANNER and NATIVE type ads through prebid js + +Module that connects to tapnative's demand sources. + +# Test Request +``` + var adUnits = [ + { + code: 'display-ad', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + } + bids: [ + { + bidder: 'tapnative', + params: { + placement_id: 111520, // Required parameter + width: 300, // Optional parameter + height: 250, // Optional parameter + bid_floor: 0.5 // Optional parameter + } + } + ] + }, + { + code: 'native-ad-container', + mediaTypes: { + native: { + title: { required: true, len: 100 }, + image: { required: true, sizes: [300, 250] }, + sponsored: { required: false }, + clickUrl: { required: true }, + desc: { required: true }, + icon: { required: false, sizes: [50, 50] }, + cta: { required: false } + } + }, + bids: [ + { + bidder: 'tapnative', + params: { + placement_id: 111519, // Required parameter + bid_floor: 1 // Optional parameter + } + } + ] + } + ]; +``` diff --git a/src/utils/privacy.js b/src/utils/privacy.js new file mode 100644 index 00000000000..20bc7909b3d --- /dev/null +++ b/src/utils/privacy.js @@ -0,0 +1,53 @@ +export function hydratePayloadWithGPPData(payload, gppData) { + if (gppData) { + let isValidConsentString = typeof gppData.gppString === 'string'; + let validateApplicableSections = + Array.isArray(gppData.applicableSections) && + gppData.applicableSections.every((section) => typeof (section) === 'number') + payload.gpp = { + consentString: isValidConsentString ? gppData.gppString : '', + applicableSectionIds: validateApplicableSections ? gppData.applicableSections : [], + }; + } + return payload; +} + +export function hydratePayloadWithGDPRData(payload, gdprData) { + if (gdprData) { + let isCmp = typeof gdprData.gdprApplies === 'boolean'; + let isConsentString = typeof gdprData.consentString === 'string'; + let status = isCmp + ? findGdprStatus(gdprData.gdprApplies, gdprData.vendorData) + : gdprStatus.CMP_NOT_FOUND_OR_ERROR; + payload.gdpr_iab = { + consent: isConsentString ? gdprData.consentString : '', + status: status, + apiVersion: gdprData.apiVersion + }; + } +} + +export function hydratePayloadWithUspConsentData(payload, uspConsentData) { + if (uspConsentData) { + payload.us_privacy = uspConsentData; + } +} + +const gdprStatus = { + GDPR_APPLIES_PUBLISHER: 12, + GDPR_APPLIES_GLOBAL: 11, + GDPR_DOESNT_APPLY: 0, + CMP_NOT_FOUND_OR_ERROR: 22 +}; + +function findGdprStatus(gdprApplies, gdprData) { + let status = gdprStatus.GDPR_APPLIES_PUBLISHER; + if (gdprApplies) { + if (gdprData && !gdprData.isServiceSpecific) { + status = gdprStatus.GDPR_APPLIES_GLOBAL; + } + } else { + status = gdprStatus.GDPR_DOESNT_APPLY; + } + return status; +} diff --git a/test/spec/modules/greenbidsBidAdapter_specs.js b/test/spec/modules/greenbidsBidAdapter_specs.js index 6bfa1b96135..31bf949a8bf 100644 --- a/test/spec/modules/greenbidsBidAdapter_specs.js +++ b/test/spec/modules/greenbidsBidAdapter_specs.js @@ -39,6 +39,25 @@ describe('greenbidsBidAdapter', () => { it('should return true when required params found', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); + + let bidNonGBCompatible = { + 'bidder': 'greenbids', + }; + + it('should return false when required params are not found', function () { + expect(spec.isBidRequestValid(bidNonGBCompatible)).to.equal(false); + }); + + let bidNonGBCompatible2 = { + 'bidder': 'greenbids', + 'params': { + 'gbPlacementId': 'toto' + }, + }; + + it('should return false when required the placement is not a number', function () { + expect(spec.isBidRequestValid(bidNonGBCompatible2)).to.equal(false); + }); }) describe('buildRequests', function () { it('should send bid request to ENDPOINT via POST', function () { @@ -679,114 +698,14 @@ describe('greenbidsBidAdapter', () => { const mediaTypesBannerSize = { 'mediaTypes': { 'banner': { - 'sizes': [46, 48] + 'sizes': [300, 250] } } }; - checkMediaTypesSizes(mediaTypesBannerSize, '46x48'); + checkMediaTypesSizes(mediaTypesBannerSize, '300x250'); }); }); - describe('User IDs', function () { - const baseBidRequest = { - 'bidder': 'teads', - 'params': { - 'placementId': 10433394, - 'pageId': 1234 - }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'creativeId': 'er2ee', - 'deviceWidth': 1680 - }; - - const userIdModules = { - unifiedId2: { uid2: { id: 'unifiedId2-id' } }, - liveRampId: { idl_env: 'liveRampId-id' }, - lotamePanoramaId: { lotamePanoramaId: 'lotamePanoramaId-id' }, - id5Id: { id5id: { uid: 'id5Id-id' } }, - criteoId: { criteoId: 'criteoId-id' }, - yahooConnectId: { connectId: 'yahooConnectId-id' }, - quantcastId: { quantcastId: 'quantcastId-id' }, - epsilonPublisherLinkId: { publinkId: 'epsilonPublisherLinkId-id' }, - publisherFirstPartyViewerId: { pubcid: 'publisherFirstPartyViewerId-id' }, - merkleId: { merkleId: { id: 'merkleId-id' } }, - kinessoId: { kpuid: 'kinessoId-id' } - }; - - describe('User Id Modules', function () { - it(`should not add param to payload if user id system is not enabled`, function () { - const bidRequest = { - ...baseBidRequest, - userId: {} // no property -> assumption that the system is disabled - }; - - const request = spec.buildRequests([bidRequest], bidderRequestDefault); - const payload = JSON.parse(request.data); - - for (const userId in userIdModules) { - expect(payload, userId).not.to.have.property(userId); - } - }); - - it(`should not add param to payload if user id field is absent`, function () { - const request = spec.buildRequests([baseBidRequest], bidderRequestDefault); - const payload = JSON.parse(request.data); - - for (const userId in userIdModules) { - expect(payload, userId).not.to.have.property(userId); - } - }); - - it(`should not add param to payload if user id is enabled but there is no value`, function () { - const bidRequest = { - ...baseBidRequest, - userId: { - idl_env: '', - pubcid: 'publisherFirstPartyViewerId-id' - } - }; - - const request = spec.buildRequests([bidRequest], bidderRequestDefault); - const payload = JSON.parse(request.data); - - expect(payload).not.to.have.property('liveRampId'); - expect(payload['publisherFirstPartyViewerId']).to.equal('publisherFirstPartyViewerId-id'); - }); - - it(`should add userId param to payload for each enabled user id system`, function () { - let userIdObject = {}; - for (const userId in userIdModules) { - userIdObject = { - ...userIdObject, - ...userIdModules[userId] - } - } - const bidRequest = { - ...baseBidRequest, - userId: userIdObject - }; - - const request = spec.buildRequests([bidRequest], bidderRequestDefault); - const payload = JSON.parse(request.data); - - expect(payload['unifiedId2']).to.equal('unifiedId2-id'); - expect(payload['liveRampId']).to.equal('liveRampId-id'); - expect(payload['lotamePanoramaId']).to.equal('lotamePanoramaId-id'); - expect(payload['id5Id']).to.equal('id5Id-id'); - expect(payload['criteoId']).to.equal('criteoId-id'); - expect(payload['yahooConnectId']).to.equal('yahooConnectId-id'); - expect(payload['quantcastId']).to.equal('quantcastId-id'); - expect(payload['epsilonPublisherLinkId']).to.equal('epsilonPublisherLinkId-id'); - expect(payload['publisherFirstPartyViewerId']).to.equal('publisherFirstPartyViewerId-id'); - expect(payload['merkleId']).to.equal('merkleId-id'); - expect(payload['kinessoId']).to.equal('kinessoId-id'); - }); - }) - }); describe('Global Placement Id', function () { let bidRequests = [ { diff --git a/test/spec/modules/tapnativeBidAdapter_spec.js b/test/spec/modules/tapnativeBidAdapter_spec.js new file mode 100644 index 00000000000..8a94b93168c --- /dev/null +++ b/test/spec/modules/tapnativeBidAdapter_spec.js @@ -0,0 +1,322 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/tapnativeBidAdapter.js'; +import * as utils from '../../../src/utils.js'; + +describe('tapnative adapter', function () { + let bannerRequest, nativeRequest; + let bannerResponse, nativeResponse, invalidBannerResponse, invalidNativeResponse; + + beforeEach(function () { + bannerRequest = [ + { + bidder: 'tapnative', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placement_id: 111520, + bid_floor: 0.5 + } + } + ]; + nativeRequest = [ + { + bidder: 'tapnative', + mediaTypes: { + native: { + title: { required: true, len: 100 }, + image: { required: true, sizes: [300, 250] }, + sponsored: { required: false }, + clickUrl: { required: true }, + desc: { required: true }, + icon: { required: false, sizes: [50, 50] }, + cta: { required: false } + } + }, + params: { + placement_id: 111519, + bid_floor: 1 + } + } + ]; + bannerResponse = { + 'body': { + 'id': '006ac3b3-67f0-43bf-a33a-388b2f869fef', + 'seatbid': [{ + 'bid': [{ + 'id': '049d07ed-c07e-4890-9f19-5cf41406a42d', + 'impid': '286e606ac84a09', + 'price': 0.11, + 'adid': '368853', + 'adm': "", + 'adomain': ['google.com'], + 'iurl': 'https://cdn.tapnative.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'tapnative', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + nativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': '{\"native\":{\"ver\":\"1.1\",\"assets\": [{\"id\":1,\"required\":1,\"title\":{\"text\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":2,\"required\":1,\"img\":{\"url\":\"https://cdn.tapnative.com/1_368852_0.png\",\"w\":500,\"h\":300,\"type\":\"3\"}},{\"id\":3,\"required\":0,\"data\":{\"value\":\"Diabetes In Control. A free weekly diabetes newsletter for Medical Professionals.\"}},{\"id\":4,\"required\":1,\"data\":{\"value\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":6,\"required\":1,\"data\":{\"value\":\"URL\"}}],\"link\":{\"url\":\"https://r.tapnative.com/adx-rtb-d/servlet/WebF_AdManager.AdLinkManager?qs=H4sIAAAAAAAAAx2U2QHDIAxDVwKDr3F84P1HqNLPtMSRpSeG9RiPXH+5a474KzO/47YX7UoP50m61fLujlNjb76/8ZiblkimHq5nL/ZRedp3031x1tnk55LjSNN6h9/Zq+qmaLLuWTl74m1ZJKnb+m2OtQm/3L4sb933pM92qMOgjJ41MYmPXKnndRVKs+9bfSEumoZIFpTXuXbCP+WXuzl725E3O+9odi5OJrnBzhwjx9+UnFN3nTNt1/HY5aeljKtvZYpoJHNXr8BWa8ysKQY7ZmNA3DHK2qRwY7+zLu+xm9z5eheJ4Pv2usSptvO3p7JHrnXn0T5yVWdccp9Yz7hhoz2iu2zqsXsGFZ9hh14J6yU4TkJ0BgnOY8tY3tS+n2qsw7xZfKuanSNbAo+9nkJ83i20+FwhfbJeDVOllXsdxmDWauYcSRgS9+yG5qHwUDjAxxA0iZnOjlsnI+y09+ATeTEwbAVGgp0Qu/ceP0kjUvpu1Ty7O9MoegfrmLPxdjUh3mJL+XhARby+Ax8iBckf6BQdn9W+DMlvmlzYLuLlIy7YociFOIvXvEiYYCMboVk8BLHbnw3Zmr5us3xbjtXL67L96F15acJXkM5BOmTaUbBkYGdCI+Et8XmlpbuE3xVQwmxryc2y4wP3ByuuP8GogPZz8OpPaBv8diWWUTrC2nnLhdNUrJRTKc9FepDvwHTDwfbbMCTSb4LhUIFkyFrw/i7GtkPi6NCCai6N47TgNsTnzZWRoVtOSLq7FsLiF29y0Gj0GHVPVYG3QOPS7Swc3UuiFAQZJx3YvpHA2geUgVBASMEL4vcDi2Dw3NPtBSC4EQEvH/uMILu6WyUwraywTeVpoqoHTqOoD84FzReKoWemJy6jyuiBieGlQIe6wY2elTaMOwEUFF5NagzPj6nauc0+aXzQN3Q72hxFAgtfORK60RRAHYZLYymIzSJcXLgRFsqrb1UoD+5Atq7TWojaLTfOyUvH9EeJvZEOilQAXrf/ALoI8ZhABQAA\"},\"imptrackers\":[\"https://i.tapnative.com/adx-rtb-d/servlet/WebF_AdManager.ImpCounter?price=${AUCTION_PRICE}&ids=111519,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=https%3A%2F%2Fqa-jboss.audiencelogy.com%2Ftn_native_prod.html\",\"https://i.tapnative.com/adx-rtb-d/servlet/WebF_AdManager.ImpTracker?price=${AUCTION_PRICE}&ids=111519,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=\",\"https://rtb-east.tapnative.com:9001/beacon?uid=44636f6605b06ec6d4389d6efb7e5054&cc=468132&fccap=5&nid=1\"]}}', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.tapnative.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'tapnative', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidBannerResponse = { + 'body': { + 'id': '006ac3b3-67f0-43bf-a33a-388b2f869fef', + 'seatbid': [{ + 'bid': [{ + 'id': '049d07ed-c07e-4890-9f19-5cf41406a42d', + 'impid': '286e606ac84a09', + 'price': 0.11, + 'adid': '368853', + 'adm': 'invalid response', + 'adomain': ['google.com'], + 'iurl': 'https://cdn.tapnative.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'tapnative', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidNativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': 'invalid response', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.tapnative.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'tapnative', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + }); + + describe('validations', function () { + it('isBidValid : placement_id is passed', function () { + let bid = { + bidder: 'tapnative', + params: { + placement_id: 111520 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(true); + }); + it('isBidValid : placement_id is not passed', function () { + let bid = { + bidder: 'tapnative', + params: { + width: 300, + height: 250, + domain: '', + bid_floor: 0.5 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + }); + describe('Validate Banner Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(bannerRequest), + bidRequest = spec.buildRequests(bannerRequest); + expect(bannerRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(bannerRequest); + expect(_Request.url).to.equal('https://rtb-east.tapnative.com/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(bannerRequest[0].bidId); + expect(data[0].placementId).to.equal(111520); + }); + it('Validate bid request : ad size', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].imp[0].banner).to.be.a('object'); + expect(data[0].imp[0].banner.w).to.equal(300); + expect(data[0].imp[0].banner.h).to.equal(250); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(bannerRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate banner response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(bannerRequest); + let bResponse = spec.interpretResponse(bannerResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(bannerResponse.body.seatbid[0].bid[0].impid); + expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('banner'); + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['google.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(bannerResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(bannerResponse.body.seatbid[0].bid[0].dealId); + }); + it('Invalid bid response check ', function () { + let bRequest = spec.buildRequests(bannerRequest); + let response = spec.interpretResponse(invalidBannerResponse, bRequest); + expect(response[0].ad).to.equal('invalid response'); + }); + }); + describe('Validate Native Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(nativeRequest), + bidRequest = spec.buildRequests(nativeRequest); + expect(nativeRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(nativeRequest); + expect(_Request.url).to.equal('https://rtb-east.tapnative.com/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(nativeRequest[0].bidId); + expect(data[0].placementId).to.equal(111519); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(nativeRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate native response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(nativeRequest); + let bResponse = spec.interpretResponse(nativeResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(nativeResponse.body.seatbid[0].bid[0].impid); + // expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + // expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('native'); + expect(bResponse[0].native.clickUrl).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.impressionTrackers).to.be.an('array').with.length.above(0); + expect(bResponse[0].native.title).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.image.url).to.be.a('string').and.not.be.empty; + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['www.diabetesincontrol.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(nativeResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(nativeResponse.body.seatbid[0].bid[0].dealId); + }); + }); + describe('GPP and coppa', function () { + it('Request params check with GPP Consent', function () { + let bidderReq = { gppConsent: { gppString: 'gpp-string-test', applicableSections: [5] } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-string-test'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it('Request params check with GPP Consent read from ortb2', function () { + let bidderReq = { + ortb2: { + regs: { + gpp: 'gpp-test-string', + gpp_sid: [5] + } + } + }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-test-string'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it(' Bid request should have coppa flag if its true', () => { + let bidderReq = { ortb2: { regs: { coppa: 1 } } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.coppa).to.equal(1); + }); + }); +}); From 6eaa0e6a6fa40cb0458d1de01f9c1177e51e2ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20G?= Date: Wed, 13 Nov 2024 15:55:48 +0100 Subject: [PATCH 3/7] refacto to make the code easier and clearer --- modules/greenbidsBidAdapter.js | 10 ++-- src/utils/privacy.js | 53 ------------------- .../spec/modules/greenbidsBidAdapter_specs.js | 14 ++--- 3 files changed, 12 insertions(+), 65 deletions(-) delete mode 100644 src/utils/privacy.js diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js index 1f21d0e1e29..a5a385a6bf2 100644 --- a/modules/greenbidsBidAdapter.js +++ b/modules/greenbidsBidAdapter.js @@ -24,10 +24,10 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - if (typeof bid.params !== 'undefined' && parseInt(getValue(bid.params, 'gbPlacementId')) > 0) { + if (typeof bid.params !== 'undefined' && parseInt(getValue(bid.params, 'placementId')) > 0) { return true; } else { - logError('Greenbids bidder adapter requires gbPlacementId to be defined and a positive number'); + logError('Greenbids bidder adapter requires placementId to be defined and a positive number'); return false; } }, @@ -117,7 +117,7 @@ export const spec = { ad: bid.ad, requestId: bid.bidId, creativeId: bid.creativeId, - gbPlacementId: bid.gbPlacementId, + placementId: bid.placementId, }; if (bid.dealId) { bidResponse.dealId = bid.dealId @@ -199,12 +199,12 @@ function getTimeToFirstByte(win) { function cleanBidsInfo(bids) { const reqObj = {}; - let gbPlacementId = getValue(bids.params, 'gbPlacementId'); + let placementId = getValue(bids.params, 'placementId'); const gpid = deepAccess(bids, 'ortb2Imp.ext.gpid'); reqObj.sizes = getSizes(bids); reqObj.bidId = getBidIdParameter('bidId', bids); reqObj.bidderRequestId = getBidIdParameter('bidderRequestId', bids); - reqObj.gbPlacementId = parseInt(gbPlacementId, 10); + reqObj.placementId = parseInt(placementId, 10); reqObj.adUnitCode = getBidIdParameter('adUnitCode', bids); reqObj.transactionId = bids.ortb2Imp?.ext?.tid || ''; if (gpid) { reqObj.gpid = gpid; } diff --git a/src/utils/privacy.js b/src/utils/privacy.js deleted file mode 100644 index 20bc7909b3d..00000000000 --- a/src/utils/privacy.js +++ /dev/null @@ -1,53 +0,0 @@ -export function hydratePayloadWithGPPData(payload, gppData) { - if (gppData) { - let isValidConsentString = typeof gppData.gppString === 'string'; - let validateApplicableSections = - Array.isArray(gppData.applicableSections) && - gppData.applicableSections.every((section) => typeof (section) === 'number') - payload.gpp = { - consentString: isValidConsentString ? gppData.gppString : '', - applicableSectionIds: validateApplicableSections ? gppData.applicableSections : [], - }; - } - return payload; -} - -export function hydratePayloadWithGDPRData(payload, gdprData) { - if (gdprData) { - let isCmp = typeof gdprData.gdprApplies === 'boolean'; - let isConsentString = typeof gdprData.consentString === 'string'; - let status = isCmp - ? findGdprStatus(gdprData.gdprApplies, gdprData.vendorData) - : gdprStatus.CMP_NOT_FOUND_OR_ERROR; - payload.gdpr_iab = { - consent: isConsentString ? gdprData.consentString : '', - status: status, - apiVersion: gdprData.apiVersion - }; - } -} - -export function hydratePayloadWithUspConsentData(payload, uspConsentData) { - if (uspConsentData) { - payload.us_privacy = uspConsentData; - } -} - -const gdprStatus = { - GDPR_APPLIES_PUBLISHER: 12, - GDPR_APPLIES_GLOBAL: 11, - GDPR_DOESNT_APPLY: 0, - CMP_NOT_FOUND_OR_ERROR: 22 -}; - -function findGdprStatus(gdprApplies, gdprData) { - let status = gdprStatus.GDPR_APPLIES_PUBLISHER; - if (gdprApplies) { - if (gdprData && !gdprData.isServiceSpecific) { - status = gdprStatus.GDPR_APPLIES_GLOBAL; - } - } else { - status = gdprStatus.GDPR_DOESNT_APPLY; - } - return status; -} diff --git a/test/spec/modules/greenbidsBidAdapter_specs.js b/test/spec/modules/greenbidsBidAdapter_specs.js index 31bf949a8bf..8f37efef299 100644 --- a/test/spec/modules/greenbidsBidAdapter_specs.js +++ b/test/spec/modules/greenbidsBidAdapter_specs.js @@ -26,7 +26,7 @@ describe('greenbidsBidAdapter', () => { let bid = { 'bidder': 'greenbids', 'params': { - 'gbPlacementId': 4242 + 'placementId': 4242 }, 'adUnitCode': 'adunit-code', 'sizes': [[300, 250], [300, 600]], @@ -51,7 +51,7 @@ describe('greenbidsBidAdapter', () => { let bidNonGBCompatible2 = { 'bidder': 'greenbids', 'params': { - 'gbPlacementId': 'toto' + 'placementId': 'toto' }, }; @@ -841,7 +841,7 @@ describe('greenbidsBidAdapter', () => { 'ttl': 360, 'width': 300, 'creativeId': 'er2ee', - 'gbPlacementId': 4242 + 'placementId': 4242 }, { 'ad': AD_SCRIPT, 'cpm': 0.5, @@ -851,7 +851,7 @@ describe('greenbidsBidAdapter', () => { 'ttl': 360, 'width': 350, 'creativeId': 'fs3ff', - 'gbPlacementId': 4242, + 'placementId': 4242, 'dealId': 'ABC_123', 'ext': { 'dsa': { @@ -881,7 +881,7 @@ describe('greenbidsBidAdapter', () => { 'ad': AD_SCRIPT, 'requestId': '3ede2a3fa0db94', 'creativeId': 'er2ee', - 'gbPlacementId': 4242 + 'placementId': 4242 }, { 'cpm': 0.5, 'width': 350, @@ -904,7 +904,7 @@ describe('greenbidsBidAdapter', () => { 'ad': AD_SCRIPT, 'requestId': '4fef3b4gb1ec15', 'creativeId': 'fs3ff', - 'gbPlacementId': 4242, + 'placementId': 4242, 'dealId': 'ABC_123' } ] @@ -937,7 +937,7 @@ let bidRequests = [ { 'bidder': 'greenbids', 'params': { - 'gbPlacementId': 4242 + 'placementId': 4242 }, 'adUnitCode': 'adunit-code', 'sizes': [[300, 250], [300, 600]], From 052803508c1bcf324363d1979a5038e8fcd91f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20G?= Date: Wed, 20 Nov 2024 11:44:52 +0100 Subject: [PATCH 4/7] Alexis' review --- modules/greenbidsBidAdapter.js | 45 ++- .../spec/modules/greenbidsBidAdapter_specs.js | 311 +++++++++++------- 2 files changed, 220 insertions(+), 136 deletions(-) diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js index a5a385a6bf2..ea509512eb5 100644 --- a/modules/greenbidsBidAdapter.js +++ b/modules/greenbidsBidAdapter.js @@ -1,4 +1,4 @@ -import { getValue, logError, deepAccess, parseSizesInput, getBidIdParameter } from '../src/utils.js'; +import { getValue, logError, deepAccess, parseSizesInput, getBidIdParameter, logInfo } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { getDM, getHC, getHLen } from '../libraries/navigatorData/navigatorData.js'; @@ -25,6 +25,7 @@ export const spec = { */ isBidRequestValid: function (bid) { if (typeof bid.params !== 'undefined' && parseInt(getValue(bid.params, 'placementId')) > 0) { + logInfo('Greenbids bidder adapter valid bid request'); return true; } else { logError('Greenbids bidder adapter requires placementId to be defined and a positive number'); @@ -69,8 +70,8 @@ export const spec = { payload.schain = firstBidRequest.schain; } - hydratePayloadWithGPPData(payload, bidderRequest.gppConsent); - hydratePayloadWithGDPRData(payload, bidderRequest.gdprConsent); + hydratePayloadWithGppData(payload, bidderRequest.gppConsent); + hydratePayloadWithGdprData(payload, bidderRequest.gdprConsent); hydratePayloadWithUspConsentData(payload, bidderRequest.uspConsent); const userAgentClientHints = deepAccess(firstBidRequest, 'ortb2.device.sua'); @@ -87,22 +88,20 @@ export const spec = { return { method: 'POST', url: ENDPOINT_URL, - data: payloadString + data: payloadString, }; }, /** * Unpack the response from the server into a list of bids. * * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. + * @return {Bid[]} An array of bids which were nested inside the server response. */ - interpretResponse: function (serverResponse, bidderRequest) { + interpretResponse: function (serverResponse) { serverResponse = serverResponse.body; - if (!serverResponse.responses) { return []; } - return serverResponse.responses.map((bid) => { const bidResponse = { cpm: bid.cpm, @@ -110,6 +109,7 @@ export const spec = { height: bid.height, currency: bid.currency, netRevenue: true, + size: bid.size, ttl: bid.ttl, meta: { advertiserDomains: bid && bid.adomain ? bid.adomain : [], @@ -133,7 +133,6 @@ export const spec = { registerBidder(spec); // Page info retrival - function getReferrerInfo(bidderRequest) { let ref = ''; if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) { @@ -217,7 +216,7 @@ function getSizes(bid) { // Privacy handling -export function hydratePayloadWithGPPData(payload, gppData) { +export function hydratePayloadWithGppData(payload, gppData) { if (gppData) { let isValidConsentString = typeof gppData.gppString === 'string'; let validateApplicableSections = @@ -228,22 +227,20 @@ export function hydratePayloadWithGPPData(payload, gppData) { applicableSectionIds: validateApplicableSections ? gppData.applicableSections : [], }; } - return payload; } -export function hydratePayloadWithGDPRData(payload, gdprData) { - if (gdprData) { - let isCmp = typeof gdprData.gdprApplies === 'boolean'; - let isConsentString = typeof gdprData.consentString === 'string'; - let status = isCmp - ? findGdprStatus(gdprData.gdprApplies, gdprData.vendorData) - : gdprStatus.CMP_NOT_FOUND_OR_ERROR; - payload.gdpr_iab = { - consent: isConsentString ? gdprData.consentString : '', - status: status, - apiVersion: gdprData.apiVersion - }; - } +export function hydratePayloadWithGdprData(payload, gdprData) { + if (!gdprData) { return; } + let isCmp = typeof gdprData.gdprApplies === 'boolean'; + let isConsentString = typeof gdprData.consentString === 'string'; + let status = isCmp + ? findGdprStatus(gdprData.gdprApplies, gdprData.vendorData) + : gdprStatus.CMP_NOT_FOUND_OR_ERROR; + payload.gdpr_iab = { + consent: isConsentString ? gdprData.consentString : '', + status: status, + apiVersion: gdprData.apiVersion + }; } export function hydratePayloadWithUspConsentData(payload, uspConsentData) { diff --git a/test/spec/modules/greenbidsBidAdapter_specs.js b/test/spec/modules/greenbidsBidAdapter_specs.js index 8f37efef299..014c3545cad 100644 --- a/test/spec/modules/greenbidsBidAdapter_specs.js +++ b/test/spec/modules/greenbidsBidAdapter_specs.js @@ -40,23 +40,21 @@ describe('greenbidsBidAdapter', () => { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - let bidNonGBCompatible = { - 'bidder': 'greenbids', - }; - it('should return false when required params are not found', function () { - expect(spec.isBidRequestValid(bidNonGBCompatible)).to.equal(false); + let bidNonGbCompatible = { + 'bidder': 'greenbids', + }; + expect(spec.isBidRequestValid(bidNonGbCompatible)).to.equal(false); }); - let bidNonGBCompatible2 = { - 'bidder': 'greenbids', - 'params': { - 'placementId': 'toto' - }, - }; - - it('should return false when required the placement is not a number', function () { - expect(spec.isBidRequestValid(bidNonGBCompatible2)).to.equal(false); + it('should return false when the placement is not a number', function () { + let bidNonGbCompatible = { + 'bidder': 'greenbids', + 'params': { + 'placementId': 'toto' + }, + }; + expect(spec.isBidRequestValid(bidNonGbCompatible)).to.equal(false); }); }) describe('buildRequests', function () { @@ -184,20 +182,35 @@ describe('greenbidsBidAdapter', () => { expect(payload.referrer).to.deep.equal('https://example.com/page.html') }); - it('should add networkBandwidth info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderRequestDefault); - const payload = JSON.parse(request.data); + const originalConnection = window.navigator.connection; + const mockConnection = { downlink: 10 }; - const bandwidth = window.navigator && window.navigator.connection && window.navigator.connection.downlink; + const setNavigatorConnection = (connection) => { + Object.defineProperty(window.navigator, 'connection', { + value: connection, + configurable: true, + }); + }; - expect(payload.networkBandwidth).to.exist; + try { + setNavigatorConnection(mockConnection); - if (bandwidth) { - expect(payload.networkBandwidth).to.deep.equal(bandwidth.toString()); - } else { - expect(payload.networkBandwidth).to.deep.equal(''); - } - }); + const requestWithConnection = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithConnection = JSON.parse(requestWithConnection.data); + + expect(payloadWithConnection.networkBandwidth).to.exist; + expect(payloadWithConnection.networkBandwidth).to.deep.equal(mockConnection.downlink.toString()); + + setNavigatorConnection(undefined); + + const requestWithoutConnection = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutConnection = JSON.parse(requestWithoutConnection.data); + + expect(payloadWithoutConnection.networkBandwidth).to.exist; + expect(payloadWithoutConnection.networkBandwidth).to.deep.equal(''); + } finally { + setNavigatorConnection(originalConnection); + } it('should add pageReferrer info to payload', function () { const request = spec.buildRequests(bidRequests, bidderRequestDefault); @@ -235,14 +248,37 @@ describe('greenbidsBidAdapter', () => { }); it('should add screenOrientation info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderRequestDefault); - const payload = JSON.parse(request.data); - const screenOrientation = window.top.screen.orientation?.type + const originalScreenOrientation = window.top.screen.orientation; + + const mockScreenOrientation = (type) => { + Object.defineProperty(window.top.screen, 'orientation', { + value: { type }, + configurable: true, + }); + }; + + try { + const mockType = 'landscape-primary'; + mockScreenOrientation(mockType); + + const requestWithOrientation = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithOrientation = JSON.parse(requestWithOrientation.data); - if (screenOrientation) { - expect(payload.screenOrientation).to.exist; - expect(payload.screenOrientation).to.deep.equal(screenOrientation); - } else expect(payload.screenOrientation).to.not.exist; + expect(payloadWithOrientation.screenOrientation).to.exist; + expect(payloadWithOrientation.screenOrientation).to.deep.equal(mockType); + + mockScreenOrientation(undefined); + + const requestWithoutOrientation = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutOrientation = JSON.parse(requestWithoutOrientation.data); + + expect(payloadWithoutOrientation.screenOrientation).to.not.exist; + } finally { + Object.defineProperty(window.top.screen, 'orientation', { + value: originalScreenOrientation, + configurable: true, + }); + } }); it('should add historyLength info to payload', function () { @@ -305,27 +341,74 @@ describe('greenbidsBidAdapter', () => { }); it('should add hardwareConcurrency info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderRequestDefault); - const payload = JSON.parse(request.data); - const hardwareConcurrency = window.top.navigator?.hardwareConcurrency + const originalHardwareConcurrency = window.top.navigator.hardwareConcurrency; - if (hardwareConcurrency) { - expect(payload.hardwareConcurrency).to.exist; - expect(payload.hardwareConcurrency).to.deep.equal(hardwareConcurrency); - } else expect(payload.hardwareConcurrency).to.not.exist + const mockHardwareConcurrency = (value) => { + Object.defineProperty(window.top.navigator, 'hardwareConcurrency', { + value, + configurable: true, + }); + }; + + try { + const mockValue = 8; + mockHardwareConcurrency(mockValue); + + const requestWithHardwareConcurrency = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithHardwareConcurrency = JSON.parse(requestWithHardwareConcurrency.data); + + expect(payloadWithHardwareConcurrency.hardwareConcurrency).to.exist; + expect(payloadWithHardwareConcurrency.hardwareConcurrency).to.deep.equal(mockValue); + + mockHardwareConcurrency(undefined); + + const requestWithoutHardwareConcurrency = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutHardwareConcurrency = JSON.parse(requestWithoutHardwareConcurrency.data); + + expect(payloadWithoutHardwareConcurrency.hardwareConcurrency).to.not.exist; + } finally { + Object.defineProperty(window.top.navigator, 'hardwareConcurrency', { + value: originalHardwareConcurrency, + configurable: true, + }); + } }); it('should add deviceMemory info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderRequestDefault); - const payload = JSON.parse(request.data); - const deviceMemory = window.top.navigator.deviceMemory + const originalDeviceMemory = window.top.navigator.deviceMemory; + + const mockDeviceMemory = (value) => { + Object.defineProperty(window.top.navigator, 'deviceMemory', { + value, + configurable: true, + }); + }; - if (deviceMemory) { - expect(payload.deviceMemory).to.exist; - expect(payload.deviceMemory).to.deep.equal(deviceMemory); - } else expect(payload.deviceMemory).to.not.exist; + try { + const mockValue = 4; + mockDeviceMemory(mockValue); + + const requestWithDeviceMemory = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithDeviceMemory = JSON.parse(requestWithDeviceMemory.data); + + expect(payloadWithDeviceMemory.deviceMemory).to.exist; + expect(payloadWithDeviceMemory.deviceMemory).to.deep.equal(mockValue); + + mockDeviceMemory(undefined); + + const requestWithoutDeviceMemory = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutDeviceMemory = JSON.parse(requestWithoutDeviceMemory.data); + + expect(payloadWithoutDeviceMemory.deviceMemory).to.not.exist; + } finally { + Object.defineProperty(window.top.navigator, 'deviceMemory', { + value: originalDeviceMemory, + configurable: true, + }); + } }); }); + describe('pageTitle', function () { it('should add pageTitle info to payload based on document title', function () { const testText = 'This is a title'; @@ -420,38 +503,65 @@ describe('greenbidsBidAdapter', () => { expect(payload.pageDescription).to.deep.equal(testText); }); - it('should add timeToFirstByte info to payload', function () { + it('should add timeToFirstByte info to payload for Navigation Timing V2', function () { + // Mock `performance` object with Navigation Timing V2 data + const mockPerformance = { + getEntriesByType: () => [ + { requestStart: 100, responseStart: 150 }, + ], + }; + + // Override the global performance object + const originalPerformance = window.performance; + window.performance = mockPerformance; + + // Execute the code const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); - const performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance; - const ttfbExpectedV2 = performance && - typeof performance.getEntriesByType === 'function' && - Object.prototype.toString.call(performance.getEntriesByType) === '[object Function]' && - performance.getEntriesByType('navigation')[0] && - performance.getEntriesByType('navigation')[0].responseStart && - performance.getEntriesByType('navigation')[0].requestStart && - performance.getEntriesByType('navigation')[0].responseStart > 0 && - performance.getEntriesByType('navigation')[0].requestStart > 0 && - Math.round( - performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart - ); + // Calculate expected TTFB for V2 + const ttfbExpected = Math.round( + mockPerformance.getEntriesByType('navigation')[0].responseStart - + mockPerformance.getEntriesByType('navigation')[0].requestStart + ).toString(); + // Assertions expect(payload.timeToFirstByte).to.exist; + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpected); - if (ttfbExpectedV2) { - expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV2.toString()); - } else { - const ttfbWithTimingV1 = performance && - performance.timing.responseStart && - performance.timing.requestStart && - performance.timing.responseStart > 0 && - performance.timing.requestStart > 0 && - performance.timing.responseStart - performance.timing.requestStart; - const ttfbExpectedV1 = ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; - - expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV1); - } + // Restore the original performance object + window.performance = originalPerformance; + }); + + it('should add timeToFirstByte info to payload for Navigation Timing V1', function () { + // Mock `performance` object with Navigation Timing V1 data + const mockPerformance = { + timing: { + requestStart: 100, + responseStart: 150, + }, + getEntriesByType: () => [], + }; + + // Override the global performance object + const originalPerformance = window.performance; + window.performance = mockPerformance; + + // Execute the code + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + // Calculate expected TTFB for V1 + const ttfbExpected = ( + mockPerformance.timing.responseStart - mockPerformance.timing.requestStart + ).toString(); + + // Assertions + expect(payload.timeToFirstByte).to.exist; + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpected); + + // Restore the original performance object + window.performance = originalPerformance; }); it('should send GDPR to endpoint with 11 status', function () { @@ -627,43 +737,7 @@ describe('greenbidsBidAdapter', () => { }); it('should add userAgentClientHints info to payload if available', function () { - const bidRequest = Object.assign({}, bidRequests[0], { - ortb2: { - device: { - sua: { - source: 2, - platform: { - brand: 'macOS', - version: ['12', '4', '0'] - }, - browsers: [ - { - brand: 'Chromium', - version: ['106', '0', '5249', '119'] - }, - { - brand: 'Google Chrome', - version: ['106', '0', '5249', '119'] - }, - { - brand: 'Not;A=Brand', - version: ['99', '0', '0', '0'] - } - ], - mobile: 0, - model: '', - bitness: '64', - architecture: 'x86' - } - } - } - }); - - const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderRequestDefault); - const payload = JSON.parse(requestWithUserAgentClientHints.data); - - expect(payload.userAgentClientHints).to.exist; - expect(payload.userAgentClientHints).to.deep.equal({ + const sua = { source: 2, platform: { brand: 'macOS', @@ -688,7 +762,20 @@ describe('greenbidsBidAdapter', () => { bitness: '64', architecture: 'x86' } - ); + + const bidRequest = Object.assign({}, bidRequests[0], { + ortb2: { + device: { + sua: sua + } + } + }); + + const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(requestWithUserAgentClientHints.data); + + expect(payload.userAgentClientHints).to.exist; + expect(payload.userAgentClientHints).to.deep.equal(sua); const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); expect(JSON.parse(defaultRequest.data).userAgentClientHints).to.not.exist; @@ -766,7 +853,7 @@ describe('greenbidsBidAdapter', () => { const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); - return payload.data.forEach(bid => { + payload.data.forEach(bid => { expect(bid).not.to.have.property('gpid'); }); }); @@ -783,7 +870,7 @@ describe('greenbidsBidAdapter', () => { const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); - return payload.data.forEach(bid => { + payload.data.forEach(bid => { expect(bid).not.to.have.property('gpid'); }); }); From 39a92a47cfce8eaa354fb0aaffb08eb63c28017f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20G?= Date: Mon, 25 Nov 2024 17:21:12 +0100 Subject: [PATCH 5/7] Alex's review part 2 --- .../timeToFirstBytesUtils.js | 37 ++++ modules/greenbidsBidAdapter.js | 161 +++++++++++------- modules/greenbidsBidAdapter.md | 4 +- 3 files changed, 139 insertions(+), 63 deletions(-) create mode 100644 libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js diff --git a/libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js b/libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js new file mode 100644 index 00000000000..5d5330d8127 --- /dev/null +++ b/libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js @@ -0,0 +1,37 @@ +/** + * Calculates the Time to First Byte (TTFB) for the given window object. + * + * This function attempts to use the Navigation Timing Level 2 API first, and falls back to + * the Navigation Timing Level 1 API if the former is not available. + * + * @param {Window} win - The window object from which to retrieve performance timing information. + * @returns {string} The TTFB in milliseconds as a string, or an empty string if the TTFB cannot be determined. + */ +export function getTimeToFirstByte(win) { + const performance = win.performance || win.webkitPerformance || win.msPerformance || win.mozPerformance; + + const ttfbWithTimingV2 = performance && + typeof performance.getEntriesByType === 'function' && + Object.prototype.toString.call(performance.getEntriesByType) === '[object Function]' && + performance.getEntriesByType('navigation')[0] && + performance.getEntriesByType('navigation')[0].responseStart && + performance.getEntriesByType('navigation')[0].requestStart && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && + Math.round( + performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart + ); + + if (ttfbWithTimingV2) { + return ttfbWithTimingV2.toString(); + } + + const ttfbWithTimingV1 = performance && + performance.timing.responseStart && + performance.timing.requestStart && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && + performance.timing.responseStart - performance.timing.requestStart; + + return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; +} diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js index ea509512eb5..c72f2a622c8 100644 --- a/modules/greenbidsBidAdapter.js +++ b/modules/greenbidsBidAdapter.js @@ -2,6 +2,7 @@ import { getValue, logError, deepAccess, parseSizesInput, getBidIdParameter, log import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { getDM, getHC, getHLen } from '../libraries/navigatorData/navigatorData.js'; +import { getTimeToFirstByte } from '../libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -10,7 +11,7 @@ import { getDM, getHC, getHLen } from '../libraries/navigatorData/navigatorData. const BIDDER_CODE = 'greenbids'; const GVL_ID = 1232; -const ENDPOINT_URL = 'https://d.greenbids.ai/hb/bid-request'; +const ENDPOINT_URL = 'https://hb.greenbids.ai'; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const spec = { @@ -40,7 +41,18 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - const bids = validBidRequests.map(cleanBidsInfo); + const bids = validBidRequests.map(bids => { + const reqObj = {}; + let placementId = getValue(bids.params, 'placementId'); + const gpid = deepAccess(bids, 'ortb2Imp.ext.gpid'); + reqObj.sizes = getSizes(bids); + reqObj.bidId = getBidIdParameter('bidId', bids); + reqObj.bidderRequestId = getBidIdParameter('bidderRequestId', bids); + reqObj.placementId = parseInt(placementId, 10); + reqObj.adUnitCode = getBidIdParameter('adUnitCode', bids); + reqObj.transactionId = bids.ortb2Imp?.ext?.tid || ''; + if (gpid) { reqObj.gpid = gpid; } + }); const topWindow = window.top; const payload = { @@ -70,8 +82,8 @@ export const spec = { payload.schain = firstBidRequest.schain; } - hydratePayloadWithGppData(payload, bidderRequest.gppConsent); - hydratePayloadWithGdprData(payload, bidderRequest.gdprConsent); + hydratePayloadWithGppConsentData(payload, bidderRequest.gppConsent); + hydratePayloadWithGdprConsentData(payload, bidderRequest.gdprConsent); hydratePayloadWithUspConsentData(payload, bidderRequest.uspConsent); const userAgentClientHints = deepAccess(firstBidRequest, 'ortb2.device.sua'); @@ -133,6 +145,15 @@ export const spec = { registerBidder(spec); // Page info retrival + +/** + * Retrieves the referrer information from the bidder request. + * + * @param {Object} bidderRequest - The bidder request object. + * @param {Object} [bidderRequest.refererInfo] - The referer information object. + * @param {string} [bidderRequest.refererInfo.page] - The page URL of the referer. + * @returns {string} The referrer URL if available, otherwise an empty string. + */ function getReferrerInfo(bidderRequest) { let ref = ''; if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) { @@ -141,6 +162,15 @@ function getReferrerInfo(bidderRequest) { return ref; } +/** + * Retrieves the title of the current web page. + * + * This function attempts to get the title from the top-level window's document. + * If an error occurs (e.g., due to cross-origin restrictions), it falls back to the current document. + * It first tries to get the title from the `og:title` meta tag, and if that is not available, it uses the document's title. + * + * @returns {string} The title of the current web page, or an empty string if no title is found. + */ function getPageTitle() { try { const ogTitle = window.top.document.querySelector('meta[property="og:title"]'); @@ -151,6 +181,15 @@ function getPageTitle() { } } +/** + * Retrieves the content of the page description meta tag. + * + * This function attempts to get the description from the top-level window's document. + * If it fails (e.g., due to cross-origin restrictions), it falls back to the current document. + * It looks for meta tags with either the name "description" or the property "og:description". + * + * @returns {string} The content of the description meta tag, or an empty string if not found. + */ function getPageDescription() { try { const element = window.top.document.querySelector('meta[name="description"]') || @@ -163,73 +202,60 @@ function getPageDescription() { } } +/** + * Retrieves the downlink speed of the user's network connection. + * + * @param {object} nav - The navigator object, typically `window.navigator`. + * @returns {string} The downlink speed as a string if available, otherwise an empty string. + */ function getConnectionDownLink(nav) { return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; } -function getTimeToFirstByte(win) { - const performance = win.performance || win.webkitPerformance || win.msPerformance || win.mozPerformance; - - const ttfbWithTimingV2 = performance && - typeof performance.getEntriesByType === 'function' && - Object.prototype.toString.call(performance.getEntriesByType) === '[object Function]' && - performance.getEntriesByType('navigation')[0] && - performance.getEntriesByType('navigation')[0].responseStart && - performance.getEntriesByType('navigation')[0].requestStart && - performance.getEntriesByType('navigation')[0].responseStart > 0 && - performance.getEntriesByType('navigation')[0].requestStart > 0 && - Math.round( - performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart - ); - - if (ttfbWithTimingV2) { - return ttfbWithTimingV2.toString(); - } - - const ttfbWithTimingV1 = performance && - performance.timing.responseStart && - performance.timing.requestStart && - performance.timing.responseStart > 0 && - performance.timing.requestStart > 0 && - performance.timing.responseStart - performance.timing.requestStart; - - return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; -} - -function cleanBidsInfo(bids) { - const reqObj = {}; - let placementId = getValue(bids.params, 'placementId'); - const gpid = deepAccess(bids, 'ortb2Imp.ext.gpid'); - reqObj.sizes = getSizes(bids); - reqObj.bidId = getBidIdParameter('bidId', bids); - reqObj.bidderRequestId = getBidIdParameter('bidderRequestId', bids); - reqObj.placementId = parseInt(placementId, 10); - reqObj.adUnitCode = getBidIdParameter('adUnitCode', bids); - reqObj.transactionId = bids.ortb2Imp?.ext?.tid || ''; - if (gpid) { reqObj.gpid = gpid; } - return reqObj; -} - +/** + * Converts the sizes from the bid object to the required format. + * + * @param {Object} bid - The bid object containing size information. + * @param {Array} bid.sizes - The sizes array from the bid object. + * @returns {Array} - The parsed sizes in the required format. + */ function getSizes(bid) { return parseSizesInput(bid.sizes); } // Privacy handling -export function hydratePayloadWithGppData(payload, gppData) { - if (gppData) { - let isValidConsentString = typeof gppData.gppString === 'string'; - let validateApplicableSections = +/** + * Hydrates the given payload with GPP consent data if available. + * + * @param {Object} payload - The payload object to be hydrated. + * @param {Object} gppData - The GPP consent data object. + * @param {string} gppData.gppString - The GPP consent string. + * @param {number[]} gppData.applicableSections - An array of applicable section IDs. + */ +function hydratePayloadWithGppConsentData(payload, gppData) { + if (!gppData) { return; } + let isValidConsentString = typeof gppData.gppString === 'string'; + let validateApplicableSections = Array.isArray(gppData.applicableSections) && gppData.applicableSections.every((section) => typeof (section) === 'number') - payload.gpp = { - consentString: isValidConsentString ? gppData.gppString : '', - applicableSectionIds: validateApplicableSections ? gppData.applicableSections : [], - }; - } + payload.gpp = { + consentString: isValidConsentString ? gppData.gppString : '', + applicableSectionIds: validateApplicableSections ? gppData.applicableSections : [], + }; } -export function hydratePayloadWithGdprData(payload, gdprData) { +/** + * Hydrates the given payload with GDPR consent data if available. + * + * @param {Object} payload - The payload object to be hydrated with GDPR consent data. + * @param {Object} gdprData - The GDPR data object containing consent information. + * @param {boolean} gdprData.gdprApplies - Indicates if GDPR applies. + * @param {string} gdprData.consentString - The GDPR consent string. + * @param {number} gdprData.apiVersion - The version of the GDPR API being used. + * @param {Object} gdprData.vendorData - Additional vendor data related to GDPR. + */ +function hydratePayloadWithGdprConsentData(payload, gdprData) { if (!gdprData) { return; } let isCmp = typeof gdprData.gdprApplies === 'boolean'; let isConsentString = typeof gdprData.consentString === 'string'; @@ -243,10 +269,15 @@ export function hydratePayloadWithGdprData(payload, gdprData) { }; } -export function hydratePayloadWithUspConsentData(payload, uspConsentData) { - if (uspConsentData) { - payload.us_privacy = uspConsentData; - } +/** + * Adds USP (CCPA) consent data to the payload if available. + * + * @param {Object} payload - The payload object to be hydrated with USP consent data. + * @param {string} uspConsentData - The USP consent string to be added to the payload. + */ +function hydratePayloadWithUspConsentData(payload, uspConsentData) { + if (!uspConsentData) { return; } + payload.us_privacy = uspConsentData; } const gdprStatus = { @@ -256,6 +287,14 @@ const gdprStatus = { CMP_NOT_FOUND_OR_ERROR: 22 }; +/** + * Determines the GDPR status based on whether GDPR applies and the provided GDPR data. + * + * @param {boolean} gdprApplies - Indicates if GDPR applies. + * @param {Object} gdprData - The GDPR data object. + * @param {boolean} gdprData.isServiceSpecific - Indicates if the GDPR data is service-specific. + * @returns {string} The GDPR status. + */ function findGdprStatus(gdprApplies, gdprData) { let status = gdprStatus.GDPR_APPLIES_PUBLISHER; if (gdprApplies) { diff --git a/modules/greenbidsBidAdapter.md b/modules/greenbidsBidAdapter.md index 18e9840fe44..df536294f47 100644 --- a/modules/greenbidsBidAdapter.md +++ b/modules/greenbidsBidAdapter.md @@ -16,7 +16,7 @@ Use `greenbids` as bidder. bids: [{ bidder: 'greenbids', params: { - gbPlacementId: 12345, + placementId: 12345, } }] },{ @@ -25,7 +25,7 @@ Use `greenbids` as bidder. bids: [{ bidder: 'greenbids', params: { - gbPlacementId: 12345, + placementId: 12345, } }] }]; From 366ca7f84312168af4ff5c0ed1d0f98fa9b1cad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20G?= Date: Thu, 28 Nov 2024 10:02:45 +0100 Subject: [PATCH 6/7] export more utils --- libraries/pageInfosUtils/pageInfosUtils.js | 65 ++++++++++++++++++++ modules/greenbidsBidAdapter.js | 70 +--------------------- 2 files changed, 66 insertions(+), 69 deletions(-) create mode 100644 libraries/pageInfosUtils/pageInfosUtils.js diff --git a/libraries/pageInfosUtils/pageInfosUtils.js b/libraries/pageInfosUtils/pageInfosUtils.js new file mode 100644 index 00000000000..5e215ad3f3d --- /dev/null +++ b/libraries/pageInfosUtils/pageInfosUtils.js @@ -0,0 +1,65 @@ +/** + * Retrieves the referrer information from the bidder request. + * + * @param {Object} bidderRequest - The bidder request object. + * @param {Object} [bidderRequest.refererInfo] - The referer information object. + * @param {string} [bidderRequest.refererInfo.page] - The page URL of the referer. + * @returns {string} The referrer URL if available, otherwise an empty string. + */ +export function getReferrerInfo(bidderRequest) { + let ref = ''; + if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) { + ref = bidderRequest.refererInfo.page; + } + return ref; +} + +/** + * Retrieves the title of the current web page. + * + * This function attempts to get the title from the top-level window's document. + * If an error occurs (e.g., due to cross-origin restrictions), it falls back to the current document. + * It first tries to get the title from the `og:title` meta tag, and if that is not available, it uses the document's title. + * + * @returns {string} The title of the current web page, or an empty string if no title is found. + */ +export function getPageTitle() { + try { + const ogTitle = window.top.document.querySelector('meta[property="og:title"]'); + return window.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]'); + return document.title || (ogTitle && ogTitle.content) || ''; + } +} + +/** + * Retrieves the content of the page description meta tag. + * + * This function attempts to get the description from the top-level window's document. + * If it fails (e.g., due to cross-origin restrictions), it falls back to the current document. + * It looks for meta tags with either the name "description" or the property "og:description". + * + * @returns {string} The content of the description meta tag, or an empty string if not found. + */ +export function getPageDescription() { + try { + const element = window.top.document.querySelector('meta[name="description"]') || + window.top.document.querySelector('meta[property="og:description"]'); + return (element && element.content) || ''; + } catch (e) { + const element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]'); + return (element && element.content) || ''; + } +} + +/** + * Retrieves the downlink speed of the user's network connection. + * + * @param {object} nav - The navigator object, typically `window.navigator`. + * @returns {string} The downlink speed as a string if available, otherwise an empty string. + */ +export function getConnectionDownLink(nav) { + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; +} diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js index c72f2a622c8..c2a6581c11e 100644 --- a/modules/greenbidsBidAdapter.js +++ b/modules/greenbidsBidAdapter.js @@ -3,7 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { getDM, getHC, getHLen } from '../libraries/navigatorData/navigatorData.js'; import { getTimeToFirstByte } from '../libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js'; - +import { getReferrerInfo, getPageTitle, getPageDescription, getConnectionDownLink } from '../libraries/pageInfosUtils/pageInfosUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid @@ -144,74 +144,6 @@ export const spec = { registerBidder(spec); -// Page info retrival - -/** - * Retrieves the referrer information from the bidder request. - * - * @param {Object} bidderRequest - The bidder request object. - * @param {Object} [bidderRequest.refererInfo] - The referer information object. - * @param {string} [bidderRequest.refererInfo.page] - The page URL of the referer. - * @returns {string} The referrer URL if available, otherwise an empty string. - */ -function getReferrerInfo(bidderRequest) { - let ref = ''; - if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) { - ref = bidderRequest.refererInfo.page; - } - return ref; -} - -/** - * Retrieves the title of the current web page. - * - * This function attempts to get the title from the top-level window's document. - * If an error occurs (e.g., due to cross-origin restrictions), it falls back to the current document. - * It first tries to get the title from the `og:title` meta tag, and if that is not available, it uses the document's title. - * - * @returns {string} The title of the current web page, or an empty string if no title is found. - */ -function getPageTitle() { - try { - const ogTitle = window.top.document.querySelector('meta[property="og:title"]'); - return window.top.document.title || (ogTitle && ogTitle.content) || ''; - } catch (e) { - const ogTitle = document.querySelector('meta[property="og:title"]'); - return document.title || (ogTitle && ogTitle.content) || ''; - } -} - -/** - * Retrieves the content of the page description meta tag. - * - * This function attempts to get the description from the top-level window's document. - * If it fails (e.g., due to cross-origin restrictions), it falls back to the current document. - * It looks for meta tags with either the name "description" or the property "og:description". - * - * @returns {string} The content of the description meta tag, or an empty string if not found. - */ -function getPageDescription() { - try { - const element = window.top.document.querySelector('meta[name="description"]') || - window.top.document.querySelector('meta[property="og:description"]'); - return (element && element.content) || ''; - } catch (e) { - const element = document.querySelector('meta[name="description"]') || - document.querySelector('meta[property="og:description"]'); - return (element && element.content) || ''; - } -} - -/** - * Retrieves the downlink speed of the user's network connection. - * - * @param {object} nav - The navigator object, typically `window.navigator`. - * @returns {string} The downlink speed as a string if available, otherwise an empty string. - */ -function getConnectionDownLink(nav) { - return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; -} - /** * Converts the sizes from the bid object to the required format. * From 378495547b3c2aaa98b5a1d8e6442c266431675d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20G?= Date: Thu, 28 Nov 2024 15:46:54 +0100 Subject: [PATCH 7/7] add test on news utils --- test/spec/libraries/pageInfosUtils.js | 102 +++++++++++++++++++ test/spec/libraries/timeToFirstBytesUtils.js | 54 ++++++++++ 2 files changed, 156 insertions(+) create mode 100644 test/spec/libraries/pageInfosUtils.js create mode 100644 test/spec/libraries/timeToFirstBytesUtils.js diff --git a/test/spec/libraries/pageInfosUtils.js b/test/spec/libraries/pageInfosUtils.js new file mode 100644 index 00000000000..59d62abc74c --- /dev/null +++ b/test/spec/libraries/pageInfosUtils.js @@ -0,0 +1,102 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { getReferrerInfo, getPageTitle, getPageDescription, getConnectionDownLink } from './pageInfosUtils'; + +describe('pageInfosUtils', () => { + describe('getReferrerInfo', () => { + it('should return the referrer URL if available', () => { + const bidderRequest = { + refererInfo: { + page: 'http://example.com' + } + }; + const result = getReferrerInfo(bidderRequest); + expect(result).to.equal('http://example.com'); + }); + + it('should return an empty string if referrer URL is not available', () => { + const bidderRequest = {}; + const result = getReferrerInfo(bidderRequest); + expect(result).to.equal(''); + }); + }); + + describe('getPageTitle', () => { + let topDocumentStub, documentStub; + + beforeEach(() => { + topDocumentStub = sinon.stub(window.top, 'document').value({ + title: 'Top Document Title', + querySelector: sinon.stub().returns(null) + }); + documentStub = sinon.stub(document, 'querySelector').returns(null); + }); + + afterEach(() => { + topDocumentStub.restore(); + documentStub.restore(); + }); + + it('should return the title from the top-level document', () => { + const result = getPageTitle(); + expect(result).to.equal('Top Document Title'); + }); + + it('should return the title from the current document if top-level document access fails', () => { + topDocumentStub.value({ + title: '', + querySelector: sinon.stub().throws(new Error('Cross-origin restriction')) + }); + documentStub.returns({ content: 'Current Document Title' }); + const result = getPageTitle(); + expect(result).to.equal('Current Document Title'); + }); + }); + + describe('getPageDescription', () => { + let topDocumentStub, documentStub; + + beforeEach(() => { + topDocumentStub = sinon.stub(window.top, 'document').value({ + querySelector: sinon.stub().returns(null) + }); + documentStub = sinon.stub(document, 'querySelector').returns(null); + }); + + afterEach(() => { + topDocumentStub.restore(); + documentStub.restore(); + }); + + it('should return the description from the top-level document', () => { + topDocumentStub.querySelector.withArgs('meta[name="description"]').returns({ content: 'Top Document Description' }); + const result = getPageDescription(); + expect(result).to.equal('Top Document Description'); + }); + + it('should return the description from the current document if top-level document access fails', () => { + topDocumentStub.querySelector.throws(new Error('Cross-origin restriction')); + documentStub.withArgs('meta[name="description"]').returns({ content: 'Current Document Description' }); + const result = getPageDescription(); + expect(result).to.equal('Current Document Description'); + }); + }); + + describe('getConnectionDownLink', () => { + it('should return the downlink speed if available', () => { + const nav = { + connection: { + downlink: 10 + } + }; + const result = getConnectionDownLink(nav); + expect(result).to.equal('10'); + }); + + it('should return an empty string if downlink speed is not available', () => { + const nav = {}; + const result = getConnectionDownLink(nav); + expect(result).to.equal(''); + }); + }); +}); diff --git a/test/spec/libraries/timeToFirstBytesUtils.js b/test/spec/libraries/timeToFirstBytesUtils.js new file mode 100644 index 00000000000..ea737e925c4 --- /dev/null +++ b/test/spec/libraries/timeToFirstBytesUtils.js @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { getTimeToFirstByte } from './timeToFirstBytesUtils'; + +describe('getTimeToFirstByte', () => { + let win; + + beforeEach(() => { + win = { + performance: { + getEntriesByType: sinon.stub(), + timing: { + responseStart: 0, + requestStart: 0 + } + } + }; + }); + + it('should return TTFB using Navigation Timing Level 2 API', () => { + win.performance.getEntriesByType.withArgs('navigation').returns([{ + responseStart: 100, + requestStart: 50 + }]); + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal('50'); + }); + + it('should return TTFB using Navigation Timing Level 1 API', () => { + win.performance.getEntriesByType.returns([]); + win.performance.timing.responseStart = 100; + win.performance.timing.requestStart = 50; + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal('50'); + }); + + it('should return an empty string if TTFB cannot be determined', () => { + win.performance.getEntriesByType.returns([]); + win.performance.timing.responseStart = 0; + win.performance.timing.requestStart = 0; + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal(''); + }); + + it('should return an empty string if performance object is not available', () => { + win.performance = null; + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal(''); + }); +});