diff --git a/modules/zedoBidAdapter.js b/modules/zedoBidAdapter.js
new file mode 100644
index 00000000000..e75b9c82065
--- /dev/null
+++ b/modules/zedoBidAdapter.js
@@ -0,0 +1,342 @@
+import * as utils from '../src/utils.js';
+import { registerBidder } from '../src/adapters/bidderFactory.js';
+import { BANNER, VIDEO } from '../src/mediaTypes.js';
+import find from 'core-js-pure/features/array/find';
+import { Renderer } from '../src/Renderer.js';
+import { getRefererInfo } from '../src/refererDetection.js';
+
+const BIDDER_CODE = 'zedo';
+const SECURE_URL = 'https://saxp.zedo.com/asw/fmh.json';
+const DIM_TYPE = {
+ '7': 'display',
+ '9': 'display',
+ '14': 'display',
+ '70': 'SBR',
+ '83': 'CurtainRaiser',
+ '85': 'Inarticle',
+ '86': 'pswipeup',
+ '88': 'Inview',
+ '100': 'display',
+ '101': 'display',
+ '102': 'display',
+ '103': 'display'
+ // '85': 'pre-mid-post-roll',
+};
+const SECURE_EVENT_PIXEL_URL = 'tt1.zedo.com/log/p.gif';
+
+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,
+ publisher: bidRequest.params.pubId ? bidRequest.params.pubId : 0,
+ 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;
+ }
+ // Add CCPA consent string
+ if (bidderRequest && bidderRequest.uspConsent) {
+ data.usp = bidderRequest.uspConsent;
+ }
+
+ 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);
+ });
+ // adding schain object
+ if (bidRequests[0].schain) {
+ data['supplyChain'] = getSupplyChain(bidRequests[0].schain);
+ }
+ return {
+ method: 'GET',
+ url: SECURE_URL,
+ 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 = '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
+ }];
+ }
+ },
+
+ onTimeout: function (timeoutData) {
+ try {
+ logEvent('117', timeoutData);
+ } catch (e) {
+ utils.logError(e);
+ }
+ },
+
+ onBidWon: function (bid) {
+ try {
+ logEvent('116', [bid]);
+ } catch (e) {
+ utils.logError(e);
+ }
+ }
+
+};
+
+function getSupplyChain (supplyChain) {
+ return {
+ complete: supplyChain.complete,
+ nodes: supplyChain.nodes
+ }
+};
+
+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,
+ network: serverBid.network,
+ adType: creativeBid.creativeDetails.type,
+ 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.bidCpm) / 1000000,
+ ttl: 3600
+ });
+ const rendererOptions = utils.deepAccess(
+ bidderRequest,
+ 'renderer.options'
+ );
+ let rendererUrl = 'https://ss3.zedo.com/gecko/beta/fmpbgt.min.js';
+ Object.assign(bid, {
+ adResponse: serverBid,
+ renderer: getRenderer(bid.adUnitCode, serverBid.slotId, rendererUrl, rendererOptions)
+ });
+ } else {
+ Object.assign(bid, {
+ width: creativeBid.width,
+ height: creativeBid.height,
+ cpm: parseInt(creativeBid.bidCpm) / 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 getRenderer(adUnitCode, rendererId, rendererUrl, rendererOptions = {}) {
+ const renderer = Renderer.install({
+ id: rendererId,
+ url: rendererUrl,
+ config: rendererOptions,
+ loaded: false,
+ });
+
+ try {
+ renderer.setRender(videoRenderer);
+ } catch (err) {
+ utils.logWarn('Prebid Error calling setRender on renderer', err);
+ }
+
+ renderer.setEventHandlers({
+ impression: () => utils.logMessage('ZEDO video impression'),
+ loaded: () => utils.logMessage('ZEDO video loaded'),
+ ended: () => {
+ utils.logMessage('ZEDO renderer video ended');
+ document.querySelector(`#${adUnitCode}`).style.display = 'none';
+ }
+ });
+ return renderer;
+}
+
+function videoRenderer(bid) {
+ // push to render queue
+ const refererInfo = getRefererInfo();
+ let referrer = '';
+ if (refererInfo) {
+ referrer = refererInfo.referer;
+ }
+ bid.renderer.push(() => {
+ let channelCode = utils.deepAccess(bid, 'params.0.channelCode') || 0;
+ let dimId = utils.deepAccess(bid, 'params.0.dimId') || 0;
+ let publisher = utils.deepAccess(bid, 'params.0.pubId') || 0;
+ let options = utils.deepAccess(bid, 'params.0.options') || {};
+ let channel = (channelCode > 0) ? (channelCode - (bid.network * 1000000)) : 0;
+
+ var rndr = new window.ZdPBTag(bid.adUnitCode, bid.network, bid.width, bid.height, bid.adType, bid.vastXml, channel, dimId,
+ (encodeURI(referrer) || ''), options);
+ rndr.renderAd(publisher);
+ });
+}
+
+function parseMediaType(creativeBid) {
+ const adType = creativeBid.creativeDetails.type;
+ if (adType === 'VAST') {
+ return VIDEO;
+ } else {
+ return BANNER;
+ }
+}
+
+function logEvent(eid, data) {
+ let getParams = {
+ protocol: 'https',
+ hostname: SECURE_EVENT_PIXEL_URL,
+ search: getLoggingData(eid, data)
+ };
+ let eventUrl = utils.buildUrl(getParams).replace(/&/g, ';');
+ utils.triggerPixel(eventUrl);
+}
+
+function getLoggingData(eid, data) {
+ data = (utils.isArray(data) && data) || [];
+
+ let params = {};
+ let channel, network, dim, publisher, adunitCode, timeToRespond, cpm;
+ data.map((adunit) => {
+ adunitCode = adunit.adUnitCode;
+ channel = utils.deepAccess(adunit, 'params.0.channelCode') || 0;
+ network = channel > 0 ? parseInt(channel / 1000000) : 0;
+ dim = utils.deepAccess(adunit, 'params.0.dimId') * 256 || 0;
+ publisher = utils.deepAccess(adunit, 'params.0.pubId') || 0;
+ timeToRespond = adunit.timeout ? adunit.timeout : adunit.timeToRespond;
+ cpm = adunit.cpm;
+ });
+ let referrer = '';
+ const refererInfo = getRefererInfo();
+ if (refererInfo) {
+ referrer = refererInfo.referer;
+ }
+ params.n = network;
+ params.c = channel;
+ params.s = publisher;
+ params.x = dim;
+ params.ai = encodeURI('Prebid^zedo^' + adunitCode + '^' + cpm + '^' + timeToRespond);
+ params.pu = encodeURI(referrer) || '';
+ params.eid = eid;
+ params.e = 'e';
+ params.z = Math.random();
+
+ return params;
+}
+
+registerBidder(spec);
diff --git a/modules/zedoBidAdapter.md b/modules/zedoBidAdapter.md
index e0f9101deaa..2f31e8aed9b 100644
--- a/modules/zedoBidAdapter.md
+++ b/modules/zedoBidAdapter.md
@@ -18,22 +18,23 @@ ZEDO has its own renderer and will render the video unit if not defined in the c
# display
```
- var adUnits = [
- {
- code: 'banner-ad-div',
- sizes: [[300, 250], [728, 90]],
- bids: [
- {
- bidder: 'zedo',
- params: {
- channelCode: 2264004118, // required
- dimId: 9, // required
- pubId: 1 // optional
- }
+ var adUnits = [{
+ code: 'div-gpt-ad-1460505748561-0',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250], [300,600]],
}
- ]
- }
- ];
+ },
+ // Replace this object to test a new Adapter!
+ bids: [{
+ bidder: 'zedo',
+ params: {
+ channelCode: 2264004735, //REQUIRED
+ dimId:9 //REQUIRED
+ }
+ }]
+
+ }];
```
# video
```
@@ -54,7 +55,7 @@ ZEDO has its own renderer and will render the video unit if not defined in the c
bidder: 'zedo',
params:
{
- channelCode: 2264004593, // required
+ channelCode: 2264004735, // required
dimId: 85, // required
pubId: 1 // optional
}
diff --git a/test/spec/modules/zedoBidAdapter_spec.js b/test/spec/modules/zedoBidAdapter_spec.js
new file mode 100644
index 00000000000..8e5a789656e
--- /dev/null
+++ b/test/spec/modules/zedoBidAdapter_spec.js
@@ -0,0 +1,354 @@
+import { expect } from 'chai';
+import { spec } from 'modules/zedoBidAdapter';
+
+describe('The ZEDO bidding adapter', function () {
+ describe('isBidRequestValid', function () {
+ it('should return false when given an invalid bid', function () {
+ const bid = {
+ bidder: 'zedo',
+ };
+ const isValid = spec.isBidRequestValid(bid);
+ expect(isValid).to.equal(false);
+ });
+
+ it('should return true when given a channelcode bid', function () {
+ const bid = {
+ bidder: 'zedo',
+ params: {
+ channelCode: 20000000,
+ dimId: 9
+ },
+ };
+ const isValid = spec.isBidRequestValid(bid);
+ expect(isValid).to.equal(true);
+ });
+ });
+
+ describe('buildRequests', function () {
+ const bidderRequest = {
+ timeout: 3000,
+ };
+
+ it('should properly build a channelCode request for dim Id with type not defined', function () {
+ const bidRequests = [
+ {
+ bidder: 'zedo',
+ adUnitCode: 'p12345',
+ transactionId: '12345667',
+ sizes: [[300, 200]],
+ params: {
+ channelCode: 20000000,
+ dimId: 10,
+ pubId: 1
+ },
+ },
+ ];
+ const request = spec.buildRequests(bidRequests, bidderRequest);
+ expect(request.url).to.match(/^https:\/\/saxp.zedo.com\/asw\/fmh.json/);
+ expect(request.method).to.equal('GET');
+ const zedoRequest = request.data;
+ expect(zedoRequest).to.equal('g={"placements":[{"network":20,"channel":0,"publisher":1,"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', function () {
+ 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(/^https:\/\/saxp.zedo.com\/asw\/fmh.json/);
+ expect(request.method).to.equal('GET');
+ const zedoRequest = request.data;
+ expect(zedoRequest).to.equal('g={"placements":[{"network":20,"channel":0,"publisher":0,"width":640,"height":480,"dimension":85,"version":"$prebid.version$","keyword":"","transactionId":"12345667","renderers":[{"name":"Inarticle"}]}]}');
+ });
+
+ describe('buildGDPRRequests', function () {
+ let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==';
+ const bidderRequest = {
+ timeout: 3000,
+ gdprConsent: {
+ 'consentString': consentString,
+ 'gdprApplies': true
+ }
+ };
+
+ it('should properly build request with gdpr consent', function () {
+ 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,"publisher":0,"width":300,"height":200,"dimension":10,"version":"$prebid.version$","keyword":"","transactionId":"12345667","renderers":[{"name":"display"}]}],"gdpr":1,"gdpr_consent":"BOJ8RZsOJ8RZsABAB8AAAAAZ+A=="}');
+ });
+ });
+ });
+ describe('interpretResponse', function () {
+ it('should return an empty array when there is bid response', function () {
+ 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', function () {
+ 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', function () {
+ const response = {
+ body: {
+ ad: [
+ {
+ 'slotId': 'ad1d762',
+ 'network': '2000',
+ 'creatives': [
+ {
+ 'adId': '12345',
+ 'height': '600',
+ 'width': '160',
+ 'isFoc': true,
+ 'creativeDetails': {
+ 'type': 'StdBanner',
+ 'adContent': ''
+ },
+ 'bidCpm': '720000'
+ }
+ ]
+ }
+ ]
+ }
+ };
+ 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', function () {
+ const response = {
+ body: {
+ ad: [
+ {
+ 'slotId': 'ad1d762',
+ 'network': '2000',
+ 'creatives': [
+ {
+ 'adId': '12345',
+ 'height': '480',
+ 'width': '640',
+ 'isFoc': true,
+ 'creativeDetails': {
+ 'type': 'VAST',
+ 'adContent': ''
+ },
+ 'bidCpm': '780000'
+ }
+ ]
+ }
+ ]
+ }
+ };
+ 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].adType).to.equal('VAST');
+ expect(bids[0].vastXml).to.not.equal('');
+ expect(bids[0].ad).to.be.an('undefined');
+ expect(bids[0].renderer).not.to.be.an('undefined');
+ });
+ });
+
+ describe('user sync', function () {
+ it('should register the iframe sync url', function () {
+ 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', function () {
+ 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');
+ });
+ });
+
+ describe('bid events', function () {
+ it('should trigger a win pixel', function () {
+ const bid = {
+ 'bidderCode': 'zedo',
+ 'width': '300',
+ 'height': '250',
+ 'statusMessage': 'Bid available',
+ 'adId': '148018fe5e',
+ 'cpm': 0.5,
+ 'ad': 'dummy data',
+ 'ad_id': '12345',
+ 'sizeId': '15',
+ 'adResponse':
+ {
+ 'creatives': [
+ {
+ 'adId': '12345',
+ 'height': '480',
+ 'width': '640',
+ 'isFoc': true,
+ 'creativeDetails': {
+ 'type': 'VAST',
+ 'adContent': ''
+ },
+ 'seeder': {
+ 'network': 1234,
+ 'servedChan': 1234567,
+ },
+ 'cpm': '1200000',
+ 'servedChan': 1234,
+ }]
+ },
+ 'params': [{
+ 'channelCode': '123456',
+ 'dimId': '85'
+ }],
+ 'requestTimestamp': 1540401686,
+ 'responseTimestamp': 1540401687,
+ 'timeToRespond': 6253,
+ 'pbLg': '0.50',
+ 'pbMg': '0.50',
+ 'pbHg': '0.53',
+ 'adUnitCode': '/123456/header-bid-tag-0',
+ 'bidder': 'zedo',
+ 'size': '300x250',
+ 'adserverTargeting': {
+ 'hb_bidder': 'zedo',
+ 'hb_adid': '148018fe5e',
+ 'hb_pb': '10.00',
+ }
+ };
+ spec.onBidWon(bid);
+ spec.onTimeout(bid);
+ });
+ it('should trigger a timeout pixel', function () {
+ const bid = {
+ 'bidderCode': 'zedo',
+ 'width': '300',
+ 'height': '250',
+ 'statusMessage': 'Bid available',
+ 'adId': '148018fe5e',
+ 'cpm': 0.5,
+ 'ad': 'dummy data',
+ 'ad_id': '12345',
+ 'sizeId': '15',
+ 'params': [{
+ 'channelCode': '123456',
+ 'dimId': '85'
+ }],
+ 'timeout': 1,
+ 'requestTimestamp': 1540401686,
+ 'responseTimestamp': 1540401687,
+ 'timeToRespond': 6253,
+ 'adUnitCode': '/123456/header-bid-tag-0',
+ 'bidder': 'zedo',
+ 'size': '300x250',
+ };
+ spec.onBidWon(bid);
+ spec.onTimeout(bid);
+ });
+ });
+});