From 4009f50076cbb87f84ca3cf170e454800781f5cc Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 19 Mar 2024 10:37:22 -0700 Subject: [PATCH 1/7] Debugging module: add PAAPI support --- modules/debugging/bidInterceptor.js | 27 +++++++++++-- modules/debugging/debugging.js | 8 +++- modules/debugging/pbsInterceptor.js | 17 ++++++++- src/adapters/bidderFactory.js | 20 +++++----- test/spec/modules/debugging_mod_spec.js | 50 +++++++++++++++++++++++-- 5 files changed, 100 insertions(+), 22 deletions(-) diff --git a/modules/debugging/bidInterceptor.js b/modules/debugging/bidInterceptor.js index 775f8fc3da2..3afaacaeb81 100644 --- a/modules/debugging/bidInterceptor.js +++ b/modules/debugging/bidInterceptor.js @@ -54,8 +54,9 @@ Object.assign(BidInterceptor.prototype, { return { no: ruleNo, match: this.matcher(ruleDef.when, ruleNo), - replace: this.replacer(ruleDef.then || {}, ruleNo), + replace: this.replacer(ruleDef.then, ruleNo), options: Object.assign({}, this.DEFAULT_RULE_OPTIONS, ruleDef.options), + paapi: this.paapiReplacer(ruleDef.paapi || [], ruleNo) } }, /** @@ -114,6 +115,10 @@ Object.assign(BidInterceptor.prototype, { * @return {ReplacerFn} */ replacer(replDef, ruleNo) { + if (replDef === null) { + return () => null + } + replDef = replDef || {}; let replFn; if (typeof replDef === 'function') { replFn = ({args}) => replDef(...args); @@ -145,6 +150,17 @@ Object.assign(BidInterceptor.prototype, { return response; } }, + + paapiReplacer(paapiDef, ruleNo) { + if (Array.isArray(paapiDef)) { + return () => paapiDef; + } else if (typeof paapiDef === 'function') { + return paapiDef + } else { + this.logger.logError(`Invalid 'paapi' definition for debug bid interceptor (in rule #${ruleNo})`); + } + }, + responseDefaults(bid) { return { requestId: bid.bidId, @@ -198,11 +214,12 @@ Object.assign(BidInterceptor.prototype, { * @param {{}[]} bids? * @param {BidRequest} bidRequest * @param {function(*)} addBid called once for each mock response + * @param addPaapiConfig called once for each mock PAAPI config * @param {function()} done called once after all mock responses have been run through `addBid` * @returns {{bids: {}[], bidRequest: {}} remaining bids that did not match any rule (this applies also to * bidRequest.bids) */ - intercept({bids, bidRequest, addBid, done}) { + intercept({bids, bidRequest, addBid, addPaapiConfig, done}) { if (bids == null) { bids = bidRequest.bids; } @@ -211,10 +228,12 @@ Object.assign(BidInterceptor.prototype, { const callDone = delayExecution(done, matches.length); matches.forEach((match) => { const mockResponse = match.rule.replace(match.bid, bidRequest); + const mockPaapi = match.rule.paapi(match.bid, bidRequest); const delay = match.rule.options.delay; - this.logger.logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response:`, match.bid, mockResponse) + this.logger.logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response, PAAPI configs:`, match.bid, mockResponse, mockPaapi) this.setTimeout(() => { - addBid(mockResponse, match.bid); + mockResponse && addBid(mockResponse, match.bid); + mockPaapi.forEach(cfg => addPaapiConfig(cfg, match.bid, bidRequest)); callDone(); }, delay) }); diff --git a/modules/debugging/debugging.js b/modules/debugging/debugging.js index 8a4ad7a9545..2fd1731dc4e 100644 --- a/modules/debugging/debugging.js +++ b/modules/debugging/debugging.js @@ -99,7 +99,13 @@ function registerBidInterceptor(getHookFn, interceptor) { export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, cbs) { const done = delayExecution(cbs.onCompletion, 2); - ({bids, bidRequest} = interceptBids({bids, bidRequest, addBid: cbs.onBid, done})); + ({bids, bidRequest} = interceptBids({ + bids, + bidRequest, + addBid: cbs.onBid, + addPaapiConfig: (config, bidRequest) => cbs.onPaapi({bidId: bidRequest.bidId, config}), + done + })); if (bids.length === 0) { done(); } else { diff --git a/modules/debugging/pbsInterceptor.js b/modules/debugging/pbsInterceptor.js index 1ca13eb4927..73df01bf205 100644 --- a/modules/debugging/pbsInterceptor.js +++ b/modules/debugging/pbsInterceptor.js @@ -5,7 +5,8 @@ export function makePbsInterceptor({createBid}) { return function pbsBidInterceptor(next, interceptBids, s2sBidRequest, bidRequests, ajax, { onResponse, onError, - onBid + onBid, + onFledge, }) { let responseArgs; const done = delayExecution(() => onResponse(...responseArgs), bidRequests.length + 1) @@ -20,7 +21,19 @@ export function makePbsInterceptor({createBid}) { }) } bidRequests = bidRequests - .map((req) => interceptBids({bidRequest: req, addBid, done}).bidRequest) + .map((req) => interceptBids({ + bidRequest: req, + addBid, + addPaapiConfig(config, bidRequest, bidderRequest) { + onFledge({ + adUnitCode: bidRequest.adUnitCode, + ortb2: bidderRequest.ortb2, + ortb2Imp: bidRequest.ortb2Imp, + config + }) + }, + done + }).bidRequest) .filter((req) => req.bids.length > 0) if (bidRequests.length > 0) { diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 3d55f2c06af..4f9237fc8d3 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -293,15 +293,13 @@ export function newBidder(spec) { onTimelyResponse(spec.code); responses.push(resp) }, - onFledgeAuctionConfigs: (fledgeAuctionConfigs) => { - fledgeAuctionConfigs.forEach((fledgeAuctionConfig) => { - const bidRequest = bidRequestMap[fledgeAuctionConfig.bidId]; - if (bidRequest) { - addComponentAuction(bidRequest, fledgeAuctionConfig.config); - } else { - logWarn('Received fledge auction configuration for an unknown bidId', fledgeAuctionConfig); - } - }); + onPaapi: (paapiConfig) => { + const bidRequest = bidRequestMap[paapiConfig.bidId]; + if (bidRequest) { + addComponentAuction(bidRequest, paapiConfig.config); + } else { + logWarn('Received fledge auction configuration for an unknown bidId', paapiConfig); + } }, // If the server responds with an error, there's not much we can do beside logging. onError: (errorMessage, error) => { @@ -378,7 +376,7 @@ export function newBidder(spec) { * @param onBid {function({})} invoked once for each bid in the response - with the bid as returned by interpretResponse * @param onCompletion {function()} invoked once when all bid requests have been processed */ -export const processBidderRequests = hook('sync', function (spec, bids, bidderRequest, ajax, wrapCallback, {onRequest, onResponse, onFledgeAuctionConfigs, onError, onBid, onCompletion}) { +export const processBidderRequests = hook('sync', function (spec, bids, bidderRequest, ajax, wrapCallback, {onRequest, onResponse, onPaapi, onError, onBid, onCompletion}) { const metrics = adapterMetrics(bidderRequest); onCompletion = metrics.startTiming('total').stopBefore(onCompletion); @@ -427,7 +425,7 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe let bids; // Extract additional data from a structured {BidderAuctionResponse} response if (response && isArray(response.fledgeAuctionConfigs)) { - onFledgeAuctionConfigs(response.fledgeAuctionConfigs); + response.fledgeAuctionConfigs.forEach(onPaapi); bids = response.bids; } else { bids = response; diff --git a/test/spec/modules/debugging_mod_spec.js b/test/spec/modules/debugging_mod_spec.js index 8c7f0e84bce..ab99ba2aa0c 100644 --- a/test/spec/modules/debugging_mod_spec.js +++ b/test/spec/modules/debugging_mod_spec.js @@ -103,8 +103,8 @@ describe('bid interceptor', () => { }); describe('rule', () => { - function matchingRule({replace, options}) { - setRules({when: {}, then: replace, options: options}); + function matchingRule({replace, options, paapi}) { + setRules({when: {}, then: replace, options: options, paapi}); return interceptor.match({}); } @@ -164,6 +164,24 @@ describe('bid interceptor', () => { }); }); + describe('paapi', () => { + it('should accept literals', () => { + const mockConfig = [ + {paapi: 1}, + {paapi: 2} + ] + const paapi = matchingRule({paapi: mockConfig}).paapi({}); + expect(paapi).to.eql(mockConfig); + }); + + it('should accept a function and pass extra args to it', () => { + const paapiDef = sinon.stub(); + const args = [{}, {}, {}]; + matchingRule({paapi: paapiDef}).paapi(...args); + expect(paapiDef.calledOnceWith(...args.map(sinon.match.same))).to.be.true; + }) + }) + describe('.options', () => { it('should include default rule options', () => { const optDef = {someOption: 'value'}; @@ -181,16 +199,17 @@ describe('bid interceptor', () => { }); describe('intercept()', () => { - let done, addBid; + let done, addBid, addPaapiConfig; function intercept(args = {}) { const bidRequest = {bids: args.bids || []}; - return interceptor.intercept(Object.assign({bidRequest, done, addBid}, args)); + return interceptor.intercept(Object.assign({bidRequest, done, addBid, addPaapiConfig}, args)); } beforeEach(() => { done = sinon.spy(); addBid = sinon.spy(); + addPaapiConfig = sinon.spy(); }); describe('on no match', () => { @@ -253,6 +272,29 @@ describe('bid interceptor', () => { }); }); + it('should call addPaapiConfigs when provided', () => { + const mockPaapiConfigs = [ + {paapi: 1}, + {paapi: 2} + ] + setRules({ + when: {id: 2}, + paapi: mockPaapiConfigs, + }); + intercept({bidRequest: REQUEST}); + expect(addPaapiConfig.callCount).to.eql(2); + mockPaapiConfigs.forEach(cfg => sinon.assert.calledWith(addPaapiConfig, cfg)) + }) + + it('should not call onBid when then is null', () => { + setRules({ + when: {id: 2}, + then: null + }); + intercept({bidRequest: REQUEST}); + sinon.assert.notCalled(addBid); + }) + it('should call done()', () => { intercept({bidRequest: REQUEST}); expect(done.calledOnce).to.be.true; From 5839accebfca4900efde24fdb6fc7b8b2d636ecd Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 19 Mar 2024 17:00:15 +0100 Subject: [PATCH 2/7] Criteo Bid Adapter: use igi.igs to register fledge auction configs (#11218) Co-authored-by: v.raybaud --- modules/criteoBidAdapter.js | 55 +----- test/spec/modules/criteoBidAdapter_spec.js | 205 +++++++++------------ 2 files changed, 98 insertions(+), 162 deletions(-) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 33eb903ab55..c1fcac4ae2f 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -24,9 +24,6 @@ export const ADAPTER_VERSION = 36; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const PROFILE_ID_INLINE = 207; -const FLEDGE_SELLER_DOMAIN = 'https://grid-mercury.criteo.com'; -const FLEDGE_SELLER_TIMEOUT = 500; -const FLEDGE_DECISION_LOGIC_URL = 'https://grid-mercury.criteo.com/fledge/decision'; export const PROFILE_ID_PUBLISHERTAG = 185; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const LOG_PREFIX = 'Criteo: '; @@ -284,53 +281,13 @@ export const spec = { }); } - if (isArray(body.ext?.igbid)) { - const seller = body.ext.seller || FLEDGE_SELLER_DOMAIN; - const sellerTimeout = body.ext.sellerTimeout || FLEDGE_SELLER_TIMEOUT; - body.ext.igbid.forEach((igbid) => { - const perBuyerSignals = {}; - igbid.igbuyer.forEach(buyerItem => { - perBuyerSignals[buyerItem.origin] = buyerItem.buyerdata; - }); - const bidRequest = request.bidRequests.find(b => b.bidId === igbid.impid); - const bidId = bidRequest.bidId; - let sellerSignals = body.ext.sellerSignals || {}; - if (!sellerSignals.floor && bidRequest.params.bidFloor) { - sellerSignals.floor = bidRequest.params.bidFloor; - } - let perBuyerTimeout = { '*': 500 }; - if (sellerSignals.perBuyerTimeout) { - for (const buyer in sellerSignals.perBuyerTimeout) { - perBuyerTimeout[buyer] = sellerSignals.perBuyerTimeout[buyer]; - } - } - let perBuyerGroupLimits = { '*': 60 }; - if (sellerSignals.perBuyerGroupLimits) { - for (const buyer in sellerSignals.perBuyerGroupLimits) { - perBuyerGroupLimits[buyer] = sellerSignals.perBuyerGroupLimits[buyer]; - } - } - if (body?.ext?.sellerSignalsPerImp !== undefined) { - const sellerSignalsPerImp = body.ext.sellerSignalsPerImp[bidId]; - if (sellerSignalsPerImp !== undefined) { - sellerSignals = {...sellerSignals, ...sellerSignalsPerImp}; - } + if (isArray(body.ext?.igi)) { + body.ext.igi.forEach((igi) => { + if (isArray(igi?.igs)) { + igi.igs.forEach((igs) => { + fledgeAuctionConfigs.push(igs); + }); } - fledgeAuctionConfigs.push({ - bidId, - config: { - seller, - sellerSignals, - sellerTimeout, - perBuyerSignals, - perBuyerTimeout, - perBuyerGroupLimits, - auctionSignals: {}, - decisionLogicUrl: FLEDGE_DECISION_LOGIC_URL, - interestGroupBuyers: Object.keys(perBuyerSignals), - sellerCurrency: sellerSignals.currency || '???', - }, - }); }); } diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index 726754f39aa..1709acb465f 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -2538,49 +2538,102 @@ describe('The Criteo bidding adapter', function () { }); it('should properly parse a bid response with FLEDGE auction configs', function () { + let auctionConfig1 = { + auctionSignals: {}, + decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', + interestGroupBuyers: ['https://first-buyer-domain.com', 'https://second-buyer-domain.com'], + perBuyerSignals: { + 'https://first-buyer-domain.com': { + foo: 'bar', + }, + 'https://second-buyer-domain.com': { + foo: 'baz' + }, + }, + perBuyerTimeout: { + '*': 500, + 'buyer1': 100, + 'buyer2': 200 + }, + perBuyerGroupLimits: { + '*': 60, + 'buyer1': 300, + 'buyer2': 400 + }, + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + foo2: 'bar2', + floor: 1, + currency: 'USD', + perBuyerTimeout: { + 'buyer1': 100, + 'buyer2': 200 + }, + perBuyerGroupLimits: { + 'buyer1': 300, + 'buyer2': 400 + }, + }, + sellerCurrency: 'USD', + }; + let auctionConfig2 = { + auctionSignals: {}, + decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', + interestGroupBuyers: ['https://first-buyer-domain.com', 'https://second-buyer-domain.com'], + perBuyerSignals: { + 'https://first-buyer-domain.com': { + foo: 'bar', + }, + 'https://second-buyer-domain.com': { + foo: 'baz' + }, + }, + perBuyerTimeout: { + '*': 500, + 'buyer1': 100, + 'buyer2': 200 + }, + perBuyerGroupLimits: { + '*': 60, + 'buyer1': 300, + 'buyer2': 400 + }, + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + floor: 1, + perBuyerTimeout: { + 'buyer1': 100, + 'buyer2': 200 + }, + perBuyerGroupLimits: { + 'buyer1': 300, + 'buyer2': 400 + }, + }, + sellerCurrency: '???' + }; const response = { body: { ext: { - igbid: [{ + igi: [{ impid: 'test-bidId', - igbuyer: [{ - origin: 'https://first-buyer-domain.com', - buyerdata: { - foo: 'bar', - }, - }, { - origin: 'https://second-buyer-domain.com', - buyerdata: { - foo: 'baz', - }, + igs: [{ + impid: 'test-bidId', + bidId: 'test-bidId', + config: auctionConfig1 }] }, { impid: 'test-bidId-2', - igbuyer: [{ - origin: 'https://first-buyer-domain.com', - buyerdata: { - foo: 'bar', - }, - }, { - origin: 'https://second-buyer-domain.com', - buyerdata: { - foo: 'baz', - }, + igs: [{ + impid: 'test-bidId-2', + bidId: 'test-bidId-2', + config: auctionConfig2 }] - }], - seller: 'https://seller-domain.com', - sellerTimeout: 500, - sellerSignals: { - foo: 'bar', - perBuyerTimeout: { 'buyer1': 100, 'buyer2': 200 }, - perBuyerGroupLimits: { 'buyer1': 300, 'buyer2': 400 }, - }, - sellerSignalsPerImp: { - 'test-bidId': { - foo2: 'bar2', - currency: 'USD' - }, - }, + }] }, }, }; @@ -2631,87 +2684,13 @@ describe('The Criteo bidding adapter', function () { expect(interpretedResponse.fledgeAuctionConfigs).to.have.lengthOf(2); expect(interpretedResponse.fledgeAuctionConfigs[0]).to.deep.equal({ bidId: 'test-bidId', - config: { - auctionSignals: {}, - decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', - interestGroupBuyers: ['https://first-buyer-domain.com', 'https://second-buyer-domain.com'], - perBuyerSignals: { - 'https://first-buyer-domain.com': { - foo: 'bar', - }, - 'https://second-buyer-domain.com': { - foo: 'baz' - }, - }, - perBuyerTimeout: { - '*': 500, - 'buyer1': 100, - 'buyer2': 200 - }, - perBuyerGroupLimits: { - '*': 60, - 'buyer1': 300, - 'buyer2': 400 - }, - seller: 'https://seller-domain.com', - sellerTimeout: 500, - sellerSignals: { - foo: 'bar', - foo2: 'bar2', - floor: 1, - currency: 'USD', - perBuyerTimeout: { - 'buyer1': 100, - 'buyer2': 200 - }, - perBuyerGroupLimits: { - 'buyer1': 300, - 'buyer2': 400 - }, - }, - sellerCurrency: 'USD', - }, + impid: 'test-bidId', + config: auctionConfig1, }); expect(interpretedResponse.fledgeAuctionConfigs[1]).to.deep.equal({ bidId: 'test-bidId-2', - config: { - auctionSignals: {}, - decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', - interestGroupBuyers: ['https://first-buyer-domain.com', 'https://second-buyer-domain.com'], - perBuyerSignals: { - 'https://first-buyer-domain.com': { - foo: 'bar', - }, - 'https://second-buyer-domain.com': { - foo: 'baz' - }, - }, - perBuyerTimeout: { - '*': 500, - 'buyer1': 100, - 'buyer2': 200 - }, - perBuyerGroupLimits: { - '*': 60, - 'buyer1': 300, - 'buyer2': 400 - }, - seller: 'https://seller-domain.com', - sellerTimeout: 500, - sellerSignals: { - foo: 'bar', - floor: 1, - perBuyerTimeout: { - 'buyer1': 100, - 'buyer2': 200 - }, - perBuyerGroupLimits: { - 'buyer1': 300, - 'buyer2': 400 - }, - }, - sellerCurrency: '???' - }, + impid: 'test-bidId-2', + config: auctionConfig2, }); }); From d7a9efbea5566b441a0d72ffe764bf728f1ae7c9 Mon Sep 17 00:00:00 2001 From: pashaGhub <48026915+pashaGhub@users.noreply.github.com> Date: Tue, 19 Mar 2024 19:04:05 +0200 Subject: [PATCH 3/7] setupad Bid Adapter: initial commit (#11008) * create setupadBidAdapter * add setupadBidAdapter * update setupadBidAdapter * update metrics collection * update analytics collection * update getUserSyncs * add setupadAnalyticsAdapter.js * test setupadAnalyticsAdapter * remove test: 1 * add GVLID && bug fixes && test updates * remove setupadAnalyticsAdapter * add userID module handling * add GVLID && bug fixes && test updates * remove setupadAnalyticsAdapter * add userID module handling * clean up && seat bugfix * clean up logs * add userID module handling * update md && clean up * Send setupad only on bidRequested * Fix bidResponse and bidWon responses * Improve bidResponse and bidWon logic * Revert changes to specific files * Remove test parameter * Fix multiple bidResponse and bidTimeout calls to getPixelUrl * eslint errors fixes(brackets added) * Add extra checks for events * Fix BIDDER_CODE const * update reporting endpoint * update setupadBidAdapter_spec.js REPORT_ENDPOINT * update readme * Revert "Merge branch 'prebid:master' into setupad-adapter" This reverts commit 1c14dbe88883e4c25bd303e2094c72f64e361877, reversing changes made to 7fe9ea569e144c37beeab83bda98564b543c7b09. * Revert "Revert "Merge branch 'prebid:master' into setupad-adapter"" This reverts commit a34e3e4983418d74e57055a12dc61e554c995089. * # This is a combination of 20 commits. # This is the 1st commit message: add setupadBidAdapter # This is the commit message #2: update setupadBidAdapter # This is the commit message #3: update metrics collection # This is the commit message #4: update analytics collection # This is the commit message #5: update getUserSyncs # This is the commit message #6: add setupadAnalyticsAdapter.js # This is the commit message #7: test setupadAnalyticsAdapter # This is the commit message #8: remove test: 1 # This is the commit message #9: add GVLID && bug fixes && test updates # This is the commit message #10: remove setupadAnalyticsAdapter # This is the commit message #11: add userID module handling # This is the commit message #12: clean up && seat bugfix # This is the commit message #13: add userID module handling # This is the commit message #14: add GVLID && bug fixes && test updates # This is the commit message #15: remove setupadAnalyticsAdapter # This is the commit message #16: add userID module handling # This is the commit message #17: clean up logs # This is the commit message #18: update md && clean up # This is the commit message #19: Send setupad only on bidRequested # This is the commit message #20: Fix bidResponse and bidWon responses * # This is a combination of 22 commits.tree 8abae7e6dffc9a21ad11770713ba485fc610028a parent cecfce3db84fdeca9bb9b66005f3c7aa6106d951 author pavel 1706627437 +0200 committer pavel 1706627437 +0200 gpgsig -----BEGIN PGP SIGNATURE----- iQIzBAABCAAdFiEEqGYI3KX/FkbObQG8FABtd4pCs/AFAmW5EW0ACgkQFABtd4pC s/CK3w//WWJSFUlycnnNKTV2XfdcBjooOeZZvjpXVthwr09CCC4uO//kw4bPluhn f5fcVFdXzrY1AZ6ch8Wo3msX/Pkso014jIGd5aIWcHpNYFtffACwH/40Y8AcJNZd bsOZxVK0awPTz/RihC5eY+0J3cP+iFWP/FlYJoHEQIBXq/Eg6mWoAhxwpL/JvxbY QbLFWsRn2ckQ6ftOZgm3/jh8VLaG1zWbWImlWEs5Zel+CorJBTniTj58VbApelYD TFMgbSR2I4NGVaqNIrHePnSMsDATxalQ2nZPwY6raKCHWIbvoUPIn/OpDMMbKgC7 nCwounNmObxFVoj3xusAZppzHpKPasY8xKWb2Kr7zfhZArsOMC6B7fYqQNK0cWG3 8RR/10oheJD9M2kRlfLiqnRv7ExY08SQ/ZMo9LA8BeRUGBXhh6++8FKhKIHvX1gL k1R5W6c+NNWP+PDFsmrFpMn+LpYdl84I7yfYK5dHuw80od7f1wuAVYpswi6Cziy9 /KY6/rfENvUrGTmWSh5GdDBel89ACCfFkasIKB92xhzKTfjzF/DXkc8XQZOMbt1j CsILgWMNfLPMo4Dlgdx/tYCSLLBNEtZ1/hhUcFQ3+0TzLf0GtMkvMnlBnDinqe1n 1P30fQ2I5W5NJKDPrCOnRymI6QOAPFXtMF11R81mbB9H8asft/E= =oJtZ -----END PGP SIGNATURE----- bugfixes # This is the commit message #22: Remove test parameter * # This is a combination of 26 commits. parent cecfce3db84fdeca9bb9b66005f3c7aa6106d951 author pavel 1706627437 +0200 committer pavel 1706627437 +0200 gpgsig -----BEGIN PGP SIGNATURE----- iQIzBAABCAAdFiEEqGYI3KX/FkbObQG8FABtd4pCs/AFAmW5EW0ACgkQFABtd4pC s/CK3w//WWJSFUlycnnNKTV2XfdcBjooOeZZvjpXVthwr09CCC4uO//kw4bPluhn f5fcVFdXzrY1AZ6ch8Wo3msX/Pkso014jIGd5aIWcHpNYFtffACwH/40Y8AcJNZd bsOZxVK0awPTz/RihC5eY+0J3cP+iFWP/FlYJoHEQIBXq/Eg6mWoAhxwpL/JvxbY QbLFWsRn2ckQ6ftOZgm3/jh8VLaG1zWbWImlWEs5Zel+CorJBTniTj58VbApelYD TFMgbSR2I4NGVaqNIrHePnSMsDATxalQ2nZPwY6raKCHWIbvoUPIn/OpDMMbKgC7 nCwounNmObxFVoj3xusAZppzHpKPasY8xKWb2Kr7zfhZArsOMC6B7fYqQNK0cWG3 8RR/10oheJD9M2kRlfLiqnRv7ExY08SQ/ZMo9LA8BeRUGBXhh6++8FKhKIHvX1gL k1R5W6c+NNWP+PDFsmrFpMn+LpYdl84I7yfYK5dHuw80od7f1wuAVYpswi6Cziy9 /KY6/rfENvUrGTmWSh5GdDBel89ACCfFkasIKB92xhzKTfjzF/DXkc8XQZOMbt1j CsILgWMNfLPMo4Dlgdx/tYCSLLBNEtZ1/hhUcFQ3+0TzLf0GtMkvMnlBnDinqe1n 1P30fQ2I5W5NJKDPrCOnRymI6QOAPFXtMF11R81mbB9H8asft/E= =oJtZ -----END PGP SIGNATURE----- bugfixes # This is the commit message #22: Remove test parameter # This is the commit message #23: Fix multiple bidResponse and bidTimeout calls to getPixelUrl # This is the commit message #25: eslint errors fixes(brackets added) # This is the commit message #26: Add extra checks for events * parent 75178b929c01b37d8bb2e83572ed83abf5849922 author pavel 1706627694 +0200 committer pavel 1706627694 +0200 gpgsig -----BEGIN PGP SIGNATURE----- iQIzBAABCAAdFiEEqGYI3KX/FkbObQG8FABtd4pCs/AFAmW5Em4ACgkQFABtd4pC s/BBUQ/+NXyHoxPM185YJLG9M1ySC/5vTT9W5mfwQ93cVDLCeuGnpsnmi4S21NuQ b7gSeokFjwztvVOUmh/xqMp4lTsvL53TUd00b1k4KGVSqgcF00Foit5g8fOGLYsI DAoqphYV6MWjpAun+II+ELY8QUkHR1cjTc7PEGtmf+8RnptGVdyJ6C9Ab8u9TQTY Apj5Srhfo3Tl8S+WScOxwwB/uqEJR4fhIrJyzFzdLDEb2olSPyrQUs87vQXlhEnK buPEg2F5JsRH6sw11Xp3TFNSZGxNnBSlTh9dixou5md4yRCv5a2TMef667N0BVDp lGgc7mCrRKXyqzphmmeHudiscEGFjtUPObXoHutSVw22wdARFCTpNFKBLLFn4v8o Zv1OvFdNprvHsoeW0HVlZdU7OKnDTRrko6DHk2AahxojjvAFEWuDsGYZNjhdQwRR lK1zm+SFQnKI0Eojd+f84fvKod9geGs640jyH/x5R4eYm4yjZb8SkRtd3cca88wS OuGq9LIkbU428b46l7VnDwudldTXPUU8eKfUtFRjdGtIWH9I3tK6TsRoCfTcXkv0 smxYiiU1XHjAkkPFWQWEeFdfZ071snFKVWouU0AoKiq+PdRoS8+3AJqIQUjlA2sH AybnSkv9KxY/Rs1bnvMubsQm1GF66qVrbxBU6FILBv1JZYwj4yA= =Gbog -----END PGP SIGNATURE----- bugfixes update setupadBidAdapter_spec.js REPORT_ENDPOINT update readme Revert "Merge branch 'prebid:master' into setupad-adapter" This reverts commit 1c14dbe88883e4c25bd303e2094c72f64e361877, reversing changes made to 7fe9ea569e144c37beeab83bda98564b543c7b09. Revert "Revert "Merge branch 'prebid:master' into setupad-adapter"" This reverts commit a34e3e4983418d74e57055a12dc61e554c995089. * change double quote to single quote --------- Co-authored-by: pavel Co-authored-by: Elgars Grodnis * bugfix setupadBidAdapter remove getAdEl, spelling correction * add onBidWon event onBidWon event handling moved from custom to native onBidWon method * minor bugfixes && remove funk getSiteObj && getDeviceObj --------- Co-authored-by: pavel Co-authored-by: Elgars Grodnis --- modules/setupadBidAdapter.js | 271 +++++++++++++++ modules/setupadBidAdapter.md | 35 ++ test/spec/modules/setupadBidAdapter_spec.js | 348 ++++++++++++++++++++ 3 files changed, 654 insertions(+) create mode 100644 modules/setupadBidAdapter.js create mode 100644 modules/setupadBidAdapter.md create mode 100644 test/spec/modules/setupadBidAdapter_spec.js diff --git a/modules/setupadBidAdapter.js b/modules/setupadBidAdapter.js new file mode 100644 index 00000000000..55677d51c56 --- /dev/null +++ b/modules/setupadBidAdapter.js @@ -0,0 +1,271 @@ +import { + _each, + createTrackPixelHtml, + deepAccess, + isStr, + getBidIdParameter, + triggerPixel, + logWarn, +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'setupad'; +const ENDPOINT = 'https://prebid.setupad.io/openrtb2/auction'; +const SYNC_ENDPOINT = 'https://cookie.stpd.cloud/sync?'; +const REPORT_ENDPOINT = 'https://adapter-analytics.setupad.io/api/adapter-analytics'; +const GVLID = 1241; +const TIME_TO_LIVE = 360; +const biddersCreativeIds = {}; + +function getEids(bidRequest) { + if (deepAccess(bidRequest, 'userIdAsEids')) return bidRequest.userIdAsEids; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + gvlid: GVLID, + + isBidRequestValid: function (bid) { + return !!(bid.params.placement_id && isStr(bid.params.placement_id)); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const requests = []; + + _each(validBidRequests, function (bid) { + const id = getBidIdParameter('placement_id', bid.params); + const accountId = getBidIdParameter('account_id', bid.params); + const auctionId = bid.auctionId; + const bidId = bid.bidId; + const eids = getEids(bid) || undefined; + let sizes = bid.sizes; + if (sizes && !Array.isArray(sizes[0])) sizes = [sizes]; + + const site = { + page: bidderRequest?.refererInfo?.page, + ref: bidderRequest?.refererInfo?.ref, + domain: bidderRequest?.refererInfo?.domain, + }; + const device = { + w: bidderRequest?.ortb2?.device?.w, + h: bidderRequest?.ortb2?.device?.h, + }; + + const payload = { + id: bid?.bidderRequestId, + ext: { + prebid: { + storedrequest: { + id: accountId || 'default', + }, + }, + }, + user: { ext: { eids } }, + device, + site, + imp: [], + }; + + const imp = { + id: bid.adUnitCode, + ext: { + prebid: { + storedrequest: { id }, + }, + }, + }; + + if (deepAccess(bid, 'mediaTypes.banner')) { + imp.banner = { + format: (sizes || []).map((s) => { + return { w: s[0], h: s[1] }; + }), + }; + } + + payload.imp.push(imp); + + const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + const uspConsent = bidderRequest && bidderRequest.uspConsent; + + if (gdprConsent || uspConsent) { + payload.regs = { ext: {} }; + + if (uspConsent) payload.regs.ext.us_privacy = uspConsent; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies !== 'undefined') { + payload.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; + } + + if (typeof gdprConsent.consentString !== 'undefined') { + payload.user.ext.consent = gdprConsent.consentString; + } + } + } + const params = bid.params; + + requests.push({ + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(payload), + options: { + contentType: 'text/plain', + withCredentials: true, + }, + + bidId, + params, + auctionId, + }); + }); + + return requests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + if ( + !serverResponse || + !serverResponse.body || + typeof serverResponse.body != 'object' || + Object.keys(serverResponse.body).length === 0 + ) { + logWarn('no response or body is malformed'); + return []; + } + + const serverBody = serverResponse.body; + const bidResponses = []; + + _each(serverBody.seatbid, (res) => { + _each(res.bid, (bid) => { + const requestId = bidRequest.bidId; + const params = bidRequest.params; + const { ad, adUrl } = getAd(bid); + + const bidResponse = { + requestId, + params, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.id, + currency: serverBody.cur, + netRevenue: true, + ttl: TIME_TO_LIVE, + meta: { + advertiserDomains: bid.adomain || [], + }, + }; + + // set a seat for creativeId for triggerPixel url + biddersCreativeIds[bidResponse.creativeId] = res.seat; + + bidResponse.ad = ad; + bidResponse.adUrl = adUrl; + bidResponses.push(bidResponse); + }); + }); + + return bidResponses; + }, + + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + if (!responses?.length) return []; + + const syncs = []; + const bidders = getBidders(responses); + + if (syncOptions.iframeEnabled && bidders) { + const queryParams = []; + + queryParams.push(`bidders=${bidders}`); + queryParams.push('gdpr=' + +gdprConsent.gdprApplies); + queryParams.push('gdpr_consent=' + gdprConsent.consentString); + queryParams.push('usp_consent=' + (uspConsent || '')); + + const strQueryParams = queryParams.join('&'); + + syncs.push({ + type: 'iframe', + url: SYNC_ENDPOINT + strQueryParams + '&type=iframe', + }); + + return syncs; + } + + return []; + }, + + onBidWon: function (bid) { + let bidder = bid.bidder || bid.bidderCode; + const auctionId = bid.auctionId; + if (bidder !== BIDDER_CODE) return; + + let params; + if (bid.params) { + params = Array.isArray(bid.params) ? bid.params : [bid.params]; + } else { + if (Array.isArray(bid.bids)) { + params = bid.bids.map((singleBid) => singleBid.params); + } + } + + if (!params?.length) return; + + const placementIdsArray = []; + params.forEach((param) => { + if (!param.placement_id) return; + placementIdsArray.push(param.placement_id); + }); + + const placementIds = (placementIdsArray.length && placementIdsArray.join(';')) || ''; + + if (!placementIds) return; + + let extraBidParams = ''; + + // find the winning bidder by using creativeId as identification + if (biddersCreativeIds.hasOwnProperty(bid.creativeId) && biddersCreativeIds[bid.creativeId]) { + bidder = biddersCreativeIds[bid.creativeId]; + } + + // Add extra parameters + extraBidParams = `&cpm=${bid.originalCpm}¤cy=${bid.originalCurrency}`; + + const url = `${REPORT_ENDPOINT}?event=bidWon&bidder=${bidder}&placementIds=${placementIds}&auctionId=${auctionId}${extraBidParams}×tamp=${Date.now()}`; + triggerPixel(url); + }, +}; + +function getBidders(serverResponse) { + const bidders = serverResponse + .map((res) => Object.keys(res.body.ext.responsetimemillis || [])) + .flat(1); + + if (bidders.length) { + return encodeURIComponent(JSON.stringify([...new Set(bidders)])); + } +} + +function getAd(bid) { + let ad, adUrl; + + switch (deepAccess(bid, 'ext.prebid.type')) { + default: + if (bid.adm && bid.nurl) { + ad = bid.adm; + ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); + } else if (bid.adm) { + ad = bid.adm; + } else if (bid.nurl) { + adUrl = bid.nurl; + } + } + + return { ad, adUrl }; +} + +registerBidder(spec); diff --git a/modules/setupadBidAdapter.md b/modules/setupadBidAdapter.md new file mode 100644 index 00000000000..0d4f0ef392e --- /dev/null +++ b/modules/setupadBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +```text +Module Name: Setupad Bid Adapter +Module Type: Bidder Adapter +Maintainer: it@setupad.com +``` + +# Description + +Module that connects to Setupad's demand sources. + +# Test Parameters + +```js +const adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: 'setupad', + params: { + placement_id: '123', //required + account_id: '123', //optional + }, + }, + ], + }, +]; +``` diff --git a/test/spec/modules/setupadBidAdapter_spec.js b/test/spec/modules/setupadBidAdapter_spec.js new file mode 100644 index 00000000000..d4ff73d005f --- /dev/null +++ b/test/spec/modules/setupadBidAdapter_spec.js @@ -0,0 +1,348 @@ +import { spec } from 'modules/setupadBidAdapter.js'; + +describe('SetupadAdapter', function () { + const userIdAsEids = [ + { + source: 'pubcid.org', + uids: [ + { + atype: 1, + id: '01EAJWWNEPN3CYMM5N8M5VXY22', + }, + ], + }, + ]; + + const bidRequests = [ + { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'rubicon', + bidderRequestId: '15246a574e859f', + uspConsent: 'usp-context-string', + gdprConsent: { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + gdprApplies: true, + }, + params: { + placement_id: '123', + account_id: 'test-account-id', + }, + sizes: [[300, 250]], + ortb2: { + device: { + w: 1500, + h: 1000, + }, + site: { + domain: 'test.com', + page: 'http://test.com', + }, + }, + userIdAsEids, + }, + ]; + + const bidderRequest = { + ortb2: { + device: { + w: 1500, + h: 1000, + }, + }, + refererInfo: { + domain: 'test.com', + page: 'http://test.com', + ref: '', + }, + }; + + const serverResponse = { + body: { + id: 'f7b3d2da-e762-410c-b069-424f92c4c4b2', + seatbid: [ + { + bid: [ + { + id: 'test-bid-id', + price: 0.8, + adm: 'this is an ad', + adid: 'test-ad-id', + adomain: ['test.addomain.com'], + w: 300, + h: 250, + }, + ], + seat: 'testBidder', + }, + ], + cur: 'USD', + ext: { + sync: { + image: ['urlA?gdpr={{.GDPR}}'], + iframe: ['urlB'], + }, + }, + }, + }; + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'setupad', + params: { + placement_id: '123', + }, + }; + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false when required params are not passed', function () { + delete bid.params.placement_id; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('check request params with GDPR and USP', function () { + const request = spec.buildRequests(bidRequests, bidRequests[0]); + expect(JSON.parse(request[0].data).user.ext.consent).to.equal( + 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA' + ); + expect(JSON.parse(request[0].data).regs.ext.gdpr).to.equal(1); + expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('usp-context-string'); + }); + + it('check request params without GDPR', function () { + let bidRequestsWithoutGDPR = Object.assign({}, bidRequests[0]); + delete bidRequestsWithoutGDPR.gdprConsent; + const request = spec.buildRequests([bidRequestsWithoutGDPR], bidRequestsWithoutGDPR); + expect(JSON.parse(request[0].data).regs.ext.gdpr).to.be.undefined; + expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('usp-context-string'); + }); + + it('should return correct storedrequest id if account_id is provided', function () { + const request = spec.buildRequests(bidRequests, bidRequests[0]); + expect(JSON.parse(request[0].data).ext.prebid.storedrequest.id).to.equal('test-account-id'); + }); + + it('should return correct storedrequest id if account_id is not provided', function () { + let bidRequestsWithoutAccountId = Object.assign({}, bidRequests[0]); + delete bidRequestsWithoutAccountId.params.account_id; + const request = spec.buildRequests( + [bidRequestsWithoutAccountId], + bidRequestsWithoutAccountId + ); + expect(JSON.parse(request[0].data).ext.prebid.storedrequest.id).to.equal('default'); + }); + + it('validate generated params', function () { + const request = spec.buildRequests(bidRequests); + expect(request[0].bidId).to.equal('22c4871113f461'); + expect(JSON.parse(request[0].data).id).to.equal('15246a574e859f'); + }); + + it('check if correct site object was added', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const siteObj = JSON.parse(request[0].data).site; + + expect(siteObj.domain).to.equal('test.com'); + expect(siteObj.page).to.equal('http://test.com'); + expect(siteObj.ref).to.equal(''); + }); + + it('check if correct device object was added', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const deviceObj = JSON.parse(request[0].data).device; + + expect(deviceObj.w).to.equal(1500); + expect(deviceObj.h).to.equal(1000); + }); + + it('check if imp object was added', function () { + const request = spec.buildRequests(bidRequests); + expect(JSON.parse(request[0].data).imp).to.be.an('array'); + }); + + it('should send "user.ext.eids" in the request for Prebid.js supported modules only', function () { + const request = spec.buildRequests(bidRequests); + expect(JSON.parse(request[0].data).user.ext.eids).to.deep.equal(userIdAsEids); + }); + + it('should send an undefined "user.ext.eids" in the request if userId module is unsupported', function () { + let bidRequestsUnsupportedUserIdModule = Object.assign({}, bidRequests[0]); + delete bidRequestsUnsupportedUserIdModule.userIdAsEids; + const request = spec.buildRequests(bidRequestsUnsupportedUserIdModule); + + expect(JSON.parse(request[0].data).user.ext.eids).to.be.undefined; + }); + }); + + describe('getUserSyncs', () => { + it('should return user sync', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true, + }; + const responses = [ + { + body: { + ext: { + responsetimemillis: { + 'test seat 1': 2, + 'test seat 2': 1, + }, + }, + }, + }, + ]; + const gdprConsent = { + gdprApplies: 1, + consentString: 'dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig', + }; + const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb'; + const expectedUserSyncs = [ + { + type: 'iframe', + url: 'https://cookie.stpd.cloud/sync?bidders=%5B%22test%20seat%201%22%2C%22test%20seat%202%22%5D&gdpr=1&gdpr_consent=dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig&usp_consent=mkjvbiniwot4827obfoy8sdg8203gb&type=iframe', + }, + ]; + + const userSyncs = spec.getUserSyncs(syncOptions, responses, gdprConsent, uspConsent); + + expect(userSyncs).to.deep.equal(expectedUserSyncs); + }); + + it('should return empty user syncs when responsetimemillis is not defined', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true, + }; + const responses = [ + { + body: { + ext: {}, + }, + }, + ]; + const gdprConsent = { + gdprApplies: 1, + consentString: 'dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig', + }; + const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb'; + const expectedUserSyncs = []; + + const userSyncs = spec.getUserSyncs(syncOptions, responses, gdprConsent, uspConsent); + + expect(userSyncs).to.deep.equal(expectedUserSyncs); + }); + }); + + describe('interpretResponse', function () { + it('should return empty array if error during parsing', () => { + const wrongServerResponse = 'wrong data'; + let request = spec.buildRequests(bidRequests, bidRequests[0]); + let result = spec.interpretResponse(wrongServerResponse, request); + + expect(result).to.be.instanceof(Array); + expect(result.length).to.equal(0); + }); + + it('should get correct bid response', function () { + const result = spec.interpretResponse(serverResponse, bidRequests[0]); + expect(result).to.be.an('array').with.lengthOf(1); + expect(result[0].requestId).to.equal('22c4871113f461'); + expect(result[0].cpm).to.equal(0.8); + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal('test-bid-id'); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(360); + expect(result[0].ad).to.equal('this is an ad'); + }); + }); + + describe('onBidWon', function () { + it('should stop if bidder is not equal to BIDDER_CODE', function () { + const bid = { + bidder: 'rubicon', + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + + it('should stop if bid.params is not provided', function () { + const bid = { + bidder: 'setupad', + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + + it('should stop if bid.params is empty array', function () { + const bid = { + bidder: 'setupad', + params: [], + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + + it('should stop if bid.params is not array', function () { + expect( + spec.onBidWon({ + bidder: 'setupad', + params: {}, + }) + ).to.be.undefined; + + expect( + spec.onBidWon({ + bidder: 'setupad', + params: 'test', + }) + ).to.be.undefined; + + expect( + spec.onBidWon({ + bidder: 'setupad', + params: 1, + }) + ).to.be.undefined; + + expect( + spec.onBidWon({ + bidder: 'setupad', + params: null, + }) + ).to.be.undefined; + + expect( + spec.onBidWon({ + bidder: 'setupad', + params: undefined, + }) + ).to.be.undefined; + }); + + it('should stop if bid.params.placement_id is not provided', function () { + const bid = { + bidder: 'setupad', + params: [{ account_id: 'test' }], + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + + it('should stop if bid.params is not provided and bid.bids is not an array', function () { + const bid = { + bidder: 'setupad', + params: undefined, + bids: {}, + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + }); +}); From e2f96ab44738df8a6ba05beeecae84c1c879baac Mon Sep 17 00:00:00 2001 From: Aymeric Le Corre Date: Tue, 19 Mar 2024 18:45:25 +0100 Subject: [PATCH 4/7] New Bidder Adapter: Adlive Plus (#11176) * New Bidder adpater : Adlive Plus * remove adlive plus md file (alias) --- modules/luceadBidAdapter.js | 8 +++++--- modules/luceadBidAdapter.md | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/luceadBidAdapter.js b/modules/luceadBidAdapter.js index 299bd47a8e4..ab7f96c4e60 100644 --- a/modules/luceadBidAdapter.js +++ b/modules/luceadBidAdapter.js @@ -5,15 +5,17 @@ import {getUniqueIdentifierStr, logInfo, deepSetValue} from '../src/utils.js'; import {fetch} from '../src/ajax.js'; const bidderCode = 'lucead'; +const bidderName = 'Lucead'; let baseUrl = 'https://lucead.com'; let staticUrl = 'https://s.lucead.com'; let companionUrl = 'https://cdn.jsdelivr.net/gh/lucead/prebid-js-external-js-lucead@master/dist/prod.min.js'; let endpointUrl = 'https://prebid.lucead.com/go'; const defaultCurrency = 'EUR'; const defaultTtl = 500; +const aliases = ['adliveplus']; function isDevEnv() { - return location.hostname.endsWith('.ngrok-free.app') || location.href.startsWith('https://ayads.io/test'); + return location.hash.includes('prebid-dev') || location.href.startsWith('https://ayads.io/test'); } function isBidRequestValid(bidRequest) { @@ -21,7 +23,7 @@ function isBidRequestValid(bidRequest) { } export function log(msg, obj) { - logInfo('Lucead - ' + msg, obj); + logInfo(`${bidderName} - ${msg}`, obj); } function buildRequests(bidRequests, bidderRequest) { @@ -149,7 +151,7 @@ function onTimeout(timeoutData) { export const spec = { code: bidderCode, // gvlid: BIDDER_GVLID, - aliases: [], + aliases, isBidRequestValid, buildRequests, interpretResponse, diff --git a/modules/luceadBidAdapter.md b/modules/luceadBidAdapter.md index d12d081f0b7..953c911cd2b 100644 --- a/modules/luceadBidAdapter.md +++ b/modules/luceadBidAdapter.md @@ -18,9 +18,9 @@ const adUnits = [ sizes: [[300, 250]], bids: [ { - bidder: "lucead", + bidder: 'lucead', params: { - placementId: '1', + placementId: '2', } } ] From 6d349412c18dbd1d93a136e7683cda10ca1644b6 Mon Sep 17 00:00:00 2001 From: tudou <42998776+lhxx121@users.noreply.github.com> Date: Wed, 20 Mar 2024 05:15:08 +0800 Subject: [PATCH 5/7] Discovery Bid Adapter : support topics (#11209) * Discovery Bid Adapter : add title, desc, keywords, hLen, nbw, hc, dm add unit test resolve conflict * Discovery Bid Adapter : add title, desc, keywords, hLen, nbw, hc, dm add unit test * Discovery Bid Adapter : synchronize mguid from third party cookie to first party cookie * test * Discovery Bid Adapter : Extend the expiration time of pmguid * Discovery Bid Adapter : Extend the expiration time of pmguid * Discovery Bid Adapter : Extend the expiration time of pmguid * Discovery Bid Adapter : Extend the expiration time of pmguid * Discovery Bid Adapter : support topics * Discovery Bid Adapter : support topics * Discovery Bid Adapter : support topics * Discovery Bid Adapter : support topics --------- Co-authored-by: lvhuixin --- modules/discoveryBidAdapter.js | 2 ++ modules/topicsFpdModule.js | 3 +++ modules/topicsFpdModule.md | 4 ++++ test/spec/modules/discoveryBidAdapter_spec.js | 23 +++++++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index aa497b99d00..de2fd3c3a94 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -437,6 +437,7 @@ function getParam(validBidRequests, bidderRequest) { const page = utils.deepAccess(bidderRequest, 'refererInfo.page'); const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); const firstPartyData = bidderRequest.ortb2; + const tpData = utils.deepAccess(bidderRequest, 'ortb2.user.data') || undefined; const topWindow = window.top; const title = getPageTitle(); const desc = getPageDescription(); @@ -463,6 +464,7 @@ function getParam(validBidRequests, bidderRequest) { firstPartyData, ssppid: storage.getCookie(COOKIE_KEY_SSPPID) || undefined, pmguid: getPmgUID(), + tpData, page: { title: title ? title.slice(0, 100) : undefined, desc: desc ? desc.slice(0, 300) : undefined, diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index 748242142c4..715f1ca735a 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -41,6 +41,9 @@ const bidderIframeList = { }, { bidder: 'taboola', iframeURL: 'https://cdn.taboola.com/libtrc/static/topics/taboola-prebid-browsing-topics.html' + }, { + bidder: 'discovery', + iframeURL: 'https://api.popin.cc/topic/prebid-topics-frame.html' }] } diff --git a/modules/topicsFpdModule.md b/modules/topicsFpdModule.md index e8daded4439..8ebddacf613 100644 --- a/modules/topicsFpdModule.md +++ b/modules/topicsFpdModule.md @@ -60,6 +60,10 @@ pbjs.setConfig({ bidder: 'taboola', iframeURL: 'https://cdn.taboola.com/libtrc/static/topics/taboola-prebid-browsing-topics.html', expiry: 7 // Configurable expiry days + }, { + bidder: 'discovery', + iframeURL: 'https://api.popin.cc/topic/prebid-topics-frame.html', + expiry: 7 // Configurable expiry days }] } .... diff --git a/test/spec/modules/discoveryBidAdapter_spec.js b/test/spec/modules/discoveryBidAdapter_spec.js index 4fb4c29b99b..f1475ec3739 100644 --- a/test/spec/modules/discoveryBidAdapter_spec.js +++ b/test/spec/modules/discoveryBidAdapter_spec.js @@ -88,6 +88,22 @@ describe('discovery:BidAdapterTests', function () { bidderWinsCount: 0, }, ], + ortb2: { + user: { + data: { + segment: [ + { + id: '412' + } + ], + name: 'test.popin.cc', + ext: { + segclass: '1', + segtax: 503 + } + } + } + } }; let request = []; @@ -189,6 +205,13 @@ describe('discovery:BidAdapterTests', function () { let req_data = JSON.parse(request.data); expect(req_data.imp).to.have.lengthOf(1); }); + describe('first party data', function () { + it('should pass additional parameter in request for topics', function () { + const request = spec.buildRequests(bidRequestData.bids, bidRequestData); + let res = JSON.parse(request.data); + expect(res.ext.tpData).to.deep.equal(bidRequestData.ortb2.user.data); + }); + }); describe('discovery: buildRequests', function() { describe('getPmgUID function', function() { From 0cdc6ec4f16ffa4f13c6509831f22dce757de8af Mon Sep 17 00:00:00 2001 From: Matthieu Wipliez <89922776+github-matthieu-wipliez@users.noreply.github.com> Date: Wed, 20 Mar 2024 01:19:13 +0100 Subject: [PATCH 6/7] Autoplay detection library: initial release && Teads Bid Adapter: detect autoplay (#11222) * Add autoplay library * Filter out bids with needAutoplay if autoplay is disabled * Refactoring + add test * Simplify logic for filtering bids * Start detection in autoplay.js directly --- libraries/autoplayDetection/autoplay.js | 42 ++++++++++++++ modules/teadsBidAdapter.js | 18 ++++-- test/spec/modules/teadsBidAdapter_spec.js | 70 ++++++++++++++++++++++- 3 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 libraries/autoplayDetection/autoplay.js diff --git a/libraries/autoplayDetection/autoplay.js b/libraries/autoplayDetection/autoplay.js new file mode 100644 index 00000000000..b598e46cbd1 --- /dev/null +++ b/libraries/autoplayDetection/autoplay.js @@ -0,0 +1,42 @@ +let autoplayEnabled = null; + +/** + * Note: this function returns true if detection is not done yet. This is by design: if autoplay is not allowed, + * the call to video.play() will fail immediately, otherwise it may not terminate. + * @returns true if autoplay is not forbidden + */ +export const isAutoplayEnabled = () => autoplayEnabled !== false; + +// generated with: +// ask ChatGPT for a 160x90 black PNG image (1/8th the size of 720p) +// +// encode with: +// ffmpeg -i black_image_160x90.png -r 1 -c:v libx264 -bsf:v 'filter_units=remove_types=6' -pix_fmt yuv420p autoplay.mp4 +// this creates a 1 second long, 1 fps YUV 4:2:0 video encoded with H.264 without encoder details. +// +// followed by: +// echo "data:video/mp4;base64,$(base64 -i autoplay.mp4)" + +const autoplayVideoUrl = + 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAADxtZGF0AAAAMGWIhAAV//73ye/Apuvb3rW/k89I/Cy3PsIqP39atohOSV14BYa1heKCYgALQC5K4QAAAwZtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAD6AABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACMHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAD6AAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAoAAAAFoAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAA+gAAAAAAAEAAAAAAahtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAAEAAAABAAFXEAAAAAAAtaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAFZpZGVvSGFuZGxlcgAAAAFTbWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABE3N0YmwAAACvc3RzZAAAAAAAAAABAAAAn2F2YzEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAoABaAEgAAABIAAAAAAAAAAEVTGF2YzYwLjMxLjEwMiBsaWJ4MjY0AAAAAAAAAAAAAAAY//8AAAA1YXZjQwFkAAr/4QAYZ2QACqzZQo35IQAAAwABAAADAAIPEiWWAQAGaOvjyyLA/fj4AAAAABRidHJ0AAAAAAAAAaAAAAGgAAAAGHN0dHMAAAAAAAAAAQAAAAEAAEAAAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAABRzdHN6AAAAAAAAADQAAAABAAAAFHN0Y28AAAAAAAAAAQAAADAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjYwLjE2LjEwMA=='; + +function startDetection() { + // we create an HTMLVideoElement muted and not displayed in which we try to play a one frame video + const videoElement = document.createElement('video'); + videoElement.src = autoplayVideoUrl; + videoElement.setAttribute('playsinline', 'true'); + videoElement.muted = true; + + videoElement + .play() + .then(() => { + autoplayEnabled = true; + videoElement.pause(); + }) + .catch(() => { + autoplayEnabled = false; + }); +} + +// starts detection as soon as this library is loaded +startDetection(); diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index d03782611e4..1108c12c822 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -1,6 +1,7 @@ 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 {isAutoplayEnabled} from '../libraries/autoplayDetection/autoplay.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -120,11 +121,18 @@ export const spec = { * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse, bidderRequest) { - const bidResponses = []; serverResponse = serverResponse.body; - if (serverResponse.responses) { - serverResponse.responses.forEach(function (bid) { + if (!serverResponse.responses) { + return []; + } + + const autoplayEnabled = isAutoplayEnabled(); + return serverResponse.responses + .filter((bid) => + // ignore this bid if it requires autoplay but it is not enabled on this browser + !bid.needAutoplay || autoplayEnabled + ).map((bid) => { const bidResponse = { cpm: bid.cpm, width: bid.width, @@ -146,10 +154,8 @@ export const spec = { if (bid?.ext?.dsa) { bidResponse.meta.dsa = bid.ext.dsa; } - bidResponses.push(bidResponse); + return bidResponse; }); - } - return bidResponses; } }; diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index f26081b0cef..1e044651315 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -1,7 +1,7 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/teadsBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; -import { off } from '../../../src/events'; +import * as autoplay from 'libraries/autoplayDetection/autoplay.js' const ENDPOINT = 'https://a.teads.tv/hb/bid-request'; const AD_SCRIPT = '"'; @@ -1059,7 +1059,8 @@ describe('teadsBidAdapter', () => { 'ttl': 360, 'width': 300, 'creativeId': 'er2ee', - 'placementId': 34 + 'placementId': 34, + 'needAutoplay': true }, { 'ad': AD_SCRIPT, 'cpm': 0.5, @@ -1070,6 +1071,7 @@ describe('teadsBidAdapter', () => { 'width': 350, 'creativeId': 'fs3ff', 'placementId': 34, + 'needAutoplay': false, 'dealId': 'ABC_123', 'ext': { 'dsa': { @@ -1132,6 +1134,70 @@ describe('teadsBidAdapter', () => { expect(result).to.eql(expectedResponse); }); + it('should filter bid responses with needAutoplay:true when autoplay is disabled', function() { + let bids = { + 'body': { + 'responses': [{ + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 250, + 'bidId': '3ede2a3fa0db94', + 'ttl': 360, + 'width': 300, + 'creativeId': 'er2ee', + 'placementId': 34, + 'needAutoplay': true + }, { + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 200, + 'bidId': '4fef3b4gb1ec15', + 'ttl': 360, + 'width': 350, + 'creativeId': 'fs3ff', + 'placementId': 34, + 'needAutoplay': false + }, { + 'ad': AD_SCRIPT, + 'cpm': 0.7, + 'currency': 'USD', + 'height': 600, + 'bidId': 'a987fbc961d', + 'ttl': 12, + 'width': 300, + 'creativeId': 'awuygfd', + 'placementId': 12, + 'needAutoplay': true + }] + } + }; + let expectedResponse = [{ + 'cpm': 0.5, + 'width': 350, + 'height': 200, + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [], + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '4fef3b4gb1ec15', + 'creativeId': 'fs3ff', + 'placementId': 34 + } + ] + ; + + const isAutoplayEnabledStub = sinon.stub(autoplay, 'isAutoplayEnabled'); + isAutoplayEnabledStub.returns(false); + let result = spec.interpretResponse(bids); + isAutoplayEnabledStub.restore(); + expect(result).to.eql(expectedResponse); + }); + it('handles nobid responses', function() { let bids = { 'body': { From 5c139d0ff20e859c3f8bec6a5770ce2ab8605d3b Mon Sep 17 00:00:00 2001 From: awiackiewicz <40166510+awiackiewicz@users.noreply.github.com> Date: Wed, 20 Mar 2024 17:08:53 +0100 Subject: [PATCH 7/7] Adquery Bid Adapter: bidWon bugfix (#11227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * removed ad data from bidWon event handler * removed ad data from bidWon event handler --------- Co-authored-by: Adam Wiąckiewicz --- modules/adqueryBidAdapter.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index bfcc56050fb..f19cf020ca8 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -134,10 +134,9 @@ export const spec = { */ onBidWon: (bid) => { logInfo('onBidWon', bid); - const bidString = JSON.stringify(bid); - let copyOfBid = JSON.parse(bidString); - delete copyOfBid.ad; - const shortBidString = JSON.stringify(bid); + let copyOfBid = { ...bid } + delete copyOfBid.ad + const shortBidString = JSON.stringify(copyOfBid); const encodedBuf = window.btoa(shortBidString); let params = {