From 6813398e3f3141caefdda1c499e6a711acec5b9d Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Thu, 2 May 2019 14:45:59 +0200 Subject: [PATCH 01/43] added file scaffold --- modules/feedadBidAdapter.js | 28 ++++++++++++++++++++++++++++ modules/feedadBidAdapter.md | 1 + 2 files changed, 29 insertions(+) create mode 100644 modules/feedadBidAdapter.js create mode 100644 modules/feedadBidAdapter.md diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js new file mode 100644 index 00000000000..b6bd0a107b3 --- /dev/null +++ b/modules/feedadBidAdapter.js @@ -0,0 +1,28 @@ +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +import {config} from 'src/config'; + +/** + * Bidder network identity code + * @type {string} + */ +const BIDDER_CODE = 'feedad'; + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid: function (bid) { + }, + buildRequests: function (validBidRequests, bidderRequest) { + }, + interpretResponse: function (serverResponse, request) { + }, + getUserSyncs: function (syncOptions, serverResponses) { + }, + onTimeout: function (timeoutData) { + }, + onBidWon: function (bid) { + }, + onSetTargeting: function (bid) { + } +}; +registerBidder(spec); diff --git a/modules/feedadBidAdapter.md b/modules/feedadBidAdapter.md new file mode 100644 index 00000000000..464090415c4 --- /dev/null +++ b/modules/feedadBidAdapter.md @@ -0,0 +1 @@ +# TODO From 030a70979df5b255653b67ef87b101c99eb0bc43 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Tue, 7 May 2019 16:42:46 +0200 Subject: [PATCH 02/43] added isBidRequestValid implementation --- modules/feedadBidAdapter.js | 79 +++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index b6bd0a107b3..22aa859f784 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -1,6 +1,6 @@ import * as utils from 'src/utils'; import {registerBidder} from 'src/adapters/bidderFactory'; -import {config} from 'src/config'; +import {BANNER, VIDEO} from '../src/mediaTypes'; /** * Bidder network identity code @@ -8,11 +8,84 @@ import {config} from 'src/config'; */ const BIDDER_CODE = 'feedad'; +/** + * The media types supported by FeedAd + * @type {MediaType[]} + */ +const MEDIA_TYPES = [VIDEO, BANNER]; + +/** + * Tag for logging + * @type {string} + */ +const TAG = '[FeedAd]'; + +/** + * Checks if the bid is compatible with FeedAd. + * + * @param {BidRequest} bid - the bid to check + * @return {boolean} true if the bid is valid + */ +function isBidRequestValid(bid) { + const clientToken = utils.deepAccess(bid, 'params.clientToken'); + if (!clientToken || !isValidClientToken(clientToken)) { + utils.logWarn(TAG, "missing or invalid parameter 'clientToken'. found value:", clientToken); + return false; + } + + const placementId = utils.deepAccess(bid, 'params.placementId'); + if (!placementId || !isValidPlacementId(placementId)) { + utils.logWarn(TAG, "missing or invalid parameter 'placementId'. found value:", placementId); + return false; + } + + return true; +} + +/** + * Checks if a client token is valid + * @param {string} clientToken - the client token + * @return {boolean} true if the token is valid + */ +function isValidClientToken(clientToken) { + return typeof clientToken === 'string' && clientToken.length > 0; +} + +/** + * Checks if the placement id is a valid FeedAd placement ID + * + * @param {string} placementId - the placement id + * @return {boolean} if the id is valid + */ +function isValidPlacementId(placementId) { + return placementId.length > 0; // TODO: add placement ID regex or convert any string to valid ID? +} + +/** + * Checks if the given media types contain unsupported settings + * @param {MediaTypes} mediaTypes - the media types to check + * @return {string[]} the unsupported settings, empty when all types are supported + */ +function checkMediaTypes(mediaTypes) { + const errors = []; + if (mediaTypes.native) { + errors.push("'native' ads are not supported"); + } + if (mediaTypes.video && mediaTypes.video.any(video => video.context !== 'outstream')) { + errors.push("only 'outstream' video context's are supported"); + } + return errors; +} + +/** + * @type {BidderSpec} + */ export const spec = { code: BIDDER_CODE, - isBidRequestValid: function (bid) { - }, + supportedMediaTypes: MEDIA_TYPES, + isBidRequestValid, buildRequests: function (validBidRequests, bidderRequest) { + utils.logMessage('buildRequests', JSON.stringify(validBidRequests), JSON.stringify(bidderRequest)); }, interpretResponse: function (serverResponse, request) { }, From 1ac46eb9bab0a22d8940df063a5b7d94b24e2dae Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Wed, 8 May 2019 14:01:15 +0200 Subject: [PATCH 03/43] added local prototype of ad integration --- modules/feedadBidAdapter.js | 98 +++++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 22aa859f784..50ae69af9bf 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -64,17 +64,89 @@ function isValidPlacementId(placementId) { /** * Checks if the given media types contain unsupported settings * @param {MediaTypes} mediaTypes - the media types to check - * @return {string[]} the unsupported settings, empty when all types are supported + * @return {MediaTypes} the unsupported settings, empty when all types are supported */ -function checkMediaTypes(mediaTypes) { - const errors = []; - if (mediaTypes.native) { - errors.push("'native' ads are not supported"); - } - if (mediaTypes.video && mediaTypes.video.any(video => video.context !== 'outstream')) { - errors.push("only 'outstream' video context's are supported"); +function filterSupportedMediaTypes(mediaTypes) { + return { + banner: mediaTypes.banner, + video: mediaTypes.video && mediaTypes.video.filter(video => video.context !== 'outstream'), + native: [] + }; +} + +/** + * Checks if the given media types are empty + * @param {MediaTypes} mediaTypes - the types to check + * @return {boolean} true if the types are empty + */ +function isMediaTypesEmpty(mediaTypes) { + return Object.keys(mediaTypes).every(type => mediaTypes[type].length === 0); +} + +/** + * Builds the bid request to the FeedAd Server + * @param {BidRequest[]} validBidRequests - all validated bid requests + * @param {object} bidderRequest - meta information + * @return {ServerRequest|ServerRequest[]} + */ +function buildRequests(validBidRequests, bidderRequest) { + let acceptableRequests = validBidRequests.filter(request => !isMediaTypesEmpty(filterSupportedMediaTypes(request.mediaTypes))); + return { + method: 'POST', + url: 'http://localhost:3000/bidRequests', + data: { + requests: acceptableRequests + }, + options: { + contentType: 'application/json' + } } - return errors; +} + +/** + * Adapts the FeedAd server response to Prebid format + * @param {ServerResponse} serverResponse - the FeedAd server response + * @param {BidRequest} request - the initial bid request + * @returns {Bid[]} the FeedAd bids + */ +function interpretResponse(serverResponse, request) { + const body = typeof serverResponse.body === "string" ? JSON.parse(serverResponse.body) : serverResponse.body; + return body.requests.map((req, idx) => ({ + requestId: req.bidId, + cpm: 0.5, + width: req.sizes[0][0], + height: req.sizes[0][1], + ad: createAdHTML(req), + ttl: 60, + creativeId: `feedad-${body.id}-${idx}`, + netRevenue: true, + currency: "EUR" + })); +} + +/** + * Creates the HTML content for a FeedAd creative + * @param {object} req - the server response body + * @return {string} the HTML string + */ +function createAdHTML(req) { + return ` + + +`; } /** @@ -84,11 +156,8 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: MEDIA_TYPES, isBidRequestValid, - buildRequests: function (validBidRequests, bidderRequest) { - utils.logMessage('buildRequests', JSON.stringify(validBidRequests), JSON.stringify(bidderRequest)); - }, - interpretResponse: function (serverResponse, request) { - }, + buildRequests, + interpretResponse, getUserSyncs: function (syncOptions, serverResponses) { }, onTimeout: function (timeoutData) { @@ -98,4 +167,5 @@ export const spec = { onSetTargeting: function (bid) { } }; + registerBidder(spec); From fdcee20de4ab760ac47606343f617214b8781924 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Mon, 20 May 2019 09:59:27 +0200 Subject: [PATCH 04/43] added implementation for placement ID validation --- modules/feedadBidAdapter.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 50ae69af9bf..07c601d17ae 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -20,6 +20,12 @@ const MEDIA_TYPES = [VIDEO, BANNER]; */ const TAG = '[FeedAd]'; +/** + * Pattern for valid placement IDs + * @type {RegExp} + */ +const PLACEMENT_ID_PATTERN = /^(([a-z0-9])+[-_]?)+$/; + /** * Checks if the bid is compatible with FeedAd. * @@ -52,13 +58,20 @@ function isValidClientToken(clientToken) { } /** - * Checks if the placement id is a valid FeedAd placement ID + * Checks if the given placement id is of a correct format. + * Valid IDs are words of lowercase letters from a to z and numbers from 0 to 9. + * The words can be separated by hyphens or underscores. + * Multiple separators must not follow each other. + * The whole placement ID must not be larger than 256 characters. * - * @param {string} placementId - the placement id - * @return {boolean} if the id is valid + * @param placementId - the placement id to verify + * @returns if the placement ID is valid. */ -function isValidPlacementId(placementId) { - return placementId.length > 0; // TODO: add placement ID regex or convert any string to valid ID? +export function isValidPlacementId(placementId) { + return typeof placementId === "string" + && placementId.length > 0 + && placementId.length <= 256 + && PLACEMENT_ID_PATTERN.test(placementId); } /** From 19380945d189139789b43de628581d0176ea389d Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Mon, 20 May 2019 10:32:42 +0200 Subject: [PATCH 05/43] fixed video context filter --- modules/feedadBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 07c601d17ae..c6b705c4e6e 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -82,7 +82,7 @@ export function isValidPlacementId(placementId) { function filterSupportedMediaTypes(mediaTypes) { return { banner: mediaTypes.banner, - video: mediaTypes.video && mediaTypes.video.filter(video => video.context !== 'outstream'), + video: mediaTypes.video && mediaTypes.video.filter(video => video.context === 'outstream'), native: [] }; } From 478b00d4195872cd6da1631f0ca2dc0ce83ffc22 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Mon, 20 May 2019 15:18:01 +0200 Subject: [PATCH 06/43] applied lint to feedad bid adapter --- modules/feedadBidAdapter.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index c6b705c4e6e..ade684cdcec 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -67,11 +67,11 @@ function isValidClientToken(clientToken) { * @param placementId - the placement id to verify * @returns if the placement ID is valid. */ -export function isValidPlacementId(placementId) { - return typeof placementId === "string" - && placementId.length > 0 - && placementId.length <= 256 - && PLACEMENT_ID_PATTERN.test(placementId); +function isValidPlacementId(placementId) { + return typeof placementId === 'string' && + placementId.length > 0 && + placementId.length <= 256 && + PLACEMENT_ID_PATTERN.test(placementId); } /** @@ -123,7 +123,7 @@ function buildRequests(validBidRequests, bidderRequest) { * @returns {Bid[]} the FeedAd bids */ function interpretResponse(serverResponse, request) { - const body = typeof serverResponse.body === "string" ? JSON.parse(serverResponse.body) : serverResponse.body; + const body = typeof serverResponse.body === 'string' ? JSON.parse(serverResponse.body) : serverResponse.body; return body.requests.map((req, idx) => ({ requestId: req.bidId, cpm: 0.5, @@ -133,7 +133,7 @@ function interpretResponse(serverResponse, request) { ttl: 60, creativeId: `feedad-${body.id}-${idx}`, netRevenue: true, - currency: "EUR" + currency: 'EUR' })); } @@ -151,11 +151,11 @@ function createAdHTML(req) { placementId: '${req.params.placementId}', adOptions: {scaleMode: "parent_width"}, beforeAttach: (el, wrp) => { - wrp = document.createElement("div"); - wrp.style.width = '${req.sizes[0][0]}px'; - wrp.style.height = '${req.sizes[0][1]}px'; - el.appendChild(wrp); - return wrp; + wrp = document.createElement("div"); + wrp.style.width = '${req.sizes[0][0]}px'; + wrp.style.height = '${req.sizes[0][1]}px'; + el.appendChild(wrp); + return wrp; } }); @@ -174,10 +174,13 @@ export const spec = { getUserSyncs: function (syncOptions, serverResponses) { }, onTimeout: function (timeoutData) { + console.log('onTimeout', timeoutData); }, onBidWon: function (bid) { + console.log('onBidWon', bid); }, onSetTargeting: function (bid) { + console.log('onSetTargeting', bid); } }; From 494258e3f1a579eaabccd3937095d183f3b7fbe7 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Mon, 20 May 2019 15:18:37 +0200 Subject: [PATCH 07/43] added unit test for bid request validation --- test/spec/modules/feedadBidAdapter_spec.js | 108 +++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 test/spec/modules/feedadBidAdapter_spec.js diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js new file mode 100644 index 00000000000..f6573b44dd1 --- /dev/null +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -0,0 +1,108 @@ +import {expect} from 'chai'; +import {spec} from 'modules/feedadBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; +import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes'; + +const CODE = 'feedad'; + +describe('FeedAdAdapter', function () { + const adapter = newBidder(spec); + + describe('Public API', function () { + it('should have the FeedAd bidder code', function () { + expect(spec.code).to.equal(CODE); + }); + it('should only support video and banner ads', function () { + expect(spec.supportedMediaTypes).to.be.a('array'); + expect(spec.supportedMediaTypes).to.include(BANNER); + expect(spec.supportedMediaTypes).to.include(VIDEO); + expect(spec.supportedMediaTypes).not.to.include(NATIVE); + }); + it('should include the BidderSpec functions', function () { + expect(spec.isBidRequestValid).to.be.a('function'); + expect(spec.buildRequests).to.be.a('function'); + expect(spec.interpretResponse).to.be.a('function'); + expect(spec.getUserSyncs).to.be.a('function'); + expect(spec.onTimeout).to.be.a('function'); + expect(spec.onBidWon).to.be.a('function'); + expect(spec.onSetTargeting).to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should detect missing params', function () { + let result = spec.isBidRequestValid({ + bidder: "feedad", + sizes: [] + }); + expect(result).to.equal(false); + }); + it('should detect missing client token', function () { + let result = spec.isBidRequestValid({ + bidder: "feedad", + sizes: [], + params: {placementId: "placement"} + }); + expect(result).to.equal(false); + }); + it('should detect zero length client token', function () { + let result = spec.isBidRequestValid({ + bidder: "feedad", + sizes: [], + params: {clientToken: "", placementId: "placement"} + }); + expect(result).to.equal(false); + }); + it('should detect missing placement id', function () { + let result = spec.isBidRequestValid({ + bidder: "feedad", + sizes: [], + params: {clientToken: "clientToken"} + }); + expect(result).to.equal(false); + }); + it('should detect zero length placement id', function () { + let result = spec.isBidRequestValid({ + bidder: "feedad", + sizes: [], + params: {clientToken: "clientToken", placementId: ""} + }); + expect(result).to.equal(false); + }); + it('should detect too long placement id', function () { + var placementId = ""; + for (var i = 0; i < 300; i++) { + placementId += "a"; + } + let result = spec.isBidRequestValid({ + bidder: "feedad", + sizes: [], + params: {clientToken: "clientToken", placementId} + }); + expect(result).to.equal(false); + }); + it('should detect invalid placement id', function () { + [ + "placement id with spaces", + "some|id", + "PLACEMENTID", + "placeme:ntId" + ].forEach(id => { + let result = spec.isBidRequestValid({ + bidder: "feedad", + sizes: [], + params: {clientToken: "clientToken", placementId: id} + }); + expect(result).to.equal(false); + }); + }); + it('should accept valid parameters', function () { + let result = spec.isBidRequestValid({ + bidder: "feedad", + sizes: [], + params: {clientToken: "clientToken", placementId: "placement-id"} + }); + expect(result).to.equal(true); + }); + }); +}); From c7111e8b497ca695a9488b1509ebadbc781ac984 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Mon, 20 May 2019 17:00:51 +0200 Subject: [PATCH 08/43] added buildRequest unit test --- modules/feedadBidAdapter.js | 6 +- test/spec/modules/feedadBidAdapter_spec.js | 157 ++++++++++++++++++--- 2 files changed, 139 insertions(+), 24 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index ade684cdcec..44887f5bdcd 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -82,8 +82,8 @@ function isValidPlacementId(placementId) { function filterSupportedMediaTypes(mediaTypes) { return { banner: mediaTypes.banner, - video: mediaTypes.video && mediaTypes.video.filter(video => video.context === 'outstream'), - native: [] + video: mediaTypes.video && mediaTypes.video.context === 'outstream' ? mediaTypes.video : undefined, + native: undefined }; } @@ -93,7 +93,7 @@ function filterSupportedMediaTypes(mediaTypes) { * @return {boolean} true if the types are empty */ function isMediaTypesEmpty(mediaTypes) { - return Object.keys(mediaTypes).every(type => mediaTypes[type].length === 0); + return Object.keys(mediaTypes).every(type => mediaTypes[type] === undefined); } /** diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index f6573b44dd1..4b92bd1276b 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -32,77 +32,192 @@ describe('FeedAdAdapter', function () { describe('isBidRequestValid', function () { it('should detect missing params', function () { let result = spec.isBidRequestValid({ - bidder: "feedad", + bidder: 'feedad', sizes: [] }); expect(result).to.equal(false); }); it('should detect missing client token', function () { let result = spec.isBidRequestValid({ - bidder: "feedad", + bidder: 'feedad', sizes: [], - params: {placementId: "placement"} + params: {placementId: 'placement'} }); expect(result).to.equal(false); }); it('should detect zero length client token', function () { let result = spec.isBidRequestValid({ - bidder: "feedad", + bidder: 'feedad', sizes: [], - params: {clientToken: "", placementId: "placement"} + params: {clientToken: '', placementId: 'placement'} }); expect(result).to.equal(false); }); it('should detect missing placement id', function () { let result = spec.isBidRequestValid({ - bidder: "feedad", + bidder: 'feedad', sizes: [], - params: {clientToken: "clientToken"} + params: {clientToken: 'clientToken'} }); expect(result).to.equal(false); }); it('should detect zero length placement id', function () { let result = spec.isBidRequestValid({ - bidder: "feedad", + bidder: 'feedad', sizes: [], - params: {clientToken: "clientToken", placementId: ""} + params: {clientToken: 'clientToken', placementId: ''} }); expect(result).to.equal(false); }); it('should detect too long placement id', function () { - var placementId = ""; + var placementId = ''; for (var i = 0; i < 300; i++) { - placementId += "a"; + placementId += 'a'; } let result = spec.isBidRequestValid({ - bidder: "feedad", + bidder: 'feedad', sizes: [], - params: {clientToken: "clientToken", placementId} + params: {clientToken: 'clientToken', placementId} }); expect(result).to.equal(false); }); it('should detect invalid placement id', function () { [ - "placement id with spaces", - "some|id", - "PLACEMENTID", - "placeme:ntId" + 'placement id with spaces', + 'some|id', + 'PLACEMENTID', + 'placeme:ntId' ].forEach(id => { let result = spec.isBidRequestValid({ - bidder: "feedad", + bidder: 'feedad', sizes: [], - params: {clientToken: "clientToken", placementId: id} + params: {clientToken: 'clientToken', placementId: id} }); expect(result).to.equal(false); }); }); it('should accept valid parameters', function () { let result = spec.isBidRequestValid({ - bidder: "feedad", + bidder: 'feedad', sizes: [], - params: {clientToken: "clientToken", placementId: "placement-id"} + params: {clientToken: 'clientToken', placementId: 'placement-id'} }); expect(result).to.equal(true); }); }); + + describe('buildRequests', function () { + it('should accept empty lists', function () { + let result = spec.buildRequests([]); + expect(result.data.requests).to.be.empty; + }); + it('should filter native media types', function () { + let bid = { + code: 'feedad', + mediaTypes: { + native: { + sizes: [[300, 250], [300, 600]], + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid]); + expect(result.data.requests).to.be.empty; + }); + it('should filter video media types without outstream context', function () { + let bid = { + code: 'feedad', + mediaTypes: { + video: { + context: 'instream' + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid]); + expect(result.data.requests).to.be.empty; + }); + it('should pass through outstream video media', function () { + let bid = { + code: 'feedad', + mediaTypes: { + video: { + context: 'outstream' + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid]); + expect(result.data.requests).to.be.lengthOf(1); + expect(result.data.requests[0]).to.deep.equal(bid); + }); + it('should pass through banner media', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid]); + expect(result.data.requests).to.be.lengthOf(1); + expect(result.data.requests[0]).to.deep.equal(bid); + }); + it('should detect empty media types', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: undefined, + video: undefined, + native: undefined + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid]); + expect(result.data.requests).to.be.empty; + }); + it('should use POST', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid]); + expect(result.method).to.equal('POST'); + }); + it('should use the correct URL', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid]); + expect(result.url).to.equal('http://localhost:3000/bidRequests'); + }); + it('should specify the content type explicitly', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid]); + expect(result.options).to.deep.equal({ + contentType: 'application/json' + }) + }); + }); }); From e57779ccc637345554bb49b0500082810fdc96ef Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Wed, 22 May 2019 09:42:17 +0200 Subject: [PATCH 09/43] added unit tests for timeout and bid won callbacks --- modules/feedadBidAdapter.js | 25 +++++++++-- test/spec/modules/feedadBidAdapter_spec.js | 50 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 44887f5bdcd..edce1e022a4 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -1,6 +1,7 @@ import * as utils from 'src/utils'; import {registerBidder} from 'src/adapters/bidderFactory'; import {BANNER, VIDEO} from '../src/mediaTypes'; +import {ajax} from '../src/ajax'; /** * Bidder network identity code @@ -173,11 +174,27 @@ export const spec = { interpretResponse, getUserSyncs: function (syncOptions, serverResponses) { }, - onTimeout: function (timeoutData) { - console.log('onTimeout', timeoutData); + onTimeout: function (timeoutData, xhr) { + if (!timeoutData) { + return; + } + xhr = typeof xhr === 'function' ? xhr : ajax; + xhr('http://localhost:3000/onTimeout', null, JSON.stringify(timeoutData), { + withCredentials: true, + method: 'POST', + contentType: 'application/json' + }) }, - onBidWon: function (bid) { - console.log('onBidWon', bid); + onBidWon: function (bid, xhr) { + if (!bid) { + return; + } + xhr = typeof xhr === "function" ? xhr : ajax; + xhr('http://localhost:3000/onBidWon', null, JSON.stringify(bid), { + withCredentials: true, + method: 'POST', + contentType: 'application/json' + }) }, onSetTargeting: function (bid) { console.log('onSetTargeting', bid); diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 4b92bd1276b..64c0804ed97 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -2,6 +2,8 @@ import {expect} from 'chai'; import {spec} from 'modules/feedadBidAdapter'; import {newBidder} from 'src/adapters/bidderFactory'; import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes'; +import * as xhr from '../../../src/ajax'; +import * as sinon from 'sinon'; const CODE = 'feedad'; @@ -220,4 +222,52 @@ describe('FeedAdAdapter', function () { }) }); }); + + describe('onTimeout', function () { + let xhr; + + beforeEach(function () { + xhr = sinon.stub(); + }); + + it('should send parameters to backend', function () { + let params = {some: 'parameters'}; + spec.onTimeout(params, xhr); + expect(xhr.calledOnceWith('http://localhost:3000/onTimeout', null, JSON.stringify(params), { + withCredentials: true, + method: 'POST', + contentType: 'application/json' + })).to.be.true; + }); + + it('should do nothing on empty data', function () { + spec.onTimeout(undefined, xhr); + spec.onTimeout(null, xhr); + expect(xhr.called).to.be.false; + }); + }); + + describe('onBidWon', function () { + let xhr; + + beforeEach(function () { + xhr = sinon.stub(); + }); + + it('should send parameters to backend', function () { + let params = {some: 'parameters'}; + spec.onBidWon(params, xhr); + expect(xhr.calledOnceWith('http://localhost:3000/onBidWon', null, JSON.stringify(params), { + withCredentials: true, + method: 'POST', + contentType: 'application/json' + })).to.be.true; + }); + + it('should do nothing on empty data', function () { + spec.onBidWon(undefined, xhr); + spec.onBidWon(null, xhr); + expect(xhr.called).to.be.false; + }); + }); }); From 0af865ca0c37f8e5f0255169d5245a2a784e34c9 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Mon, 3 Jun 2019 09:31:38 +0200 Subject: [PATCH 10/43] updated bid request to FeedAd API --- modules/feedadBidAdapter.js | 59 +++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index edce1e022a4..465376692c7 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -3,6 +3,30 @@ import {registerBidder} from 'src/adapters/bidderFactory'; import {BANNER, VIDEO} from '../src/mediaTypes'; import {ajax} from '../src/ajax'; +/** + * Version of the FeedAd bid adapter + * @type {string} + */ +const VERSION = "1.0.0"; + +/** + * @typedef {object} FeedAdApiBidRequest + * @inner + * + * @property {number} ad_type + * @property {string} client_token + * @property {string} placement_id + * @property {string} sdk_version + * @property {boolean} app_hybrid + * + * @property {string} [app_bundle_id] + * @property {string} [app_name] + * @property {object} [custom_params] + * @property {number} [connectivity] + * @property {string} [device_adid] + * @property {string} [device_platform] + */ + /** * Bidder network identity code * @type {string} @@ -27,6 +51,9 @@ const TAG = '[FeedAd]'; */ const PLACEMENT_ID_PATTERN = /^(([a-z0-9])+[-_]?)+$/; +const API_ENDPOINT = 'https://feedad-backend-dev.appspot.com'; +const API_PATH_BID_REQUEST = '/1/prebid/web/bids'; + /** * Checks if the bid is compatible with FeedAd. * @@ -97,6 +124,21 @@ function isMediaTypesEmpty(mediaTypes) { return Object.keys(mediaTypes).every(type => mediaTypes[type] === undefined); } +/** + * Creates the bid request params the api expects from the prebid bid request + * @param {BidRequest} request - the validated prebid bid request + * @return {FeedAdApiBidRequest} + */ +function createApiBidRParams(request) { + return { + ad_type: 0, + client_token: request.params.clientToken, + placement_id: request.params.placementId, + sdk_version: `prebid_${VERSION}`, + app_hybrid: false, + }; +} + /** * Builds the bid request to the FeedAd Server * @param {BidRequest[]} validBidRequests - all validated bid requests @@ -105,16 +147,23 @@ function isMediaTypesEmpty(mediaTypes) { */ function buildRequests(validBidRequests, bidderRequest) { let acceptableRequests = validBidRequests.filter(request => !isMediaTypesEmpty(filterSupportedMediaTypes(request.mediaTypes))); + if (acceptableRequests.length === 0) { + return []; + } + let data = Object.assign({}, bidderRequest, { + bids: acceptableRequests.map(req => { + req.params = createApiBidRParams(req); + return req; + }) + }); return { method: 'POST', - url: 'http://localhost:3000/bidRequests', - data: { - requests: acceptableRequests - }, + url: `${API_ENDPOINT}${API_PATH_BID_REQUEST}`, + data, options: { contentType: 'application/json' } - } + }; } /** From 4d218990dcfbc578b2c76530b455c0189eac1d7a Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Mon, 3 Jun 2019 10:30:02 +0200 Subject: [PATCH 11/43] added parsing of feedad api bid response --- modules/feedadBidAdapter.js | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 465376692c7..cd59d588836 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -27,6 +27,21 @@ const VERSION = "1.0.0"; * @property {string} [device_platform] */ +/** + * @typedef {object} FeedAdApiBidResponse + * @inner + * + * @property {string} ad - Ad HTML payload + * @property {number} cpm - number / float + * @property {string} creativeId - ID of creative for tracking + * @property {string} currency - 3-letter ISO 4217 currency-code + * @property {number} height - Height of creative returned in [].ad + * @property {boolean} netRevenue - Is the CPM net (true) or gross (false)? + * @property {string} requestId - bids[].bidId + * @property {number} ttl - Time to live for this ad + * @property {number} width - Width of creative returned in [].ad + */ + /** * Bidder network identity code * @type {string} @@ -173,17 +188,20 @@ function buildRequests(validBidRequests, bidderRequest) { * @returns {Bid[]} the FeedAd bids */ function interpretResponse(serverResponse, request) { + /** + * @type FeedAdApiBidResponse[] + */ const body = typeof serverResponse.body === 'string' ? JSON.parse(serverResponse.body) : serverResponse.body; - return body.requests.map((req, idx) => ({ - requestId: req.bidId, + return body.map((response, idx) => ({ + requestId: response.requestId, cpm: 0.5, - width: req.sizes[0][0], - height: req.sizes[0][1], - ad: createAdHTML(req), - ttl: 60, - creativeId: `feedad-${body.id}-${idx}`, - netRevenue: true, - currency: 'EUR' + width: response.width, + height: response.height, + ad: `${response.ad}`, + ttl: response.ttl, + creativeId: response.creativeId, + netRevenue: response.netRevenue, + currency: response.currency })); } From 318535843e3a815d921342c1591aefcb79565fe5 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Wed, 5 Jun 2019 19:32:40 +0200 Subject: [PATCH 12/43] added transmisison of tracking events to FeedAd Api --- modules/feedadBidAdapter.js | 89 ++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 15 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index cd59d588836..7b3f6796492 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -42,6 +42,25 @@ const VERSION = "1.0.0"; * @property {number} width - Width of creative returned in [].ad */ +/** + * @typedef {object} FeedAdApiTrackingParams + * @inner + * + * app_hybrid {boolean} + * client_token {string} + * klass {'prebid_bidWon'|'prebid_bidTimeout'} + * placement_id {string} + * prebid_auction_id {string} + * prebid_bid_id {string} + * prebid_transaction_id {string} + * referer {string} + * sdk_version {string} + * [app_bundle_id] {string} + * [app_name] {string} + * [device_adid] {string} + * [device_platform] {1|2|3} 1 - Android | 2 - iOS | 3 - Windows + */ + /** * Bidder network identity code * @type {string} @@ -68,6 +87,13 @@ const PLACEMENT_ID_PATTERN = /^(([a-z0-9])+[-_]?)+$/; const API_ENDPOINT = 'https://feedad-backend-dev.appspot.com'; const API_PATH_BID_REQUEST = '/1/prebid/web/bids'; +const API_PATH_TRACK_REQUEST = '/1/prebid/web/events'; + +/** + * Stores temporary auction metadata + * @type {Object.} + */ +const BID_METADATA = {}; /** * Checks if the bid is compatible with FeedAd. @@ -171,6 +197,10 @@ function buildRequests(validBidRequests, bidderRequest) { return req; }) }); + data.bids.forEach(bid => BID_METADATA[bid.bidId] = { + referer: data.refererInfo.referer, + transactionId: bid.transactionId + }); return { method: 'POST', url: `${API_ENDPOINT}${API_PATH_BID_REQUEST}`, @@ -230,6 +260,32 @@ function createAdHTML(req) { `; } +/** + * Creates the parameters for the FeedAd tracking call + * @param {object} data - prebid data + * @param {'prebid_bidWon'|'prebid_bidTimeout'} klass - type of tracking call + * @return {FeedAdApiTrackingParams|null} + */ +function createTrackingParams(data, klass) { + const bidId = data.bidId || data.requestId; + if (!BID_METADATA.hasOwnProperty(bidId)) { + return null; + } + const {referer, transactionId} = BID_METADATA[bidId]; + delete BID_METADATA[bidId]; + return { + app_hybrid: false, + client_token: data.params[0].clientToken, + placement_id: data.params[0].placementId, + klass, + prebid_auction_id: data.auctionId, + prebid_bid_id: bidId, + prebid_transaction_id: transactionId, + referer, + sdk_version: VERSION + }; +} + /** * @type {BidderSpec} */ @@ -245,26 +301,29 @@ export const spec = { if (!timeoutData) { return; } - xhr = typeof xhr === 'function' ? xhr : ajax; - xhr('http://localhost:3000/onTimeout', null, JSON.stringify(timeoutData), { - withCredentials: true, - method: 'POST', - contentType: 'application/json' - }) + let params = createTrackingParams(timeoutData, 'prebid_bidTimeout'); + if (params) { + xhr = typeof xhr === 'function' ? xhr : ajax; + xhr(`${API_ENDPOINT}${API_PATH_TRACK_REQUEST}`, null, JSON.stringify(params), { + withCredentials: true, + method: 'POST', + contentType: 'application/json' + }); + } }, onBidWon: function (bid, xhr) { if (!bid) { return; } - xhr = typeof xhr === "function" ? xhr : ajax; - xhr('http://localhost:3000/onBidWon', null, JSON.stringify(bid), { - withCredentials: true, - method: 'POST', - contentType: 'application/json' - }) - }, - onSetTargeting: function (bid) { - console.log('onSetTargeting', bid); + let params = createTrackingParams(bid, 'prebid_bidWon'); + if (params) { + xhr = typeof xhr === 'function' ? xhr : ajax; + xhr(`${API_ENDPOINT}${API_PATH_TRACK_REQUEST}`, null, JSON.stringify(params), { + withCredentials: true, + method: 'POST', + contentType: 'application/json' + }); + } } }; From d7673e4f47a234a1d5dd4efdb139d3bd4f874820 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 7 Jun 2019 10:16:20 +0200 Subject: [PATCH 13/43] code cleanup --- modules/feedadBidAdapter.js | 84 ++++++++++--------------------------- 1 file changed, 21 insertions(+), 63 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 7b3f6796492..e0bab956d02 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -221,43 +221,7 @@ function interpretResponse(serverResponse, request) { /** * @type FeedAdApiBidResponse[] */ - const body = typeof serverResponse.body === 'string' ? JSON.parse(serverResponse.body) : serverResponse.body; - return body.map((response, idx) => ({ - requestId: response.requestId, - cpm: 0.5, - width: response.width, - height: response.height, - ad: `${response.ad}`, - ttl: response.ttl, - creativeId: response.creativeId, - netRevenue: response.netRevenue, - currency: response.currency - })); -} - -/** - * Creates the HTML content for a FeedAd creative - * @param {object} req - the server response body - * @return {string} the HTML string - */ -function createAdHTML(req) { - return ` - - -`; + return typeof serverResponse.body === 'string' ? JSON.parse(serverResponse.body) : serverResponse.body; } /** @@ -287,35 +251,16 @@ function createTrackingParams(data, klass) { } /** - * @type {BidderSpec} + * Creates a tracking handler for the given event type + * @param klass - the event type + * @return {Function} the tracking handler function */ -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: MEDIA_TYPES, - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs: function (syncOptions, serverResponses) { - }, - onTimeout: function (timeoutData, xhr) { - if (!timeoutData) { - return; - } - let params = createTrackingParams(timeoutData, 'prebid_bidTimeout'); - if (params) { - xhr = typeof xhr === 'function' ? xhr : ajax; - xhr(`${API_ENDPOINT}${API_PATH_TRACK_REQUEST}`, null, JSON.stringify(params), { - withCredentials: true, - method: 'POST', - contentType: 'application/json' - }); - } - }, - onBidWon: function (bid, xhr) { - if (!bid) { +function trackingHandlerFactory(klass) { + return (data, xhr) => { + if (!data) { return; } - let params = createTrackingParams(bid, 'prebid_bidWon'); + let params = createTrackingParams(data, klass); if (params) { xhr = typeof xhr === 'function' ? xhr : ajax; xhr(`${API_ENDPOINT}${API_PATH_TRACK_REQUEST}`, null, JSON.stringify(params), { @@ -325,6 +270,19 @@ export const spec = { }); } } +} + +/** + * @type {BidderSpec} + */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: MEDIA_TYPES, + isBidRequestValid, + buildRequests, + interpretResponse, + onTimeout: trackingHandlerFactory('prebid_bidTimeout'), + onBidWon: trackingHandlerFactory('prebid_bidWon') }; registerBidder(spec); From 79a3cbc37e68060fd85bb1059d1a40d7bdfcd576 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 7 Jun 2019 10:58:11 +0200 Subject: [PATCH 14/43] updated feedad unit tests for buildRequest method --- modules/feedadBidAdapter.js | 5 +- test/spec/modules/feedadBidAdapter_spec.js | 77 +++++++++++++++------- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index e0bab956d02..6b094caedd7 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -7,7 +7,7 @@ import {ajax} from '../src/ajax'; * Version of the FeedAd bid adapter * @type {string} */ -const VERSION = "1.0.0"; +const VERSION = '1.0.0'; /** * @typedef {object} FeedAdApiBidRequest @@ -187,6 +187,9 @@ function createApiBidRParams(request) { * @return {ServerRequest|ServerRequest[]} */ function buildRequests(validBidRequests, bidderRequest) { + if (!bidderRequest) { + return []; + } let acceptableRequests = validBidRequests.filter(request => !isMediaTypesEmpty(filterSupportedMediaTypes(request.mediaTypes))); if (acceptableRequests.length === 0) { return []; diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 64c0804ed97..3a728accb36 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -1,15 +1,11 @@ import {expect} from 'chai'; import {spec} from 'modules/feedadBidAdapter'; -import {newBidder} from 'src/adapters/bidderFactory'; import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes'; -import * as xhr from '../../../src/ajax'; import * as sinon from 'sinon'; const CODE = 'feedad'; describe('FeedAdAdapter', function () { - const adapter = newBidder(spec); - describe('Public API', function () { it('should have the FeedAd bidder code', function () { expect(spec.code).to.equal(CODE); @@ -20,14 +16,12 @@ describe('FeedAdAdapter', function () { expect(spec.supportedMediaTypes).to.include(VIDEO); expect(spec.supportedMediaTypes).not.to.include(NATIVE); }); - it('should include the BidderSpec functions', function () { + it('should export the BidderSpec functions', function () { expect(spec.isBidRequestValid).to.be.a('function'); expect(spec.buildRequests).to.be.a('function'); expect(spec.interpretResponse).to.be.a('function'); - expect(spec.getUserSyncs).to.be.a('function'); expect(spec.onTimeout).to.be.a('function'); expect(spec.onBidWon).to.be.a('function'); - expect(spec.onSetTargeting).to.be.a('function'); }); }); @@ -109,9 +103,16 @@ describe('FeedAdAdapter', function () { }); describe('buildRequests', function () { + const bidderRequest = { + refererInfo: { + referer: 'the referer' + }, + some: 'thing' + }; + it('should accept empty lists', function () { - let result = spec.buildRequests([]); - expect(result.data.requests).to.be.empty; + let result = spec.buildRequests([], bidderRequest); + expect(result).to.be.empty; }); it('should filter native media types', function () { let bid = { @@ -123,8 +124,8 @@ describe('FeedAdAdapter', function () { }, params: {clientToken: 'clientToken', placementId: 'placement-id'} }; - let result = spec.buildRequests([bid]); - expect(result.data.requests).to.be.empty; + let result = spec.buildRequests([bid], bidderRequest); + expect(result).to.be.empty; }); it('should filter video media types without outstream context', function () { let bid = { @@ -136,8 +137,8 @@ describe('FeedAdAdapter', function () { }, params: {clientToken: 'clientToken', placementId: 'placement-id'} }; - let result = spec.buildRequests([bid]); - expect(result.data.requests).to.be.empty; + let result = spec.buildRequests([bid], bidderRequest); + expect(result).to.be.empty; }); it('should pass through outstream video media', function () { let bid = { @@ -149,9 +150,9 @@ describe('FeedAdAdapter', function () { }, params: {clientToken: 'clientToken', placementId: 'placement-id'} }; - let result = spec.buildRequests([bid]); - expect(result.data.requests).to.be.lengthOf(1); - expect(result.data.requests[0]).to.deep.equal(bid); + let result = spec.buildRequests([bid], bidderRequest); + expect(result.data.bids).to.be.lengthOf(1); + expect(result.data.bids[0]).to.deep.equal(bid); }); it('should pass through banner media', function () { let bid = { @@ -163,9 +164,9 @@ describe('FeedAdAdapter', function () { }, params: {clientToken: 'clientToken', placementId: 'placement-id'} }; - let result = spec.buildRequests([bid]); - expect(result.data.requests).to.be.lengthOf(1); - expect(result.data.requests[0]).to.deep.equal(bid); + let result = spec.buildRequests([bid], bidderRequest); + expect(result.data.bids).to.be.lengthOf(1); + expect(result.data.bids[0]).to.deep.equal(bid); }); it('should detect empty media types', function () { let bid = { @@ -177,8 +178,8 @@ describe('FeedAdAdapter', function () { }, params: {clientToken: 'clientToken', placementId: 'placement-id'} }; - let result = spec.buildRequests([bid]); - expect(result.data.requests).to.be.empty; + let result = spec.buildRequests([bid], bidderRequest); + expect(result).to.be.empty; }); it('should use POST', function () { let bid = { @@ -190,7 +191,7 @@ describe('FeedAdAdapter', function () { }, params: {clientToken: 'clientToken', placementId: 'placement-id'} }; - let result = spec.buildRequests([bid]); + let result = spec.buildRequests([bid], bidderRequest); expect(result.method).to.equal('POST'); }); it('should use the correct URL', function () { @@ -203,8 +204,8 @@ describe('FeedAdAdapter', function () { }, params: {clientToken: 'clientToken', placementId: 'placement-id'} }; - let result = spec.buildRequests([bid]); - expect(result.url).to.equal('http://localhost:3000/bidRequests'); + let result = spec.buildRequests([bid], bidderRequest); + expect(result.url).to.equal('https://feedad-backend-dev.appspot.com/1/prebid/web/bids'); }); it('should specify the content type explicitly', function () { let bid = { @@ -216,11 +217,37 @@ describe('FeedAdAdapter', function () { }, params: {clientToken: 'clientToken', placementId: 'placement-id'} }; - let result = spec.buildRequests([bid]); + let result = spec.buildRequests([bid], bidderRequest); expect(result.options).to.deep.equal({ contentType: 'application/json' }) }); + it('should include the bidder request', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid, bid, bid], bidderRequest); + expect(result.data).to.deep.include(bidderRequest); + }); + it('should detect missing bidder request parameter', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid, bid, bid]); + expect(result).to.be.empty; + }); }); describe('onTimeout', function () { From d446300392b3c9ec73d6f777242b14d96de8d265 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 7 Jun 2019 15:07:29 +0200 Subject: [PATCH 15/43] added unit tests for event tracking implementation --- modules/feedadBidAdapter.js | 26 +-- test/spec/modules/feedadBidAdapter_spec.js | 189 ++++++++++++++++----- 2 files changed, 162 insertions(+), 53 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 6b094caedd7..c55430b1a14 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -46,19 +46,19 @@ const VERSION = '1.0.0'; * @typedef {object} FeedAdApiTrackingParams * @inner * - * app_hybrid {boolean} - * client_token {string} - * klass {'prebid_bidWon'|'prebid_bidTimeout'} - * placement_id {string} - * prebid_auction_id {string} - * prebid_bid_id {string} - * prebid_transaction_id {string} - * referer {string} - * sdk_version {string} - * [app_bundle_id] {string} - * [app_name] {string} - * [device_adid] {string} - * [device_platform] {1|2|3} 1 - Android | 2 - iOS | 3 - Windows + * @property app_hybrid {boolean} + * @property client_token {string} + * @property klass {'prebid_bidWon'|'prebid_bidTimeout'} + * @property placement_id {string} + * @property prebid_auction_id {string} + * @property prebid_bid_id {string} + * @property prebid_transaction_id {string} + * @property referer {string} + * @property sdk_version {string} + * @property [app_bundle_id] {string} + * @property [app_name] {string} + * @property [device_adid] {string} + * @property [device_platform] {1|2|3} 1 - Android | 2 - iOS | 3 - Windows */ /** diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 3a728accb36..bc2f5db9141 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -250,51 +250,160 @@ describe('FeedAdAdapter', function () { }); }); - describe('onTimeout', function () { - let xhr; - - beforeEach(function () { - xhr = sinon.stub(); - }); - - it('should send parameters to backend', function () { - let params = {some: 'parameters'}; - spec.onTimeout(params, xhr); - expect(xhr.calledOnceWith('http://localhost:3000/onTimeout', null, JSON.stringify(params), { - withCredentials: true, - method: 'POST', - contentType: 'application/json' - })).to.be.true; - }); + describe('event tracking calls', function () { + const clientToken = 'clientToken'; + const placementId = 'placement id'; + const auctionId = 'the auction id'; + const bidId = 'the bid id'; + const transactionId = 'the transaction id'; + const referer = 'the referer'; + const bidderRequest = { + refererInfo: { + referer: referer + }, + some: 'thing' + }; + const bid = { + 'bidder': 'feedad', + 'params': { + 'clientToken': 'fupp', + 'placementId': 'prebid-test' + }, + 'crumbs': { + 'pubcid': '6254a85f-bded-489a-9736-83c45d45ef1d' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': transactionId, + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': bidId, + 'bidderRequestId': '10739fe6fe2127', + 'auctionId': '5ac67dff-d971-4b56-84a3-345a87a1f786', + 'src': 'client', + 'bidRequestsCount': 1 + }; + const timeoutData = { + 'bidId': bidId, + 'bidder': 'feedad', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'auctionId': auctionId, + 'params': [ + { + 'clientToken': clientToken, + 'placementId': placementId + } + ], + 'timeout': 3000 + }; + const bidWonData = { + 'bidderCode': 'feedad', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '3a4529aa05114d', + 'requestId': bidId, + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'ad': 'ad content', + 'ttl': 60, + 'creativeId': 'feedad-21-0', + 'netRevenue': true, + 'currency': 'EUR', + 'auctionId': auctionId, + 'responseTimestamp': 1558365914596, + 'requestTimestamp': 1558365914506, + 'bidder': 'feedad', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'timeToRespond': 90, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'feedad', + 'hb_adid': '3a4529aa05114d', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'status': 'rendered', + 'params': [ + { + 'clientToken': clientToken, + 'placementId': placementId + } + ] + }; + const cases = [ + ['onTimeout', timeoutData, 'prebid_bidTimeout'], + ['onBidWon', bidWonData, 'prebid_bidWon'], + ]; - it('should do nothing on empty data', function () { - spec.onTimeout(undefined, xhr); - spec.onTimeout(null, xhr); - expect(xhr.called).to.be.false; - }); - }); + cases.forEach(([name, data, eventKlass]) => { + let subject = spec[name]; + describe(name + ' handler', function () { + let xhr; - describe('onBidWon', function () { - let xhr; + beforeEach(function () { + xhr = sinon.stub(); + }); - beforeEach(function () { - xhr = sinon.stub(); - }); + it('should do nothing on empty data', function () { + subject(undefined, xhr); + subject(null, xhr); + expect(xhr.called).to.be.false; + }); - it('should send parameters to backend', function () { - let params = {some: 'parameters'}; - spec.onBidWon(params, xhr); - expect(xhr.calledOnceWith('http://localhost:3000/onBidWon', null, JSON.stringify(params), { - withCredentials: true, - method: 'POST', - contentType: 'application/json' - })).to.be.true; - }); + it('should do nothing when bid metadata is not set', function () { + subject(data, xhr); + expect(xhr.callCount).to.equal(0); + }); - it('should do nothing on empty data', function () { - spec.onBidWon(undefined, xhr); - spec.onBidWon(null, xhr); - expect(xhr.called).to.be.false; + it('should send tracking params when correct metadata was set', function () { + spec.buildRequests([bid], bidderRequest); + let expectedData = { + app_hybrid: false, + client_token: clientToken, + placement_id: placementId, + klass: eventKlass, + prebid_auction_id: auctionId, + prebid_bid_id: bidId, + prebid_transaction_id: transactionId, + referer, + sdk_version: '1.0.0' + }; + subject(data, xhr); + expect(xhr.callCount).to.equal(1); + let call = xhr.getCall(0); + expect(call.args[0]).to.equal('https://feedad-backend-dev.appspot.com/1/prebid/web/events'); + expect(call.args[1]).to.be.null; + expect(JSON.parse(call.args[2])).to.deep.equal(expectedData); + expect(call.args[3]).to.deep.equal({ + withCredentials: true, + method: 'POST', + contentType: 'application/json' + }); + }) + }); }); }); }); From 766f23c1c85da907d8b328ea4e473b492f02a011 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 7 Jun 2019 18:36:49 +0200 Subject: [PATCH 16/43] added unit test for interpretResponse method --- modules/feedadBidAdapter.js | 2 +- test/spec/modules/feedadBidAdapter_spec.js | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index c55430b1a14..7c26e581e4c 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -265,7 +265,7 @@ function trackingHandlerFactory(klass) { } let params = createTrackingParams(data, klass); if (params) { - xhr = typeof xhr === 'function' ? xhr : ajax; + xhr = typeof xhr === 'function' ? xhr : ajax; // required to test calls to the ajax method because it cannot be mocked xhr(`${API_ENDPOINT}${API_PATH_TRACK_REQUEST}`, null, JSON.stringify(params), { withCredentials: true, method: 'POST', diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index bc2f5db9141..5bf64683380 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -250,6 +250,27 @@ describe('FeedAdAdapter', function () { }); }); + describe('interpretResponse', function () { + const body = [{ + foo: "bar", + sub: { + obj: 5 + } + }, { + bar: "foo" + }]; + + it('should convert string bodies to JSON', function () { + let result = spec.interpretResponse({body: JSON.stringify(body)}); + expect(result).to.deep.equal(body); + }); + + it('should pass through body objects', function () { + let result = spec.interpretResponse({body}); + expect(result).to.deep.equal(body); + }); + }); + describe('event tracking calls', function () { const clientToken = 'clientToken'; const placementId = 'placement id'; From 2d469c0bfba4ff11e1b5c1733a7ccadcf6551f7f Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 7 Jun 2019 18:50:04 +0200 Subject: [PATCH 17/43] added adapter documentation --- modules/feedadBidAdapter.md | 45 ++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/modules/feedadBidAdapter.md b/modules/feedadBidAdapter.md index 464090415c4..fd57025c29e 100644 --- a/modules/feedadBidAdapter.md +++ b/modules/feedadBidAdapter.md @@ -1 +1,44 @@ -# TODO +# Overview + +``` +Module Name: FeedAd Adapter +Module Type: Bidder Adapter +Maintainer: mail@feedad.com +``` + +# Description + +Prebid.JS adapter that connects to the FeedAd demand sources. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { // supports all banner sizes + sizes: [[300, 250]], + }, + video: { // supports only outstream video + context: 'outstream' + } + }, + bids: [ + { + bidder: "feedad", + params: { + clientToken: 'your-client-token' // see below for more info + placementId: 'your-placement-id' // see below for more info + } + } + ] + } + ]; +``` + +# Required Parameters + +| Parameter | Description | +| --------- | ----------- | +| `clientToken` | Your FeedAd web client token. You can view your client token inside the FeedAd admin panel. | +| `placementId` | You can choose placement IDs yourself. A placement ID should be named after the ad position inside your product. For example, if you want to display an ad inside a list of news articles, you could name it "ad-news-overview".
A placement ID may consist of lowercase `a-z`, `0-9`, `-` and `_`. You do not have to manually create the placement IDs before using them. Just specify them within the code, and they will appear in the FeedAd admin panel after the first request.
[Learn more](/concept/feed_ad/index.html) about Placement IDs and how they are grouped to play the same Creative. | From 138d1ff1a728ed8221b259cb90edbaef1d9a2131 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 7 Jun 2019 19:06:04 +0200 Subject: [PATCH 18/43] added dedicated feedad example page --- integrationExamples/gpt/feedad_dfp.html | 96 +++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 integrationExamples/gpt/feedad_dfp.html diff --git a/integrationExamples/gpt/feedad_dfp.html b/integrationExamples/gpt/feedad_dfp.html new file mode 100644 index 00000000000..43305f87638 --- /dev/null +++ b/integrationExamples/gpt/feedad_dfp.html @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + From e4c7214ed51f52eeb1ac0d534d0d9ea4e92afefa Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 7 Jun 2019 19:06:52 +0200 Subject: [PATCH 19/43] updated feedad adapter to use live system --- modules/feedadBidAdapter.js | 2 +- test/spec/modules/feedadBidAdapter_spec.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 7c26e581e4c..b70314c5be4 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -85,7 +85,7 @@ const TAG = '[FeedAd]'; */ const PLACEMENT_ID_PATTERN = /^(([a-z0-9])+[-_]?)+$/; -const API_ENDPOINT = 'https://feedad-backend-dev.appspot.com'; +const API_ENDPOINT = 'https://api.feedad.com'; const API_PATH_BID_REQUEST = '/1/prebid/web/bids'; const API_PATH_TRACK_REQUEST = '/1/prebid/web/events'; diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 5bf64683380..4bd163dacae 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -205,7 +205,7 @@ describe('FeedAdAdapter', function () { params: {clientToken: 'clientToken', placementId: 'placement-id'} }; let result = spec.buildRequests([bid], bidderRequest); - expect(result.url).to.equal('https://feedad-backend-dev.appspot.com/1/prebid/web/bids'); + expect(result.url).to.equal('https://api.feedad.com/1/prebid/web/bids'); }); it('should specify the content type explicitly', function () { let bid = { @@ -252,12 +252,12 @@ describe('FeedAdAdapter', function () { describe('interpretResponse', function () { const body = [{ - foo: "bar", + foo: 'bar', sub: { obj: 5 } }, { - bar: "foo" + bar: 'foo' }]; it('should convert string bodies to JSON', function () { @@ -415,7 +415,7 @@ describe('FeedAdAdapter', function () { subject(data, xhr); expect(xhr.callCount).to.equal(1); let call = xhr.getCall(0); - expect(call.args[0]).to.equal('https://feedad-backend-dev.appspot.com/1/prebid/web/events'); + expect(call.args[0]).to.equal('https://api.feedad.com/1/prebid/web/events'); expect(call.args[1]).to.be.null; expect(JSON.parse(call.args[2])).to.deep.equal(expectedData); expect(call.args[3]).to.deep.equal({ From 0eabd34facf3481b8e81672c75e36be841f21d2f Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Mon, 10 Jun 2019 15:39:00 +0200 Subject: [PATCH 20/43] updated FeedAd adapter placement ID regex --- modules/feedadBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index b70314c5be4..f180c2782cc 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -83,7 +83,7 @@ const TAG = '[FeedAd]'; * Pattern for valid placement IDs * @type {RegExp} */ -const PLACEMENT_ID_PATTERN = /^(([a-z0-9])+[-_]?)+$/; +const PLACEMENT_ID_PATTERN = /^(?:[a-z0-9]+[-_]?)+$/; const API_ENDPOINT = 'https://api.feedad.com'; const API_PATH_BID_REQUEST = '/1/prebid/web/bids'; From 1ff7caa53351aed660f35e8a632337f0b0f463df Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Tue, 11 Jun 2019 10:44:13 +0200 Subject: [PATCH 21/43] removed groups from FeedAd adapter placement ID regex --- modules/feedadBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index f180c2782cc..61caf416538 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -83,7 +83,7 @@ const TAG = '[FeedAd]'; * Pattern for valid placement IDs * @type {RegExp} */ -const PLACEMENT_ID_PATTERN = /^(?:[a-z0-9]+[-_]?)+$/; +const PLACEMENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]+[a-z0-9]$/; const API_ENDPOINT = 'https://api.feedad.com'; const API_PATH_BID_REQUEST = '/1/prebid/web/bids'; From 748590ea44e03edaa5a21e32f6460b15d67b832a Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 14 Jun 2019 11:51:42 +0200 Subject: [PATCH 22/43] removed dedicated feedad example page --- integrationExamples/gpt/feedad_dfp.html | 96 ------------------------- 1 file changed, 96 deletions(-) delete mode 100644 integrationExamples/gpt/feedad_dfp.html diff --git a/integrationExamples/gpt/feedad_dfp.html b/integrationExamples/gpt/feedad_dfp.html deleted file mode 100644 index 43305f87638..00000000000 --- a/integrationExamples/gpt/feedad_dfp.html +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - -

Prebid.js Test

-
Div-1
-
- -
- - From 98e422ada066d20328cc4fad768df60ab3406500 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 14 Jun 2019 11:52:57 +0200 Subject: [PATCH 23/43] updated imports in FeedAd adapter file to use relative paths --- modules/feedadBidAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 61caf416538..2227d1c1116 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -1,5 +1,5 @@ -import * as utils from 'src/utils'; -import {registerBidder} from 'src/adapters/bidderFactory'; +import * as utils from '../src/utils'; +import {registerBidder} from '../src/adapters/bidderFactory'; import {BANNER, VIDEO} from '../src/mediaTypes'; import {ajax} from '../src/ajax'; From f2836f34de478f98207f0d4b7ef7256d39d13314 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 14 Jun 2019 12:09:29 +0200 Subject: [PATCH 24/43] updated FeedAd adapter unit test to use sinon.useFakeXMLHttpRequest() --- modules/feedadBidAdapter.js | 5 ++- test/spec/modules/feedadBidAdapter_spec.js | 37 ++++++++++++---------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 2227d1c1116..1e995ee8914 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -259,14 +259,13 @@ function createTrackingParams(data, klass) { * @return {Function} the tracking handler function */ function trackingHandlerFactory(klass) { - return (data, xhr) => { + return (data) => { if (!data) { return; } let params = createTrackingParams(data, klass); if (params) { - xhr = typeof xhr === 'function' ? xhr : ajax; // required to test calls to the ajax method because it cannot be mocked - xhr(`${API_ENDPOINT}${API_PATH_TRACK_REQUEST}`, null, JSON.stringify(params), { + ajax(`${API_ENDPOINT}${API_PATH_TRACK_REQUEST}`, null, JSON.stringify(params), { withCredentials: true, method: 'POST', contentType: 'application/json' diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 4bd163dacae..3432a40eca4 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -383,20 +383,27 @@ describe('FeedAdAdapter', function () { let subject = spec[name]; describe(name + ' handler', function () { let xhr; + let requests; beforeEach(function () { - xhr = sinon.stub(); + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = xhr => requests.push(xhr); + }); + + afterEach(function () { + xhr.restore(); }); it('should do nothing on empty data', function () { - subject(undefined, xhr); - subject(null, xhr); - expect(xhr.called).to.be.false; + subject(undefined); + subject(null); + expect(requests.length).to.equal(0); }); it('should do nothing when bid metadata is not set', function () { - subject(data, xhr); - expect(xhr.callCount).to.equal(0); + subject(data); + expect(requests.length).to.equal(0); }); it('should send tracking params when correct metadata was set', function () { @@ -412,17 +419,13 @@ describe('FeedAdAdapter', function () { referer, sdk_version: '1.0.0' }; - subject(data, xhr); - expect(xhr.callCount).to.equal(1); - let call = xhr.getCall(0); - expect(call.args[0]).to.equal('https://api.feedad.com/1/prebid/web/events'); - expect(call.args[1]).to.be.null; - expect(JSON.parse(call.args[2])).to.deep.equal(expectedData); - expect(call.args[3]).to.deep.equal({ - withCredentials: true, - method: 'POST', - contentType: 'application/json' - }); + subject(data); + expect(requests.length).to.equal(1); + let call = requests[0]; + expect(call.url).to.equal('https://api.feedad.com/1/prebid/web/events'); + expect(JSON.parse(call.requestBody)).to.deep.equal(expectedData); + expect(call.method).to.equal('POST'); + expect(call.requestHeaders).to.include({'Content-Type': 'application/json;charset=utf-8'}); }) }); }); From d9f91247e0fb943799b6202845e40707be0ce90c Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Wed, 7 Apr 2021 13:03:20 +0200 Subject: [PATCH 25/43] added GDPR fields to the FeedAd bid request --- modules/feedadBidAdapter.js | 4 +++ test/spec/modules/feedadBidAdapter_spec.js | 34 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 3992f2db5e0..e34e018b303 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -204,6 +204,10 @@ function buildRequests(validBidRequests, bidderRequest) { referer: data.refererInfo.referer, transactionId: bid.transactionId }); + if (bidderRequest && bidderRequest.gdprConsent) { + data.consentIabTcf = bidderRequest.gdprConsent.consentString; + data.gdprApplies = bidderRequest.gdprConsent.gdprApplies; + } return { method: 'POST', url: `${API_ENDPOINT}${API_PATH_BID_REQUEST}`, diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 066ab6b21f6..58b24a9ca36 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -248,6 +248,40 @@ describe('FeedAdAdapter', function () { let result = spec.buildRequests([bid, bid, bid]); expect(result).to.be.empty; }); + it('should not include GDPR data if the bidder request has none available', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid], bidderRequest); + expect(result.data.gdprApplies).to.be.undefined; + expect(result.data.consentIabTcf).to.be.undefined; + }); + it('should include GDPR data if the bidder requests contains it', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let request = Object.assign({}, bidderRequest, { + gdprConsent: { + consentString: 'the consent string', + gdprApplies: true + } + }); + let result = spec.buildRequests([bid], request); + expect(result.data.gdprApplies).to.equal(request.gdprConsent.gdprApplies); + expect(result.data.consentIabTcf).to.equal(request.gdprConsent.consentString); + }); }); describe('interpretResponse', function () { From 19456aa0e75fbe4f0d0d9235a9a1a0d8471dce83 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Wed, 7 Apr 2021 13:04:44 +0200 Subject: [PATCH 26/43] removed video from supported media types of the FeedAd adapter --- modules/feedadBidAdapter.js | 4 ++-- test/spec/modules/feedadBidAdapter_spec.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index e34e018b303..056ca224672 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -1,6 +1,6 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {BANNER} from '../src/mediaTypes.js'; import {ajax} from '../src/ajax.js'; /** @@ -71,7 +71,7 @@ const BIDDER_CODE = 'feedad'; * The media types supported by FeedAd * @type {MediaType[]} */ -const MEDIA_TYPES = [VIDEO, BANNER]; +const MEDIA_TYPES = [BANNER]; /** * Tag for logging diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 58b24a9ca36..c16f7ef11f9 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -13,7 +13,7 @@ describe('FeedAdAdapter', function () { it('should only support video and banner ads', function () { expect(spec.supportedMediaTypes).to.be.a('array'); expect(spec.supportedMediaTypes).to.include(BANNER); - expect(spec.supportedMediaTypes).to.include(VIDEO); + expect(spec.supportedMediaTypes).not.to.include(VIDEO); expect(spec.supportedMediaTypes).not.to.include(NATIVE); }); it('should export the BidderSpec functions', function () { From 3ed82460ce49c3cde724f1a09146092d9c3a984a Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Wed, 7 Apr 2021 13:17:48 +0200 Subject: [PATCH 27/43] increased version code of FeedAd adapter to 1.0.2 --- modules/feedadBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 056ca224672..9fbaea152b0 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -7,7 +7,7 @@ import {ajax} from '../src/ajax.js'; * Version of the FeedAd bid adapter * @type {string} */ -const VERSION = '1.0.0'; +const VERSION = '1.0.2'; /** * @typedef {object} FeedAdApiBidRequest From b8f178ce8edfd712245f02b61f470d988273c3df Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Wed, 7 Apr 2021 13:51:41 +0200 Subject: [PATCH 28/43] removed unnecessary check of bidder request --- modules/feedadBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 9fbaea152b0..af544a723b7 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -204,7 +204,7 @@ function buildRequests(validBidRequests, bidderRequest) { referer: data.refererInfo.referer, transactionId: bid.transactionId }); - if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent) { data.consentIabTcf = bidderRequest.gdprConsent.consentString; data.gdprApplies = bidderRequest.gdprConsent.gdprApplies; } From 67014fd2c0009f3d2a1c7931c19d80d50e578d0e Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Wed, 7 Apr 2021 14:10:50 +0200 Subject: [PATCH 29/43] fixed unit test testing for old FeedAd version --- test/spec/modules/feedadBidAdapter_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index c16f7ef11f9..8c7ba46374a 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -438,7 +438,7 @@ describe('FeedAdAdapter', function () { prebid_bid_id: bidId, prebid_transaction_id: transactionId, referer, - sdk_version: '1.0.0' + sdk_version: '1.0.2' }; subject(data); expect(server.requests.length).to.equal(1); From e7e7dfff9a018caa0f314b80d941ffbff9b4f3c9 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Wed, 7 Apr 2021 14:51:14 +0200 Subject: [PATCH 30/43] removed video media type example from documentation file --- modules/feedadBidAdapter.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/feedadBidAdapter.md b/modules/feedadBidAdapter.md index fd57025c29e..6f705df36b5 100644 --- a/modules/feedadBidAdapter.md +++ b/modules/feedadBidAdapter.md @@ -18,9 +18,6 @@ Prebid.JS adapter that connects to the FeedAd demand sources. mediaTypes: { banner: { // supports all banner sizes sizes: [[300, 250]], - }, - video: { // supports only outstream video - context: 'outstream' } }, bids: [ From c8242fb97a3f00a088702332689e97a53a953074 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Fri, 9 Apr 2021 09:30:17 +0200 Subject: [PATCH 31/43] added gvlid to FeedAd adapter --- modules/feedadBidAdapter.js | 6 ++++++ test/spec/modules/feedadBidAdapter_spec.js | 3 +++ 2 files changed, 9 insertions(+) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index af544a723b7..54a4ef0c998 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -61,6 +61,11 @@ const VERSION = '1.0.2'; * @property [device_platform] {1|2|3} 1 - Android | 2 - iOS | 3 - Windows */ +/** + * The IAB TCF 2.0 vendor ID for the FeedAd GmbH + */ +const TCF_VENDOR_ID = 781; + /** * Bidder network identity code * @type {string} @@ -283,6 +288,7 @@ function trackingHandlerFactory(klass) { */ export const spec = { code: BIDDER_CODE, + gvlid: TCF_VENDOR_ID, supportedMediaTypes: MEDIA_TYPES, isBidRequestValid, buildRequests, diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 8c7ba46374a..6b75af0d55d 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -23,6 +23,9 @@ describe('FeedAdAdapter', function () { expect(spec.onTimeout).to.be.a('function'); expect(spec.onBidWon).to.be.a('function'); }); + it('should export the TCF vendor ID', function () { + expect(spec.gvlid).to.equal(781); + }) }); describe('isBidRequestValid', function () { From 3bd97a9e27b3c8434bfd825ff6524fa4f92c0bf8 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Tue, 7 Dec 2021 10:05:56 +0100 Subject: [PATCH 32/43] added decoration parameter to adapter documentation --- modules/feedadBidAdapter.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/feedadBidAdapter.md b/modules/feedadBidAdapter.md index 6f705df36b5..fc3537ec331 100644 --- a/modules/feedadBidAdapter.md +++ b/modules/feedadBidAdapter.md @@ -26,6 +26,7 @@ Prebid.JS adapter that connects to the FeedAd demand sources. params: { clientToken: 'your-client-token' // see below for more info placementId: 'your-placement-id' // see below for more info + decoration: 'decoration parameters' // optional, see below for info } } ] @@ -35,7 +36,8 @@ Prebid.JS adapter that connects to the FeedAd demand sources. # Required Parameters -| Parameter | Description | -| --------- | ----------- | +| Parameter | Description | +|---------------| ----------- | | `clientToken` | Your FeedAd web client token. You can view your client token inside the FeedAd admin panel. | | `placementId` | You can choose placement IDs yourself. A placement ID should be named after the ad position inside your product. For example, if you want to display an ad inside a list of news articles, you could name it "ad-news-overview".
A placement ID may consist of lowercase `a-z`, `0-9`, `-` and `_`. You do not have to manually create the placement IDs before using them. Just specify them within the code, and they will appear in the FeedAd admin panel after the first request.
[Learn more](/concept/feed_ad/index.html) about Placement IDs and how they are grouped to play the same Creative. | +| `decoration` | Optional. If you want to apply a [decoration](https://docs.feedad.com/web/feed_ad/#decorations) to the ad. From 2aa7c007fba00ec6b049cc1d9f3caa772f4a0dc3 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Tue, 7 Dec 2021 10:21:23 +0100 Subject: [PATCH 33/43] added pass through of additional bid parameters --- modules/feedadBidAdapter.js | 4 ++-- test/spec/modules/feedadBidAdapter_spec.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 492c35474ba..6fb39c49ec8 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -176,13 +176,13 @@ function isMediaTypesEmpty(mediaTypes) { * @return {FeedAdApiBidRequest} */ function createApiBidRParams(request) { - return { + return Object.assign({}, request.params, { ad_type: 0, client_token: request.params.clientToken, placement_id: request.params.placementId, sdk_version: `prebid_${VERSION}`, app_hybrid: false, - }; + }); } /** diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 6b75af0d55d..2739654eb5d 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -171,6 +171,21 @@ describe('FeedAdAdapter', function () { expect(result.data.bids).to.be.lengthOf(1); expect(result.data.bids[0]).to.deep.equal(bid); }); + it('should pass through additional bid parameters', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id', another: 'parameter', more: 'parameters'} + }; + let result = spec.buildRequests([bid], bidderRequest); + expect(result.data.bids).to.be.lengthOf(1); + expect(result.data.bids[0].params.another).to.equal('parameter'); + expect(result.data.bids[0].params.more).to.equal('parameters'); + }); it('should detect empty media types', function () { let bid = { code: 'feedad', From 1d862727b0ba90af59c18d24870f7bba9f0f2e5a Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Thu, 8 Sep 2022 17:01:34 +0200 Subject: [PATCH 34/43] added user syncs to FeedAd bid adapter --- modules/feedadBidAdapter.js | 46 +++++- test/spec/modules/feedadBidAdapter_spec.js | 156 +++++++++++++++++++-- 2 files changed, 185 insertions(+), 17 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index d695292bb4a..e8fb2472ef7 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -1,4 +1,4 @@ -import { deepAccess, logWarn } from '../src/utils.js'; +import {deepAccess, isArray, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {ajax} from '../src/ajax.js'; @@ -31,7 +31,7 @@ const VERSION = '1.0.2'; * @typedef {object} FeedAdApiBidResponse * @inner * - * @property {string} ad - Ad HTML payload + * @property {string} [ad] - Ad HTML payload * @property {number} cpm - number / float * @property {string} creativeId - ID of creative for tracking * @property {string} currency - 3-letter ISO 4217 currency-code @@ -40,6 +40,7 @@ const VERSION = '1.0.2'; * @property {string} requestId - bids[].bidId * @property {number} ttl - Time to live for this ad * @property {number} width - Width of creative returned in [].ad + * @property {object} [ext] - an extension object */ /** @@ -231,10 +232,16 @@ function buildRequests(validBidRequests, bidderRequest) { * @returns {Bid[]} the FeedAd bids */ function interpretResponse(serverResponse, request) { - /** - * @type FeedAdApiBidResponse[] - */ - return typeof serverResponse.body === 'string' ? JSON.parse(serverResponse.body) : serverResponse.body; + const response = typeof serverResponse.body === 'string' ? JSON.parse(serverResponse.body) : serverResponse.body; + if (!isArray(response)) { + return []; + } + return response.filter(bid => Object.prototype.hasOwnProperty.call(bid, "ad")) + .map(bid => { + const copy = Object.assign({}, bid); + delete copy.ext; + return copy; + }); } /** @@ -284,6 +291,30 @@ function trackingHandlerFactory(klass) { } } +/** + * Reads the user syncs off the server responses and converts them into Prebid.JS format + * @param {SyncOptions} syncOptions + * @param {FeedAdApiBidResponse[]} serverResponses + * @param gdprConsent + * @param uspConsent + */ +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + return serverResponses.map(response => response.ext) + .flatMap(extension => { + // extract user syncs from extension + const pixels = syncOptions.pixelEnabled && extension.pixels ? extension.pixels : []; + const iframes = syncOptions.iframeEnabled && extension.iframes ? extension.iframes : []; + return pixels.concat(...iframes); + }) + .reduce((syncs, sync) => { + // remove duplicates + if (!syncs.find(it => it.type === sync.type && it.url === sync.url)) { + syncs.push(sync); + } + return syncs; + }, []); +} + /** * @type {BidderSpec} */ @@ -295,7 +326,8 @@ export const spec = { buildRequests, interpretResponse, onTimeout: trackingHandlerFactory('prebid_bidTimeout'), - onBidWon: trackingHandlerFactory('prebid_bidWon') + onBidWon: trackingHandlerFactory('prebid_bidWon'), + getUserSyncs }; registerBidder(spec); diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index fc26d9bc0cf..f0ca6c452ca 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -303,24 +303,160 @@ describe('FeedAdAdapter', function () { }); describe('interpretResponse', function () { - const body = [{ - foo: 'bar', - sub: { - obj: 5 - } - }, { - bar: 'foo' - }]; - it('should convert string bodies to JSON', function () { + const body = [{ + ad: 'bar', + }]; let result = spec.interpretResponse({body: JSON.stringify(body)}); expect(result).to.deep.equal(body); }); - it('should pass through body objects', function () { + it('should pass through object bodies', function () { + const body = [{ + ad: 'bar', + }]; let result = spec.interpretResponse({body}); expect(result).to.deep.equal(body); }); + + it('should pass through only bodies with ad fields', function () { + const bid1 = { + ad: 'bar', + other: 'field', + some: 'thing' + }; + const bid2 = { + foo: 'bar' + }; + const bid3 = { + ad: 'ad html', + }; + const body = [bid1, bid2, bid3]; + let result = spec.interpretResponse({body: JSON.stringify(body)}); + expect(result).to.deep.equal([bid1, bid3]); + }); + + it('should remove extension fields from bid responses', function () { + const bid = { + ext: {}, + ad: 'ad html', + cpm: 100 + }; + const result = spec.interpretResponse({body: JSON.stringify([bid])}); + expect(result[0]).not.to.haveOwnProperty('ext'); + }); + + it('should return an empty array if the response is not an array', function () { + const bid = {}; + const result = spec.interpretResponse({body: JSON.stringify(bid)}); + expect(result).to.deep.equal([]); + }); + }); + + describe('getUserSyncs', function () { + const pixelSync1 = {type: "image", url: "the pixel url 1"}; + const pixelSync2 = {type: "image", url: "the pixel url 2"}; + const iFrameSync1 = {type: "iframe", url: "the iFrame url 1"}; + const iFrameSync2 = {type: "iframe", url: "the iFrame url 2"}; + + it('should pass through the syncs out of the extension fields of the server response', function () { + const serverResponse = [{ + ext: { + pixels: [pixelSync1, pixelSync2], + iframes: [iFrameSync1, iFrameSync2], + } + }]; + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse) + expect(result).to.deep.equal([ + pixelSync1, + pixelSync2, + iFrameSync1, + iFrameSync2, + ]); + }); + + it('should concat the syncs of all responses', function () { + const serverResponse = [{ + ext: { + pixels: [pixelSync1], + iframes: [iFrameSync2], + }, + ad: "ad html", + cpm: 100 + }, { + ext: { + iframes: [iFrameSync1], + } + }, { + ext: { + pixels: [pixelSync2], + } + }]; + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse); + expect(result).to.deep.equal([ + pixelSync1, + iFrameSync2, + iFrameSync1, + pixelSync2, + ]); + }); + + it('should filter out duplicates', function () { + const serverResponse = [{ + ext: { + pixels: [pixelSync1, pixelSync1], + iframes: [iFrameSync2, iFrameSync2], + } + }, { + ext: { + iframes: [iFrameSync2, iFrameSync2], + } + }]; + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse); + expect(result).to.deep.equal([ + pixelSync1, + iFrameSync2, + ]); + }); + + it('should not include iFrame syncs if the option is disabled', function () { + const serverResponse = [{ + ext: { + pixels: [pixelSync1, pixelSync2], + iframes: [iFrameSync1, iFrameSync2], + } + }]; + const result = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, serverResponse); + expect(result).to.deep.equal([ + pixelSync1, + pixelSync2, + ]); + }); + + it('should not include pixel syncs if the option is disabled', function () { + const serverResponse = [{ + ext: { + pixels: [pixelSync1, pixelSync2], + iframes: [iFrameSync1, iFrameSync2], + } + }]; + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, serverResponse); + expect(result).to.deep.equal([ + iFrameSync1, + iFrameSync2, + ]); + }); + + it('should not include any syncs if the sync options are disabled or missing', function () { + const serverResponse = [{ + ext: { + pixels: [pixelSync1, pixelSync2], + iframes: [iFrameSync1, iFrameSync2], + } + }]; + const result = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}, serverResponse); + expect(result).to.deep.equal([]); + }); }); describe('event tracking calls', function () { From f76f232ab898204bb0646bfe162b88179d782ea0 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Thu, 8 Sep 2022 17:03:49 +0200 Subject: [PATCH 35/43] increased FeedAd bid adapter version --- modules/feedadBidAdapter.js | 2 +- test/spec/modules/feedadBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index e8fb2472ef7..eddf72d0a97 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -7,7 +7,7 @@ import {ajax} from '../src/ajax.js'; * Version of the FeedAd bid adapter * @type {string} */ -const VERSION = '1.0.2'; +const VERSION = '1.0.3'; /** * @typedef {object} FeedAdApiBidRequest diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index f0ca6c452ca..e5635fbde39 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -592,7 +592,7 @@ describe('FeedAdAdapter', function () { prebid_bid_id: bidId, prebid_transaction_id: transactionId, referer, - sdk_version: '1.0.2' + sdk_version: '1.0.3' }; subject(data); expect(server.requests.length).to.equal(1); From d9d266cdb6bc5e26ba6dc06c3b85747efd465a89 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Thu, 8 Sep 2022 17:33:25 +0200 Subject: [PATCH 36/43] lint pass over FeedAd bid adapter --- modules/feedadBidAdapter.js | 38 +++++++++++----------- test/spec/modules/feedadBidAdapter_spec.js | 10 +++--- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index eddf72d0a97..0ab6def38ae 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -236,12 +236,12 @@ function interpretResponse(serverResponse, request) { if (!isArray(response)) { return []; } - return response.filter(bid => Object.prototype.hasOwnProperty.call(bid, "ad")) - .map(bid => { - const copy = Object.assign({}, bid); - delete copy.ext; - return copy; - }); + return response.filter(bid => Object.prototype.hasOwnProperty.call(bid, 'ad')) + .map(bid => { + const copy = Object.assign({}, bid); + delete copy.ext; + return copy; + }); } /** @@ -300,19 +300,19 @@ function trackingHandlerFactory(klass) { */ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { return serverResponses.map(response => response.ext) - .flatMap(extension => { - // extract user syncs from extension - const pixels = syncOptions.pixelEnabled && extension.pixels ? extension.pixels : []; - const iframes = syncOptions.iframeEnabled && extension.iframes ? extension.iframes : []; - return pixels.concat(...iframes); - }) - .reduce((syncs, sync) => { - // remove duplicates - if (!syncs.find(it => it.type === sync.type && it.url === sync.url)) { - syncs.push(sync); - } - return syncs; - }, []); + .flatMap(extension => { + // extract user syncs from extension + const pixels = syncOptions.pixelEnabled && extension.pixels ? extension.pixels : []; + const iframes = syncOptions.iframeEnabled && extension.iframes ? extension.iframes : []; + return pixels.concat(...iframes); + }) + .reduce((syncs, sync) => { + // remove duplicates + if (!syncs.find(it => it.type === sync.type && it.url === sync.url)) { + syncs.push(sync); + } + return syncs; + }, []); } /** diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index e5635fbde39..6db0b88a9bf 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -354,10 +354,10 @@ describe('FeedAdAdapter', function () { }); describe('getUserSyncs', function () { - const pixelSync1 = {type: "image", url: "the pixel url 1"}; - const pixelSync2 = {type: "image", url: "the pixel url 2"}; - const iFrameSync1 = {type: "iframe", url: "the iFrame url 1"}; - const iFrameSync2 = {type: "iframe", url: "the iFrame url 2"}; + const pixelSync1 = {type: 'image', url: 'the pixel url 1'}; + const pixelSync2 = {type: 'image', url: 'the pixel url 2'}; + const iFrameSync1 = {type: 'iframe', url: 'the iFrame url 1'}; + const iFrameSync2 = {type: 'iframe', url: 'the iFrame url 2'}; it('should pass through the syncs out of the extension fields of the server response', function () { const serverResponse = [{ @@ -381,7 +381,7 @@ describe('FeedAdAdapter', function () { pixels: [pixelSync1], iframes: [iFrameSync2], }, - ad: "ad html", + ad: 'ad html', cpm: 100 }, { ext: { From e23c75aa9d7506732f1b7beb3d1532968a2f6373 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Tue, 20 Dec 2022 17:06:12 +0100 Subject: [PATCH 37/43] fixed parsing of user syncs from server response --- modules/feedadBidAdapter.js | 12 +++++- test/spec/modules/feedadBidAdapter_spec.js | 49 ++++++++++++++++------ 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 0ab6def38ae..657d863a1d2 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -294,12 +294,20 @@ function trackingHandlerFactory(klass) { /** * Reads the user syncs off the server responses and converts them into Prebid.JS format * @param {SyncOptions} syncOptions - * @param {FeedAdApiBidResponse[]} serverResponses + * @param {ServerResponse[]} serverResponses * @param gdprConsent * @param uspConsent */ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { - return serverResponses.map(response => response.ext) + return serverResponses.map(response => { + // validate response format + const ext = deepAccess(response, 'body.ext', []); + if (ext == null) { + return null; + } + return ext; + }) + .filter(ext => ext != null) .flatMap(extension => { // extract user syncs from extension const pixels = syncOptions.pixelEnabled && extension.pixels ? extension.pixels : []; diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 6db0b88a9bf..ce7bc75f177 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -358,14 +358,20 @@ describe('FeedAdAdapter', function () { const pixelSync2 = {type: 'image', url: 'the pixel url 2'}; const iFrameSync1 = {type: 'iframe', url: 'the iFrame url 1'}; const iFrameSync2 = {type: 'iframe', url: 'the iFrame url 2'}; + const mockServerResponse = (content) => { + if (!(content instanceof Array)) { + content = [content]; + } + return content.map(it => ({body: it})); + }; it('should pass through the syncs out of the extension fields of the server response', function () { - const serverResponse = [{ + const serverResponse = mockServerResponse([{ ext: { pixels: [pixelSync1, pixelSync2], iframes: [iFrameSync1, iFrameSync2], } - }]; + }]); const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse) expect(result).to.deep.equal([ pixelSync1, @@ -376,7 +382,7 @@ describe('FeedAdAdapter', function () { }); it('should concat the syncs of all responses', function () { - const serverResponse = [{ + const serverResponse = mockServerResponse([{ ext: { pixels: [pixelSync1], iframes: [iFrameSync2], @@ -391,7 +397,7 @@ describe('FeedAdAdapter', function () { ext: { pixels: [pixelSync2], } - }]; + }]); const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse); expect(result).to.deep.equal([ pixelSync1, @@ -402,7 +408,7 @@ describe('FeedAdAdapter', function () { }); it('should filter out duplicates', function () { - const serverResponse = [{ + const serverResponse = mockServerResponse([{ ext: { pixels: [pixelSync1, pixelSync1], iframes: [iFrameSync2, iFrameSync2], @@ -411,7 +417,7 @@ describe('FeedAdAdapter', function () { ext: { iframes: [iFrameSync2, iFrameSync2], } - }]; + }]); const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse); expect(result).to.deep.equal([ pixelSync1, @@ -420,12 +426,12 @@ describe('FeedAdAdapter', function () { }); it('should not include iFrame syncs if the option is disabled', function () { - const serverResponse = [{ + const serverResponse = mockServerResponse([{ ext: { pixels: [pixelSync1, pixelSync2], iframes: [iFrameSync1, iFrameSync2], } - }]; + }]); const result = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, serverResponse); expect(result).to.deep.equal([ pixelSync1, @@ -434,12 +440,12 @@ describe('FeedAdAdapter', function () { }); it('should not include pixel syncs if the option is disabled', function () { - const serverResponse = [{ + const serverResponse = mockServerResponse([{ ext: { pixels: [pixelSync1, pixelSync2], iframes: [iFrameSync1, iFrameSync2], } - }]; + }]); const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, serverResponse); expect(result).to.deep.equal([ iFrameSync1, @@ -448,15 +454,34 @@ describe('FeedAdAdapter', function () { }); it('should not include any syncs if the sync options are disabled or missing', function () { - const serverResponse = [{ + const serverResponse = mockServerResponse([{ ext: { pixels: [pixelSync1, pixelSync2], iframes: [iFrameSync1, iFrameSync2], } - }]; + }]); const result = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}, serverResponse); expect(result).to.deep.equal([]); }); + + it('should handle empty responses', function () { + const serverResponse = mockServerResponse([]); + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse) + expect(result).to.deep.equal([]); + }); + + it('should not throw if the server response is weird', function () { + const responses = [ + mockServerResponse(null), + mockServerResponse('null'), + mockServerResponse(1234), + mockServerResponse({}), + mockServerResponse([{}, 123]), + ]; + responses.forEach(it => { + expect(() => spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, it)).not.to.throw; + }); + }); }); describe('event tracking calls', function () { From 2f98d6dd9b63f1af403e539851ec66bbad35c5a6 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Tue, 20 Dec 2022 17:06:34 +0100 Subject: [PATCH 38/43] increased FeedAd bid adapter version --- modules/feedadBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 657d863a1d2..ef2e57c553f 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -7,7 +7,7 @@ import {ajax} from '../src/ajax.js'; * Version of the FeedAd bid adapter * @type {string} */ -const VERSION = '1.0.3'; +const VERSION = '1.0.4'; /** * @typedef {object} FeedAdApiBidRequest From 6b013f683b67002a4652c2207f8f6c00e7426c38 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Tue, 20 Dec 2022 17:12:43 +0100 Subject: [PATCH 39/43] fixed version code in test file --- test/spec/modules/feedadBidAdapter_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index ce7bc75f177..6aed670a563 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -617,7 +617,7 @@ describe('FeedAdAdapter', function () { prebid_bid_id: bidId, prebid_transaction_id: transactionId, referer, - sdk_version: '1.0.3' + sdk_version: '1.0.4' }; subject(data); expect(server.requests.length).to.equal(1); From d93f4dd4e21dc9ddb2a9ea019920d2659d0fd970 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Thu, 5 Jan 2023 11:16:08 +0100 Subject: [PATCH 40/43] added adapter and prebid version to bid request parameters --- modules/feedadBidAdapter.js | 9 ++++++--- test/spec/modules/feedadBidAdapter_spec.js | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index ef2e57c553f..94325c7d577 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -16,7 +16,8 @@ const VERSION = '1.0.4'; * @property {number} ad_type * @property {string} client_token * @property {string} placement_id - * @property {string} sdk_version + * @property {string} prebid_adapter_version + * @property {string} prebid_sdk_version * @property {boolean} app_hybrid * * @property {string} [app_bundle_id] @@ -181,7 +182,8 @@ function createApiBidRParams(request) { ad_type: 0, client_token: request.params.clientToken, placement_id: request.params.placementId, - sdk_version: `prebid_${VERSION}`, + prebid_adapter_version: VERSION, + prebid_sdk_version: '$prebid.version$', app_hybrid: false, }); } @@ -266,7 +268,8 @@ function createTrackingParams(data, klass) { prebid_bid_id: bidId, prebid_transaction_id: transactionId, referer, - sdk_version: VERSION + prebid_adapter_version: VERSION, + prebid_sdk_version: '$prebid.version$', }; } diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 6aed670a563..94644d9f0f4 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -4,6 +4,7 @@ import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; import {server} from 'test/mocks/xhr.js'; const CODE = 'feedad'; +const EXPECTED_ADAPTER_VERSION = '1.0.4'; describe('FeedAdAdapter', function () { describe('Public API', function () { @@ -300,6 +301,20 @@ describe('FeedAdAdapter', function () { expect(result.data.gdprApplies).to.equal(request.gdprConsent.gdprApplies); expect(result.data.consentIabTcf).to.equal(request.gdprConsent.consentString); }); + it('should include adapter and prebid version', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid], bidderRequest); + expect(result.data.bids[0].params.prebid_adapter_version).to.equal(EXPECTED_ADAPTER_VERSION); + expect(result.data.bids[0].params.prebid_sdk_version).to.equal('$prebid.version$'); + }); }); describe('interpretResponse', function () { @@ -617,7 +632,8 @@ describe('FeedAdAdapter', function () { prebid_bid_id: bidId, prebid_transaction_id: transactionId, referer, - sdk_version: '1.0.4' + prebid_adapter_version: EXPECTED_ADAPTER_VERSION, + prebid_sdk_version: '$prebid.version$', }; subject(data); expect(server.requests.length).to.equal(1); From b443e103dbf02233c90d5fa7c7e3c2ffb6c51a16 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Thu, 5 Jan 2023 11:17:01 +0100 Subject: [PATCH 41/43] removed TODO item --- modules/feedadBidAdapter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 94325c7d577..a8e8d51852e 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -209,7 +209,6 @@ function buildRequests(validBidRequests, bidderRequest) { }) }); data.bids.forEach(bid => BID_METADATA[bid.bidId] = { - // TODO: is 'page' the right value here? referer: data.refererInfo.page, transactionId: bid.transactionId }); From f1209aa9d2a5bcc6ea10318e465e0f02eff9ac27 Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Thu, 5 Jan 2023 11:22:29 +0100 Subject: [PATCH 42/43] added missing test case for user syncs --- test/spec/modules/feedadBidAdapter_spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 94644d9f0f4..e5ff7243c65 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -497,6 +497,12 @@ describe('FeedAdAdapter', function () { expect(() => spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, it)).not.to.throw; }); }); + + it('should return empty array if the body extension is null', function () { + const response = mockServerResponse({ext: null}); + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, response); + expect(result).to.deep.equal([]); + }); }); describe('event tracking calls', function () { From 485e41ba9421e7f62301de0a86a915ffefe8f54d Mon Sep 17 00:00:00 2001 From: FeedAd GmbH Date: Thu, 5 Jan 2023 11:23:59 +0100 Subject: [PATCH 43/43] increased adapter version to 1.0.5 --- modules/feedadBidAdapter.js | 2 +- test/spec/modules/feedadBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index a8e8d51852e..7b684efab3c 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -7,7 +7,7 @@ import {ajax} from '../src/ajax.js'; * Version of the FeedAd bid adapter * @type {string} */ -const VERSION = '1.0.4'; +const VERSION = '1.0.5'; /** * @typedef {object} FeedAdApiBidRequest diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index e5ff7243c65..8cbd6907890 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -4,7 +4,7 @@ import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; import {server} from 'test/mocks/xhr.js'; const CODE = 'feedad'; -const EXPECTED_ADAPTER_VERSION = '1.0.4'; +const EXPECTED_ADAPTER_VERSION = '1.0.5'; describe('FeedAdAdapter', function () { describe('Public API', function () {