From 7600b38cebde8d11000916f74d9cc5f5842ebb8e Mon Sep 17 00:00:00 2001 From: redaguermas Date: Tue, 3 Oct 2023 06:53:46 -0700 Subject: [PATCH] NoBid Bid Adapter : add support for first party user id (#10519) * Enable supplyChain support * Added support for COPPA * rebuilt * Added support for Extended User IDs. * Added support for the "meta" attribute in bid response. * Delete nobidBidAdapter.js.orig * Delete a * Delete .jsdtscope * Delete org.eclipse.wst.jsdt.ui.superType.container * Delete org.eclipse.wst.jsdt.ui.superType.name * Delete .project * Fix tests * Added support for First Party User ID in NoBid Bid Adapter. --------- Co-authored-by: Reda Guermas --- modules/nobidBidAdapter.js | 123 ++++++++++++++++++++- test/spec/modules/nobidBidAdapter_spec.js | 129 +++++++++++++++------- 2 files changed, 212 insertions(+), 40 deletions(-) diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 68010b32b37..fb052a99695 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -8,12 +8,17 @@ import { hasPurpose1Consent } from '../src/utils/gpdr.js'; const GVLID = 816; const BIDDER_CODE = 'nobid'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); -window.nobidVersion = '1.3.3'; +window.nobidVersion = '1.4.1'; window.nobid = window.nobid || {}; window.nobid.bidResponses = window.nobid.bidResponses || {}; window.nobid.timeoutTotal = 0; window.nobid.bidWonTotal = 0; window.nobid.refreshCount = 0; +window.nobid.firstPartyIds = null; +window.nobid.firstPartyIdEnabled = false; +const FIRST_PARTY_KEY = 'fppcid.nobid.io'; +const FIRST_PARTY_SOURCE_KEY = 'fpid.nobid.io'; +const FIRST_PARTY_DATA_EXPIRY_DAYS = 7 * 24 * 3600 * 1000; function log(msg, obj) { logInfo('-NoBid- ' + msg, obj) } @@ -135,8 +140,10 @@ function nobidBuildRequests(bids, bidderRequest) { src.push({source: eid.source, uids: ids}); } }); + if (window.nobid.firstPartyIds && window.nobid.firstPartyIds) src.push({source: FIRST_PARTY_SOURCE_KEY, uids: [{id: window.nobid.firstPartyIds.ids}]}); return src; } + if (window.nobid.firstPartyIds && window.nobid.firstPartyIds.ids) return [{source: FIRST_PARTY_SOURCE_KEY, uids: [{id: window.nobid.firstPartyIds.ids}]}]; } var state = {}; state['sid'] = siteId; @@ -286,6 +293,12 @@ function nobidInterpretResponse(response, bidRequest) { var setRefreshLimit = function(response) { if (response && typeof response.rlimit !== 'undefined') window.nobid.refreshLimit = response.rlimit; } + var setFirstPartyIdEnabled = function(response) { + if (response && typeof response.fpid !== 'undefined') window.nobid.firstPartyIdEnabled = response.fpid; + if (window?.nobid?.firstPartyIdEnabled) { + nobidFirstPartyData.loadOrCreateFirstPartyData(); + } + } var setUserBlock = function(response) { if (response && typeof response.ublock !== 'undefined') { nobidSetCookie('_ublock', '1', response.ublock); @@ -293,6 +306,7 @@ function nobidInterpretResponse(response, bidRequest) { } setRefreshLimit(response); setUserBlock(response); + setFirstPartyIdEnabled(response); var bidResponses = []; for (var i = 0; response.bids && i < response.bids.length; i++) { var bid = response.bids[i]; @@ -359,6 +373,113 @@ window.addEventListener('message', function (event) { } } }, false); +const nobidFirstPartyData = { + isJson: function (str) { + return str && str.startsWith('{') && str.endsWith('}'); + }, + hasLocalStorage: function () { + try { + return window.localStorage; + } catch (error) { + logWarn('Local storage api disabled', error); + } + return false; + }, + readFirstPartyDataIds: function () { + try { + if (this.hasLocalStorage()) { + const idsStr = window.localStorage.getItem(FIRST_PARTY_SOURCE_KEY); + if (this.isJson(idsStr)) { + const idsObj = JSON.parse(idsStr); + if (idsObj.ts + FIRST_PARTY_DATA_EXPIRY_DAYS < Date.now()) return { pid: idsObj.pid }; // expired? + return idsObj; + } + return null; + } + } catch (error) { + logWarn('Local storage api disabled', error); + } + return null; + }, + loadOrCreateFirstPartyData: function () { + const storeFirstPartyDataIds = function ({ids: theIds, pid: thePid}) { + try { + if (nobidFirstPartyData.hasLocalStorage()) { + window.localStorage.setItem(FIRST_PARTY_SOURCE_KEY, JSON.stringify({ids: theIds, pid: thePid, ts: Date.now()})); + } + } catch (error) { + logWarn('Local storage api disabled', error); + } + }; + const readFirstPartyId = function () { + try { + if (nobidFirstPartyData.hasLocalStorage()) { + const idStr = window.localStorage.getItem(FIRST_PARTY_KEY); + if (nobidFirstPartyData.isJson(idStr)) { + return JSON.parse(idStr); + } + return null; + } + } catch (error) { + logWarn('Local storage api disabled', error); + } + return null; + }; + const storeFirstPartyId = function (theId) { + try { + if (nobidFirstPartyData.hasLocalStorage()) { + window.localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify(theId)); + } + } catch (error) { + logWarn('Local storage api disabled', error); + } + }; + const _loadOrCreateFirstPartyData = function () { + const generateGUID = function () { + let d = new Date().getTime(); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); + }); + }; + const ajaxGet = function (ajaxParams, callback) { + const ajax = new XMLHttpRequest(); + ajax.withCredentials = false; + ajax.timeout = ajaxParams.timeout; + ajax.open('GET', ajaxParams.url, true); + ajax.onreadystatechange = function () { + if (this.readyState === XMLHttpRequest.DONE) { + callback(this.response); + } + }; + ajax.send(ajaxParams.data); + }; + let firstPartyIdObj = readFirstPartyId(); + if (!firstPartyIdObj || !firstPartyIdObj.id || !firstPartyIdObj.ts) { + const firstPartyId = generateGUID(); + firstPartyIdObj = {id: firstPartyId, ts: Date.now()}; + storeFirstPartyId(firstPartyIdObj); + } + let firstPartyIds = nobidFirstPartyData.readFirstPartyDataIds(); + if (firstPartyIdObj?.ts && !firstPartyIds?.ids) { + const pid = firstPartyIds?.pid || ''; + const pdate = firstPartyIdObj.ts; + const firstPartyId = firstPartyIdObj.id; + const url = `https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&pt=17&dpn=1&iiqidtype=2&dpi=430542822&iiqpcid=${firstPartyId}&iiqpciddate=${pdate}&pid=${pid}`; + if (window.nobid.firstPartyRequestInProgress) return; + window.nobid.firstPartyRequestInProgress = true; + ajaxGet({ url: url }, function (response) { + response = JSON.parse(response); + if (response?.data) storeFirstPartyDataIds({ ids: response.data, pid: response.pid }); + }); + } + }; + window.nobid.firstPartyIds = this.readFirstPartyDataIds(); + if (window.nobid.firstPartyIdEnabled && !window.nobid.firstPartyIds?.ids) _loadOrCreateFirstPartyData(); + } +}; +window.nobid.firstPartyIds = nobidFirstPartyData.readFirstPartyDataIds(); export const spec = { code: BIDDER_CODE, gvlid: GVLID, diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index b1e303bde6e..f2059900a2e 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -57,27 +57,26 @@ describe('Nobid Adapter', function () { 'auctionId': '1d1a030790a475', }; - it('should return true when required params found', function () { + it('should return true when required params found 1', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return true when required params found', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + it('should return true when required params found 2', function () { + let mybid = Object.assign({}, bid); + delete mybid.params; + mybid.params = { 'siteId': 2 }; - - expect(spec.isBidRequestValid(bid)).to.equal(true); + expect(spec.isBidRequestValid(mybid)).to.equal(true); }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let mybid = Object.assign({}, bid); + delete mybid.params; + mybid.params = { 'siteId': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(mybid)).to.equal(false); }); }); @@ -253,12 +252,12 @@ describe('Nobid Adapter', function () { }); it('sends bid request to site id', function () { - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.a).to.exist; - expect(payload.a[0].sid).to.equal(2); - expect(payload.a[0].at).to.equal('banner'); - expect(payload.a[0].params.siteId).to.equal(2); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.a).to.exist; + expect(payload.a[0].sid).to.equal(2); + expect(payload.a[0].at).to.equal('banner'); + expect(payload.a[0].params.siteId).to.equal(2); }); it('sends bid request to ad type', function () { @@ -423,6 +422,58 @@ describe('Nobid Adapter', function () { }); }); + describe('First Party ID Test', function () { + const CREATIVE_ID_300x250 = 'CREATIVE-100'; + const ADUNIT_300x250 = 'ADUNIT-1'; + const ADMARKUP_300x250 = 'ADMARKUP-300x250'; + const PRICE_300x250 = 0.51; + const REQUEST_ID = '3db3773286ee59'; + const DEAL_ID = 'deal123'; + let response = { + country: 'US', + ip: '68.83.15.75', + device: 'COMPUTER', + site: 2, + fpid: true, + bids: [ + {id: 1, + bdrid: 101, + divid: ADUNIT_300x250, + creativeid: CREATIVE_ID_300x250, + size: {'w': 300, 'h': 250}, + adm: ADMARKUP_300x250, + price: '' + PRICE_300x250 + } + ] + }; + + it('first party ID', function () { + const bidderRequest = { + bids: [{ + bidId: REQUEST_ID, + adUnitCode: ADUNIT_300x250 + }] + } + const bidRequests = [ + { + 'bidder': 'nobid', + 'params': { + 'siteId': 2 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.method).to.equal('POST'); + spec.interpretResponse({ body: response }, {bidderRequest: bidderRequest}); + expect(window.nobid.firstPartyIdEnabled).to.equal(true); + }); + }); + describe('isVideoBidRequestValid', function () { let bid = { bidder: 'nobid', @@ -635,12 +686,12 @@ describe('Nobid Adapter', function () { }); it('sends bid request to site id', function () { - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.a).to.exist; - expect(payload.a[0].sid).to.equal(2); - expect(payload.a[0].at).to.equal('banner'); - expect(payload.a[0].params.siteId).to.equal(2); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.a).to.exist; + expect(payload.a[0].sid).to.equal(2); + expect(payload.a[0].at).to.equal('banner'); + expect(payload.a[0].params.siteId).to.equal(2); }); it('sends bid request to ad type', function () { @@ -939,19 +990,19 @@ describe('Nobid Adapter', function () { auctionId: '1d1a030790a475', coppa: true, schain: { - validation: 'strict', - config: { - ver: '1.0', - complete: 1, - nodes: [ - { - asi: 'indirectseller.com', - sid: '00001', - name: 'name.com', - hp: 1 - } - ] - } + validation: 'strict', + config: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'indirectseller.com', + sid: '00001', + name: 'name.com', + hp: 1 + } + ] + } } } ]; @@ -1001,7 +1052,7 @@ describe('Nobid Adapter', function () { ] }; - it('should ULimit be respected', function () { + it('Limit should be respected', function () { const bidderRequest = { bids: [{ bidId: REQUEST_ID, @@ -1060,8 +1111,8 @@ describe('Nobid Adapter', function () { }); it('should get correct user sync when !iframeEnabled', function () { - let pixel = spec.getUserSyncs({}) - expect(pixel.length).to.equal(0); + let pixel = spec.getUserSyncs({}) + expect(pixel.length).to.equal(0); }); });