From 3750395a6202dadd4c63e312af77f4a3bbd0d808 Mon Sep 17 00:00:00 2001 From: koshe Date: Tue, 13 Feb 2024 13:38:55 +0100 Subject: [PATCH 1/5] Add mediaimpact bid adapter --- modules/mediaimpactBidAdapter.js | 211 +++++++++++++++++++++++++++++++ modules/mediaimpactBidAdapter.md | 44 +++++++ 2 files changed, 255 insertions(+) create mode 100644 modules/mediaimpactBidAdapter.js create mode 100644 modules/mediaimpactBidAdapter.md diff --git a/modules/mediaimpactBidAdapter.js b/modules/mediaimpactBidAdapter.js new file mode 100644 index 00000000000..9a86632f052 --- /dev/null +++ b/modules/mediaimpactBidAdapter.js @@ -0,0 +1,211 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { buildUrl } from '../src/utils.js' +import {ajax} from '../src/ajax.js'; + +const BIDDER_CODE = 'mediaimpact'; +export const ENDPOINT_PROTOCOL = 'https'; +export const ENDPOINT_DOMAIN = 'bidder.smartytouch.co'; +export const ENDPOINT_PATH = '/hb/bid'; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function (bidRequest) { + return !!parseInt(bidRequest.params.unitId) || !!parseInt(bidRequest.params.partnerId); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + // TODO does it make sense to fall back to window.location.href? + const referer = bidderRequest?.refererInfo?.page || window.location.href; + + let bidRequests = []; + let beaconParams = { + tag: [], + partner: [], + sizes: [], + referer: '' + }; + + validBidRequests.forEach(function(validBidRequest) { + let bidRequestObject = { + adUnitCode: validBidRequest.adUnitCode, + sizes: validBidRequest.sizes, + bidId: validBidRequest.bidId, + referer: referer + }; + + if (parseInt(validBidRequest.params.unitId)) { + bidRequestObject.unitId = parseInt(validBidRequest.params.unitId); + beaconParams.tag.push(validBidRequest.params.unitId); + } + + if (parseInt(validBidRequest.params.partnerId)) { + bidRequestObject.unitId = 0; + bidRequestObject.partnerId = parseInt(validBidRequest.params.partnerId); + beaconParams.partner.push(validBidRequest.params.partnerId); + } + + bidRequests.push(bidRequestObject); + + beaconParams.sizes.push(spec.joinSizesToString(validBidRequest.sizes)); + beaconParams.referer = encodeURIComponent(referer); + }); + + if (beaconParams.partner.length > 0) { + beaconParams.partner = beaconParams.partner.join(','); + } else { + delete beaconParams.partner; + } + + beaconParams.tag = beaconParams.tag.join(','); + beaconParams.sizes = beaconParams.sizes.join(','); + + let adRequestUrl = buildUrl({ + protocol: ENDPOINT_PROTOCOL, + hostname: ENDPOINT_DOMAIN, + pathname: ENDPOINT_PATH, + search: beaconParams + }); + + return { + method: 'POST', + url: adRequestUrl, + data: JSON.stringify(bidRequests) + }; + }, + + joinSizesToString: function(sizes) { + let res = []; + sizes.forEach(function(size) { + res.push(size.join('x')); + }); + + return res.join('|'); + }, + + interpretResponse: function (serverResponse, bidRequest) { + const validBids = JSON.parse(bidRequest.data); + + if (typeof serverResponse.body === 'undefined') { + return []; + } + + return validBids + .map(bid => ({ + bid: bid, + ad: serverResponse.body[bid.adUnitCode] + })) + .filter(item => item.ad) + .map(item => spec.adResponse(item.bid, item.ad)); + }, + + adResponse: function(bid, ad) { + const bidObject = { + requestId: bid.bidId, + ad: ad.ad, + cpm: ad.cpm, + width: ad.width, + height: ad.height, + ttl: 60, + creativeId: ad.creativeId, + netRevenue: ad.netRevenue, + currency: ad.currency, + winNotification: ad.winNotification + } + + bidObject.meta = {}; + if (ad.adomain && ad.adomain.length > 0) { + bidObject.meta.advertiserDomains = ad.adomain; + } + + return bidObject; + }, + + onBidWon: function(data) { + data.winNotification.forEach(function(unitWon) { + let adBidWonUrl = buildUrl({ + protocol: ENDPOINT_PROTOCOL, + hostname: ENDPOINT_DOMAIN, + pathname: unitWon.path + }); + + if (unitWon.method === 'POST') { + spec.postRequest(adBidWonUrl, JSON.stringify(unitWon.data)); + } + }); + + return true; + }, + + postRequest(endpoint, data) { + ajax(endpoint, null, data, {method: 'POST'}); + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return syncs; + } + + let appendGdprParams = function (url, gdprParams) { + if (gdprParams === null) { + return url; + } + + return url + (url.indexOf('?') >= 0 ? '&' : '?') + gdprParams; + }; + + let gdprParams = null; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `gdpr_consent=${gdprConsent.consentString}`; + } + } + + serverResponses.forEach(resp => { + if (resp.body) { + Object.keys(resp.body).map(function(key, index) { + let respObject = resp.body[key]; + if (respObject['syncs'] !== undefined && + Array.isArray(respObject.syncs) && + respObject.syncs.length > 0) { + if (syncOptions.iframeEnabled) { + respObject.syncs.filter(function (syncIframeObject) { + if (syncIframeObject['type'] !== undefined && + syncIframeObject['link'] !== undefined && + syncIframeObject.type === 'iframe') { return true; } + return false; + }).forEach(function (syncIframeObject) { + syncs.push({ + type: 'iframe', + url: appendGdprParams(syncIframeObject.link, gdprParams) + }); + }); + } + if (syncOptions.pixelEnabled) { + respObject.syncs.filter(function (syncImageObject) { + if (syncImageObject['type'] !== undefined && + syncImageObject['link'] !== undefined && + syncImageObject.type === 'image') { return true; } + return false; + }).forEach(function (syncImageObject) { + syncs.push({ + type: 'image', + url: appendGdprParams(syncImageObject.link, gdprParams) + }); + }); + } + } + }); + } + }); + + return syncs; + }, + +} + +registerBidder(spec); diff --git a/modules/mediaimpactBidAdapter.md b/modules/mediaimpactBidAdapter.md new file mode 100644 index 00000000000..2fc6697fffb --- /dev/null +++ b/modules/mediaimpactBidAdapter.md @@ -0,0 +1,44 @@ +# Overview + +Module Name: MEDIAIMPACT Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: Info@mediaimpact.com.ua + +# Description + +You can use this adapter to get a bid from mediaimpact.com.ua. + +About us : https://mediaimpact.com.ua + + +# Test Parameters +```javascript + var adUnits = [ + { + code: 'div-ad-example', + sizes: [[300, 250]], + bids: [ + { + bidder: "mediaimpact", + params: { + unitId: 6698 + } + } + ] + }, + { + code: 'div-ad-example-2', + sizes: [[300, 250]], + bids: [ + { + bidder: "mediaimpact", + params: { + partnerId: 6698 + } + } + ] + } + ]; +``` From 26e4dc9b2791f6183988ecc103d8703e86efaa68 Mon Sep 17 00:00:00 2001 From: koshe Date: Tue, 13 Feb 2024 14:08:36 +0100 Subject: [PATCH 2/5] Add mediaimpact bid adapter tests --- .../modules/mediaimpactBidAdapter_spec.js | 336 ++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 test/spec/modules/mediaimpactBidAdapter_spec.js diff --git a/test/spec/modules/mediaimpactBidAdapter_spec.js b/test/spec/modules/mediaimpactBidAdapter_spec.js new file mode 100644 index 00000000000..3d706e59c3f --- /dev/null +++ b/test/spec/modules/mediaimpactBidAdapter_spec.js @@ -0,0 +1,336 @@ +import {expect} from 'chai'; +import {spec, ENDPOINT_PROTOCOL, ENDPOINT_DOMAIN, ENDPOINT_PATH} from 'modules/mediaimpactBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'mediaimpact'; + +describe('MediaimpactAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.be.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + let validRequest = { + 'params': { + 'unitId': 123 + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(true); + }); + + it('should return true when required params is srting', function () { + let validRequest = { + 'params': { + 'unitId': '456' + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let validRequest = { + 'params': { + 'unknownId': 123 + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + + it('should return false when required params is 0', function () { + let validRequest = { + 'params': { + 'unitId': 0 + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let validEndpoint = ENDPOINT_PROTOCOL + '://' + ENDPOINT_DOMAIN + ENDPOINT_PATH + '?tag=123,456&partner=777&sizes=300x250|300x600,728x90,300x250&referer=https%3A%2F%2Ftest.domain'; + + let validRequest = [ + { + 'bidder': BIDDER_CODE, + 'params': { + 'unitId': 123 + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e' + }, + { + 'bidder': BIDDER_CODE, + 'params': { + 'unitId': '456' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '22aidtbx5eabd9' + }, + { + 'bidder': BIDDER_CODE, + 'params': { + 'partnerId': 777 + }, + 'adUnitCode': 'partner-code-3', + 'sizes': [[300, 250]], + 'bidId': '5d4531d5a6c013' + } + ]; + + let bidderRequest = { + refererInfo: { + page: 'https://test.domain' + } + }; + + it('bidRequest HTTP method', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request.method).to.equal('POST'); + }); + + it('bidRequest url', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request.url).to.equal(validEndpoint); + }); + + it('bidRequest data', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload[0].unitId).to.equal(123); + expect(payload[0].sizes).to.deep.equal([[300, 250], [300, 600]]); + expect(payload[0].bidId).to.equal('30b31c1838de1e'); + expect(payload[1].unitId).to.equal(456); + expect(payload[1].sizes).to.deep.equal([[728, 90]]); + expect(payload[1].bidId).to.equal('22aidtbx5eabd9'); + expect(payload[2].partnerId).to.equal(777); + expect(payload[2].sizes).to.deep.equal([[300, 250]]); + expect(payload[2].bidId).to.equal('5d4531d5a6c013'); + }); + }); + + describe('joinSizesToString', function () { + it('success convert sizes list to string', function () { + const sizesStr = spec.joinSizesToString([[300, 250], [300, 600]]); + expect(sizesStr).to.equal('300x250|300x600'); + }); + }); + + describe('interpretResponse', function () { + const bidRequest = { + 'method': 'POST', + 'url': ENDPOINT_PROTOCOL + '://' + ENDPOINT_DOMAIN + ENDPOINT_PATH + '?tag=123,456&partner=777code=adunit-code-1,adunit-code-2,partner-code-3&bid=30b31c1838de1e,22aidtbx5eabd9,5d4531d5a6c013&sizes=300x250|300x600,728x90,300x250&referer=https%3A%2F%2Ftest.domain', + 'data': '[{"unitId": 13144370,"adUnitCode": "div-gpt-ad-1460505748561-0","sizes": [[300, 250], [300, 600]],"bidId": "2bdcb0b203c17d","referer": "https://test.domain/index.html"},' + + '{"unitId": 13144370,"adUnitCode":"div-gpt-ad-1460505748561-1","sizes": [[768, 90]],"bidId": "3dc6b8084f91a8","referer": "https://test.domain/index.html"},' + + '{"unitId": 0,"partnerId": 777,"adUnitCode":"div-gpt-ad-1460505748561-2","sizes": [[300, 250]],"bidId": "5d4531d5a6c013","referer": "https://test.domain/index.html"}]' + }; + + const bidResponse = { + body: { + 'div-gpt-ad-1460505748561-0': + { + 'ad': '
ad
', + 'width': 300, + 'height': 250, + 'creativeId': '8:123456', + 'adomain': [ + 'test.domain' + ], + 'syncs': [ + {'type': 'image', 'url': 'https://test.domain/tracker_1.gif'}, + {'type': 'image', 'url': 'https://test.domain/tracker_2.gif'}, + {'type': 'image', 'url': 'https://test.domain/tracker_3.gif'} + ], + 'winNotification': [ + { + 'method': 'POST', + 'path': '/hb/bid_won?test=1', + 'data': { + 'ad': [ + {'dsp': 8, 'id': 800008, 'cost': 1.0e-5, 'nurl': 'https://test.domain/'} + ], + 'unit_id': 1234, + 'site_id': 123 + } + } + ], + 'cpm': 0.01, + 'currency': 'USD', + 'netRevenue': true + } + }, + headers: {} + }; + + it('result is correct', function () { + const result = spec.interpretResponse(bidResponse, bidRequest); + expect(result[0].requestId).to.equal('2bdcb0b203c17d'); + expect(result[0].cpm).to.equal(0.01); + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal('8:123456'); + expect(result[0].currency).to.equal('USD'); + expect(result[0].ttl).to.equal(60); + expect(result[0].meta.advertiserDomains).to.deep.equal(['test.domain']); + expect(result[0].winNotification[0]).to.deep.equal({'method': 'POST', 'path': '/hb/bid_won?test=1', 'data': {'ad': [{'dsp': 8, 'id': 800008, 'cost': 1.0e-5, 'nurl': 'https://test.domain/'}], 'unit_id': 1234, 'site_id': 123}}); + }); + }); + + describe('adResponse', function () { + const bid = { + 'unitId': 13144370, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '2bdcb0b203c17d', + 'referer': 'https://test.domain/index.html' + }; + const ad = { + 'ad': '
ad
', + 'width': 300, + 'height': 250, + 'creativeId': '8:123456', + 'syncs': [], + 'winNotification': [], + 'cpm': 0.01, + 'currency': 'USD', + 'netRevenue': true, + 'adomain': [ + 'test.domain' + ], + }; + + it('fill ad for response', function () { + const result = spec.adResponse(bid, ad); + expect(result.requestId).to.equal('2bdcb0b203c17d'); + expect(result.cpm).to.equal(0.01); + expect(result.width).to.equal(300); + expect(result.height).to.equal(250); + expect(result.creativeId).to.equal('8:123456'); + expect(result.currency).to.equal('USD'); + expect(result.ttl).to.equal(60); + expect(result.meta.advertiserDomains).to.deep.equal(['test.domain']); + }); + }); + + describe('onBidWon', function () { + const bid = { + winNotification: [ + { + 'method': 'POST', + 'path': '/hb/bid_won?test=1', + 'data': { + 'ad': [ + {'dsp': 8, 'id': 800008, 'cost': 0.01, 'nurl': 'http://test.domain/'} + ], + 'unit_id': 1234, + 'site_id': 123 + } + } + ] + }; + + let ajaxStub; + + beforeEach(() => { + ajaxStub = sinon.stub(spec, 'postRequest') + }) + + afterEach(() => { + ajaxStub.restore() + }) + + it('calls mediaimpact callback endpoint', () => { + const result = spec.onBidWon(bid); + expect(result).to.equal(true); + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(ENDPOINT_PROTOCOL + '://' + ENDPOINT_DOMAIN + '/hb/bid_won?test=1'); + expect(ajaxStub.firstCall.args[1]).to.deep.equal(JSON.stringify(bid.winNotification[0].data)); + }); + }); + + describe('getUserSyncs', function () { + const bidResponse = [{ + body: { + 'div-gpt-ad-1460505748561-0': + { + 'ad': '
ad
', + 'width': 300, + 'height': 250, + 'creativeId': '8:123456', + 'adomain': [ + 'test.domain' + ], + 'syncs': [ + {'type': 'image', 'link': 'https://test.domain/tracker_1.gif'}, + {'type': 'image', 'link': 'https://test.domain/tracker_2.gif'}, + {'type': 'image', 'link': 'https://test.domain/tracker_3.gif'} + ], + 'winNotification': [ + { + 'method': 'POST', + 'path': '/hb/bid_won?test=1', + 'data': { + 'ad': [ + {'dsp': 8, 'id': 800008, 'cost': 1.0e-5, 'nurl': 'https://test.domain/'} + ], + 'unit_id': 1234, + 'site_id': 123 + } + } + ], + 'cpm': 0.01, + 'currency': 'USD', + 'netRevenue': true + } + }, + headers: {} + }]; + + it('should return nothing when sync is disabled', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': false + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs).to.deep.equal([]); + }); + + it('should register image sync when only image is enabled where gdprConsent is undefined', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': true + }; + + const gdprConsent = undefined; + let syncs = spec.getUserSyncs(syncOptions, bidResponse, gdprConsent); + expect(syncs.length).to.equal(3); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://test.domain/tracker_1.gif'); + }); + + it('should register image sync when only image is enabled where gdprConsent is defined', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': true + }; + const gdprConsent = { + consentString: 'someString', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + + let syncs = spec.getUserSyncs(syncOptions, bidResponse, gdprConsent); + expect(syncs.length).to.equal(3); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://test.domain/tracker_1.gif?gdpr=1&gdpr_consent=someString'); + }); + }); +}); From cecb514da3e9e36d62f1d9bcd4ed959f4d3d0e41 Mon Sep 17 00:00:00 2001 From: koshe Date: Mon, 25 Mar 2024 13:08:31 +0100 Subject: [PATCH 3/5] Add custom sizes --- modules/mediaimpactBidAdapter.js | 9 +++++++-- modules/mediaimpactBidAdapter.md | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/mediaimpactBidAdapter.js b/modules/mediaimpactBidAdapter.js index 9a86632f052..4ce11201507 100644 --- a/modules/mediaimpactBidAdapter.js +++ b/modules/mediaimpactBidAdapter.js @@ -27,9 +27,14 @@ export const spec = { }; validBidRequests.forEach(function(validBidRequest) { + let sizes = validBidRequest.sizes; + if (typeof validBidRequest.params.sizes !== 'undefined') { + sizes = validBidRequest.params.sizes; + } + let bidRequestObject = { adUnitCode: validBidRequest.adUnitCode, - sizes: validBidRequest.sizes, + sizes: sizes, bidId: validBidRequest.bidId, referer: referer }; @@ -47,7 +52,7 @@ export const spec = { bidRequests.push(bidRequestObject); - beaconParams.sizes.push(spec.joinSizesToString(validBidRequest.sizes)); + beaconParams.sizes.push(spec.joinSizesToString(sizes)); beaconParams.referer = encodeURIComponent(referer); }); diff --git a/modules/mediaimpactBidAdapter.md b/modules/mediaimpactBidAdapter.md index 2fc6697fffb..fb1a3ea27b2 100644 --- a/modules/mediaimpactBidAdapter.md +++ b/modules/mediaimpactBidAdapter.md @@ -35,7 +35,8 @@ About us : https://mediaimpact.com.ua { bidder: "mediaimpact", params: { - partnerId: 6698 + partnerId: 6698, + sizes: [[300, 600]], } } ] From e1352645eed69d0654fee278f56a433c3176c3a9 Mon Sep 17 00:00:00 2001 From: koshe Date: Fri, 31 May 2024 11:58:48 +0200 Subject: [PATCH 4/5] Refactor response meta --- modules/mediaimpactBidAdapter.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/mediaimpactBidAdapter.js b/modules/mediaimpactBidAdapter.js index 4ce11201507..d62cb789ea4 100644 --- a/modules/mediaimpactBidAdapter.js +++ b/modules/mediaimpactBidAdapter.js @@ -115,12 +115,12 @@ export const spec = { creativeId: ad.creativeId, netRevenue: ad.netRevenue, currency: ad.currency, - winNotification: ad.winNotification - } + winNotification: ad.winNotification, + meta: {} + }; - bidObject.meta = {}; - if (ad.adomain && ad.adomain.length > 0) { - bidObject.meta.advertiserDomains = ad.adomain; + if (ad.meta) { + bidObject.meta = ad.meta; } return bidObject; From fc9915a8c2f8c23506732f558d45762b4c7824af Mon Sep 17 00:00:00 2001 From: koshe Date: Fri, 31 May 2024 13:34:20 +0200 Subject: [PATCH 5/5] mediaimpact adapter fix tests --- .../spec/modules/mediaimpactBidAdapter_spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/spec/modules/mediaimpactBidAdapter_spec.js b/test/spec/modules/mediaimpactBidAdapter_spec.js index 3d706e59c3f..806f0adeabe 100644 --- a/test/spec/modules/mediaimpactBidAdapter_spec.js +++ b/test/spec/modules/mediaimpactBidAdapter_spec.js @@ -139,9 +139,9 @@ describe('MediaimpactAdapter', function () { 'width': 300, 'height': 250, 'creativeId': '8:123456', - 'adomain': [ - 'test.domain' - ], + 'meta': { + 'advertiserDomains': ['test.domain'] + }, 'syncs': [ {'type': 'image', 'url': 'https://test.domain/tracker_1.gif'}, {'type': 'image', 'url': 'https://test.domain/tracker_2.gif'}, @@ -200,9 +200,9 @@ describe('MediaimpactAdapter', function () { 'cpm': 0.01, 'currency': 'USD', 'netRevenue': true, - 'adomain': [ - 'test.domain' - ], + 'meta': { + 'advertiserDomains': ['test.domain'] + }, }; it('fill ad for response', function () { @@ -263,9 +263,9 @@ describe('MediaimpactAdapter', function () { 'width': 300, 'height': 250, 'creativeId': '8:123456', - 'adomain': [ - 'test.domain' - ], + 'meta': { + 'advertiserDomains': ['test.domain'] + }, 'syncs': [ {'type': 'image', 'link': 'https://test.domain/tracker_1.gif'}, {'type': 'image', 'link': 'https://test.domain/tracker_2.gif'},