diff --git a/integrationExamples/gpt/pbjs_example_gpt.html b/integrationExamples/gpt/pbjs_example_gpt.html
index e54a604e2817..fbf21f1f8560 100644
--- a/integrationExamples/gpt/pbjs_example_gpt.html
+++ b/integrationExamples/gpt/pbjs_example_gpt.html
@@ -288,6 +288,13 @@
pubId: 50357, //REQUIRED
host: 'dsp-staging.adkernel.com' //OPTIONAL
}
+ },
+ {
+ bidder: 'zedo',
+ params: {
+ channelCode: 2264002816, //REQUIRED
+ dimId: 9 //REQUIRED
+ }
}
]
}, {
diff --git a/modules/zedoBidAdapter.js b/modules/zedoBidAdapter.js
new file mode 100644
index 000000000000..0420c479ae99
--- /dev/null
+++ b/modules/zedoBidAdapter.js
@@ -0,0 +1,205 @@
+import * as utils from 'src/utils';
+import { registerBidder } from 'src/adapters/bidderFactory';
+import { BANNER, VIDEO } from 'src/mediaTypes';
+import find from 'core-js/library/fn/array/find';
+
+const BIDDER_CODE = 'zedo';
+const URL = '//z2.zedo.com/asw/fmb.json';
+const SECURE_URL = '//z2.zedo.com/asw/fmb.json';
+const DIM_TYPE = {
+ '7': 'display',
+ '9': 'display',
+ '14': 'display',
+ '70': 'SBR',
+ '83': 'CurtainRaiser',
+ '85': 'Inarticle',
+ '86': 'pswipeup',
+ '88': 'Inview',
+ // '85': 'pre-mid-post-roll',
+};
+
+export const spec = {
+ code: BIDDER_CODE,
+ aliases: [],
+ supportedMediaTypes: [BANNER, VIDEO],
+
+ /**
+ * Determines whether or not the given bid request is valid.
+ *
+ * @param {object} bid The bid to validate.
+ * @return boolean True if this is a valid bid, and false otherwise.
+ */
+ isBidRequestValid: function (bid) {
+ return !!(bid.params && bid.params.channelCode && bid.params.dimId);
+ },
+
+ /**
+ * Make a server request from the list of BidRequests.
+ *
+ * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server.
+ * @return ServerRequest Info describing the request to the server.
+ */
+ buildRequests: function (bidRequests, bidderRequest) {
+ let data = {
+ placements: []
+ };
+ bidRequests.map(bidRequest => {
+ let channelCode = parseInt(bidRequest.params.channelCode);
+ let network = parseInt(channelCode / 1000000);
+ let channel = channelCode % 1000000;
+ let dim = getSizes(bidRequest.sizes);
+ let placement = {
+ id: bidRequest.bidId,
+ network: network,
+ channel: channel,
+ width: dim[0],
+ height: dim[1],
+ dimension: bidRequest.params.dimId,
+ version: '$prebid.version$',
+ keyword: '',
+ transactionId: bidRequest.transactionId
+ }
+ if (bidderRequest && bidderRequest.gdprConsent) {
+ if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') {
+ data.gdpr = Number(bidderRequest.gdprConsent.gdprApplies);
+ }
+ data.gdpr_consent = bidderRequest.gdprConsent.consentString;
+ }
+ let dimType = DIM_TYPE[String(bidRequest.params.dimId)]
+ if (dimType) {
+ placement['renderers'] = [{
+ 'name': dimType
+ }]
+ } else { // default to display
+ placement['renderers'] = [{
+ 'name': 'display'
+ }]
+ }
+ data['placements'].push(placement);
+ });
+ let reqUrl = utils.getTopWindowLocation().protocol === 'http:' ? URL : SECURE_URL;
+ return {
+ method: 'GET',
+ url: reqUrl,
+ data: 'g=' + JSON.stringify(data)
+ }
+ },
+
+ /**
+ * Unpack the response from the server into a list of bids.
+ *
+ * @param {*} serverResponse A successful response from the server.
+ * @return {Bid[]} An array of bids which were nested inside the server.
+ */
+ interpretResponse: function (serverResponse, request) {
+ serverResponse = serverResponse.body;
+ const bids = [];
+ if (!serverResponse || serverResponse.error) {
+ let errorMessage = `in response for ${request.bidderCode} adapter`;
+ if (serverResponse && serverResponse.error) { errorMessage += `: ${serverResponse.error}`; }
+ utils.logError(errorMessage);
+ return bids;
+ }
+
+ if (serverResponse.ad) {
+ serverResponse.ad.forEach(ad => {
+ const creativeBid = getCreative(ad);
+ if (creativeBid) {
+ if (parseInt(creativeBid.cpm) !== 0) {
+ const bid = newBid(ad, creativeBid, request);
+ bid.mediaType = parseMediaType(creativeBid);
+ bids.push(bid);
+ }
+ }
+ });
+ }
+ return bids;
+ },
+
+ getUserSyncs: function (syncOptions, responses, gdprConsent) {
+ if (syncOptions.iframeEnabled) {
+ let url = utils.getTopWindowLocation().protocol === 'http:' ? 'http://d3.zedo.com/rs/us/fcs.html' : 'https://tt3.zedo.com/rs/us/fcs.html';
+ if (gdprConsent && typeof gdprConsent.consentString === 'string') {
+ // add 'gdpr' only if 'gdprApplies' is defined
+ if (typeof gdprConsent.gdprApplies === 'boolean') {
+ url += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`;
+ } else {
+ url += `?gdpr_consent=${gdprConsent.consentString}`;
+ }
+ }
+ return [{
+ type: 'iframe',
+ url: url
+ }];
+ }
+ }
+};
+
+function getCreative(ad) {
+ return ad && ad.creatives && ad.creatives.length && find(ad.creatives, creative => creative.adId);
+}
+/**
+ * Unpack the Server's Bid into a Prebid-compatible one.
+ * @param serverBid
+ * @param rtbBid
+ * @param bidderRequest
+ * @return Bid
+ */
+function newBid(serverBid, creativeBid, bidderRequest) {
+ const bid = {
+ requestId: serverBid.slotId,
+ creativeId: creativeBid.adId,
+ dealId: 99999999,
+ currency: 'USD',
+ netRevenue: true,
+ ttl: 300
+ };
+
+ if (creativeBid.creativeDetails.type === 'VAST') {
+ Object.assign(bid, {
+ width: creativeBid.width,
+ height: creativeBid.height,
+ vastXml: creativeBid.creativeDetails.adContent,
+ cpm: (parseInt(creativeBid.cpm) * 0.65) / 1000000,
+ ttl: 3600
+ });
+ } else {
+ Object.assign(bid, {
+ width: creativeBid.width,
+ height: creativeBid.height,
+ cpm: (parseInt(creativeBid.cpm) * 0.6) / 1000000,
+ ad: creativeBid.creativeDetails.adContent
+ });
+ }
+
+ return bid;
+}
+/* Turn bid request sizes into compatible format */
+function getSizes(requestSizes) {
+ let width = 0;
+ let height = 0;
+ if (utils.isArray(requestSizes) && requestSizes.length === 2 &&
+ !utils.isArray(requestSizes[0])) {
+ width = parseInt(requestSizes[0], 10);
+ height = parseInt(requestSizes[1], 10);
+ } else if (typeof requestSizes === 'object') {
+ for (let i = 0; i < requestSizes.length; i++) {
+ let size = requestSizes[i];
+ width = parseInt(size[0], 10);
+ height = parseInt(size[1], 10);
+ break;
+ }
+ }
+ return [width, height];
+}
+
+function parseMediaType(creativeBid) {
+ const adType = creativeBid.creativeDetails.type;
+ if (adType === 'VAST') {
+ return VIDEO;
+ } else {
+ return BANNER;
+ }
+}
+
+registerBidder(spec);
diff --git a/modules/zedoBidAdapter.md b/modules/zedoBidAdapter.md
new file mode 100644
index 000000000000..9ffcd61f1643
--- /dev/null
+++ b/modules/zedoBidAdapter.md
@@ -0,0 +1,30 @@
+# Overview
+
+Module Name: ZEDO Bidder Adapter
+Module Type: Bidder Adapter
+Maintainer: prebidsupport@zedo.com
+
+# Description
+
+Module that connects to ZEDO's demand sources.
+
+For video integration, ZEDO returns content as vastXML and requires the publisher to define the cache url in config passed to Prebid for it to be valid in the auction
+
+# Test Parameters
+```
+ var adUnits = [
+ {
+ code: 'banner-ad-div',
+ sizes: [[300, 250], [728, 90]],
+ bids: [
+ {
+ bidder: 'zedo',
+ params: {
+ code: 2264004118
+ dimId: 9
+ }
+ }
+ ]
+ }
+ ];
+```
diff --git a/test/spec/modules/zedoBidAdapter_spec.js b/test/spec/modules/zedoBidAdapter_spec.js
new file mode 100644
index 000000000000..6d0ab7c68f63
--- /dev/null
+++ b/test/spec/modules/zedoBidAdapter_spec.js
@@ -0,0 +1,268 @@
+import { expect } from 'chai';
+import { spec } from 'modules/zedoBidAdapter';
+
+describe('The ZEDO bidding adapter', () => {
+ describe('isBidRequestValid', () => {
+ it('should return false when given an invalid bid', () => {
+ const bid = {
+ bidder: 'zedo',
+ };
+ const isValid = spec.isBidRequestValid(bid);
+ expect(isValid).to.equal(false);
+ });
+
+ it('should return true when given a channelcode bid', () => {
+ const bid = {
+ bidder: 'zedo',
+ params: {
+ channelCode: 20000000,
+ dimId: 9
+ },
+ };
+ const isValid = spec.isBidRequestValid(bid);
+ expect(isValid).to.equal(true);
+ });
+ });
+
+ describe('buildRequests', () => {
+ const bidderRequest = {
+ timeout: 3000,
+ };
+
+ it('should properly build a channelCode request for dim Id with type not defined', () => {
+ const bidRequests = [
+ {
+ bidder: 'zedo',
+ adUnitCode: 'p12345',
+ transactionId: '12345667',
+ sizes: [[300, 200]],
+ params: {
+ channelCode: 20000000,
+ dimId: 10
+ },
+ },
+ ];
+ const request = spec.buildRequests(bidRequests, bidderRequest);
+ expect(request.url).to.match(/^\/\/z2.zedo.com\/asw\/fmb.json/);
+ expect(request.method).to.equal('GET');
+ const zedoRequest = request.data;
+ expect(zedoRequest).to.equal('g={"placements":[{"network":20,"channel":0,"width":300,"height":200,"dimension":10,"version":"$prebid.version$","keyword":"","transactionId":"12345667","renderers":[{"name":"display"}]}]}');
+ });
+
+ it('should properly build a channelCode request for video with type defined', () => {
+ const bidRequests = [
+ {
+ bidder: 'zedo',
+ adUnitCode: 'p12345',
+ transactionId: '12345667',
+ sizes: [640, 480],
+ mediaTypes: {
+ video: {
+ context: 'instream',
+ },
+ },
+ params: {
+ channelCode: 20000000,
+ dimId: 85
+ },
+ },
+ ];
+ const request = spec.buildRequests(bidRequests, bidderRequest);
+ expect(request.url).to.match(/^\/\/z2.zedo.com\/asw\/fmb.json/);
+ expect(request.method).to.equal('GET');
+ const zedoRequest = request.data;
+ expect(zedoRequest).to.equal('g={"placements":[{"network":20,"channel":0,"width":640,"height":480,"dimension":85,"version":"$prebid.version$","keyword":"","transactionId":"12345667","renderers":[{"name":"Inarticle"}]}]}');
+ });
+
+ describe('buildGDPRRequests', () => {
+ let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==';
+ const bidderRequest = {
+ timeout: 3000,
+ gdprConsent: {
+ 'consentString': consentString,
+ 'gdprApplies': true
+ }
+ };
+
+ it('should properly build request with gdpr consent', () => {
+ const bidRequests = [
+ {
+ bidder: 'zedo',
+ adUnitCode: 'p12345',
+ transactionId: '12345667',
+ sizes: [[300, 200]],
+ params: {
+ channelCode: 20000000,
+ dimId: 10
+ },
+ },
+ ];
+ const request = spec.buildRequests(bidRequests, bidderRequest);
+ expect(request.method).to.equal('GET');
+ const zedoRequest = request.data;
+ expect(zedoRequest).to.equal('g={"placements":[{"network":20,"channel":0,"width":300,"height":200,"dimension":10,"version":"$prebid.version$","keyword":"","transactionId":"12345667","renderers":[{"name":"display"}]}],"gdpr":1,"gdpr_consent":"BOJ8RZsOJ8RZsABAB8AAAAAZ+A=="}');
+ });
+ });
+ });
+ describe('interpretResponse', () => {
+ it('should return an empty array when there is bid response', () => {
+ const response = {};
+ const request = { bidRequests: [] };
+ const bids = spec.interpretResponse(response, request);
+ expect(bids).to.have.lengthOf(0);
+ });
+
+ it('should properly parse a bid response with no valid creative', () => {
+ const response = {
+ body: {
+ ad: [
+ {
+ 'slotId': 'ad1d762',
+ 'network': '2000',
+ 'creatives': [
+ {
+ 'adId': '12345',
+ 'height': '600',
+ 'width': '160',
+ 'isFoc': true,
+ 'creativeDetails': {
+ 'type': 'StdBanner',
+ 'adContent': {
+ 'focImage': {
+ 'url': 'https://c13.zedo.com/OzoDB/0/0/0/blank.gif',
+ 'target': '_blank',
+ }
+ }
+ },
+ 'cpm': '0'
+ }
+ ]
+ }
+ ]
+ }
+ };
+ const request = {
+ bidRequests: [{
+ bidder: 'zedo',
+ adUnitCode: 'p12345',
+ bidId: 'test-bidId',
+ params: {
+ channelCode: 2000000,
+ dimId: 9
+ }
+ }]
+ };
+ const bids = spec.interpretResponse(response, request);
+ expect(bids).to.have.lengthOf(0);
+ });
+
+ it('should properly parse a bid response with valid display creative', () => {
+ const response = {
+ body: {
+ ad: [
+ {
+ 'slotId': 'ad1d762',
+ 'network': '2000',
+ 'creatives': [
+ {
+ 'adId': '12345',
+ 'height': '600',
+ 'width': '160',
+ 'isFoc': true,
+ 'creativeDetails': {
+ 'type': 'StdBanner',
+ 'adContent': ''
+ },
+ 'cpm': '1200000'
+ }
+ ]
+ }
+ ]
+ }
+ };
+ const request = {
+ bidRequests: [{
+ bidder: 'zedo',
+ adUnitCode: 'test-requestId',
+ bidId: 'test-bidId',
+ params: {
+ channelCode: 2000000,
+ dimId: 9
+ },
+ }]
+ };
+ const bids = spec.interpretResponse(response, request);
+ expect(bids).to.have.lengthOf(1);
+ expect(bids[0].requestId).to.equal('ad1d762');
+ expect(bids[0].cpm).to.equal(0.72);
+ expect(bids[0].width).to.equal('160');
+ expect(bids[0].height).to.equal('600');
+ });
+
+ it('should properly parse a bid response with valid video creative', () => {
+ const response = {
+ body: {
+ ad: [
+ {
+ 'slotId': 'ad1d762',
+ 'network': '2000',
+ 'creatives': [
+ {
+ 'adId': '12345',
+ 'height': '480',
+ 'width': '640',
+ 'isFoc': true,
+ 'creativeDetails': {
+ 'type': 'VAST',
+ 'adContent': ''
+ },
+ 'cpm': '1200000'
+ }
+ ]
+ }
+ ]
+ }
+ };
+ const request = {
+ bidRequests: [{
+ bidder: 'zedo',
+ adUnitCode: 'test-requestId',
+ bidId: 'test-bidId',
+ params: {
+ channelCode: 2000000,
+ dimId: 85
+ },
+ }]
+ };
+ const bids = spec.interpretResponse(response, request);
+ expect(bids).to.have.lengthOf(1);
+ expect(bids[0].requestId).to.equal('ad1d762');
+ expect(bids[0].cpm).to.equal(0.78);
+ expect(bids[0].width).to.equal('640');
+ expect(bids[0].height).to.equal('480');
+ expect(bids[0].vastXml).to.not.equal('');
+ expect(bids[0].ad).to.be.an('undefined');
+ });
+ });
+
+ describe('user sync', () => {
+ it('should register the iframe sync url', () => {
+ let syncs = spec.getUserSyncs({
+ iframeEnabled: true
+ });
+ expect(syncs).to.not.be.an('undefined');
+ expect(syncs).to.have.lengthOf(1);
+ expect(syncs[0].type).to.equal('iframe');
+ });
+
+ it('should pass gdpr params', () => {
+ let syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, {
+ gdprApplies: false, consentString: 'test'
+ });
+ expect(syncs).to.not.be.an('undefined');
+ expect(syncs).to.have.lengthOf(1);
+ expect(syncs[0].type).to.equal('iframe');
+ expect(syncs[0].url).to.contains('gdpr=0');
+ });
+ });
+});