diff --git a/libraries/creative-renderer-display/renderer.js b/libraries/creative-renderer-display/renderer.js index 72f3658fe79..146afab46ae 100644 --- a/libraries/creative-renderer-display/renderer.js +++ b/libraries/creative-renderer-display/renderer.js @@ -1,2 +1,2 @@ // this file is autogenerated, see creative/README.md -export const RENDERER = "!function(){\"use strict\";window.render=function({ad:d,adUrl:i,width:n,height:e},{mkFrame:o},r){if(!d&&!i)throw{reason:\"noAd\",message:\"Missing ad markup or URL\"};{const t=r.document,s={width:n,height:e};i&&!d?s.src=i:s.srcdoc=d,t.body.appendChild(o(t,s))}}}();" \ No newline at end of file +export const RENDERER = "(()=>{\"use strict\";window.render=function({ad:d,adUrl:e,width:i,height:r},{mkFrame:n},o){if(!d&&!e)throw{reason:\"noAd\",message:\"Missing ad markup or URL\"};{const s=o.document,t={width:i,height:r};e&&!d?t.src=e:t.srcdoc=d,s.body.appendChild(n(s,t))}}})();" \ No newline at end of file diff --git a/libraries/creative-renderer-native/renderer.js b/libraries/creative-renderer-native/renderer.js index 57d86fc8ce3..d7d85cdd7ba 100644 --- a/libraries/creative-renderer-native/renderer.js +++ b/libraries/creative-renderer-native/renderer.js @@ -1,2 +1,2 @@ // this file is autogenerated, see creative/README.md -export const RENDERER = "!function(){\"use strict\";const e=\"Prebid Native\",t={title:\"text\",data:\"value\",img:\"url\",video:\"vasttag\"};function n(e,t){return new Promise(((n,r)=>{const i=t.createElement(\"script\");i.onload=n,i.onerror=r,i.src=e,t.body.appendChild(i)}))}function r(e,t,r,i,o=n){const{rendererUrl:s,assets:a,ortb:d,adTemplate:c}=t,l=i.document;return s?o(s,l).then((()=>{if(\"function\"!=typeof i.renderAd)throw new Error(`Renderer from '${s}' does not define renderAd()`);const e=a||[];return e.ortb=d,i.renderAd(e)})):Promise.resolve(r(c??l.body.innerHTML))}window.render=function({adId:n,native:i},{sendMessage:o},s,a=r){const{head:d,body:c}=s.document,l=()=>o(e,{action:\"resizeNativeHeight\",height:c.offsetHeight,width:c.offsetWidth}),u=function(e,{assets:n=[],ortb:r,nativeKeys:i={}}){const o=Object.fromEntries(n.map((({key:e,value:t})=>[e,t])));let s=Object.fromEntries(Object.entries(i).flatMap((([t,n])=>{const r=o.hasOwnProperty(t)?o[t]:void 0;return[[`##${n}##`,r],[`${n}:${e}`,r]]})));return r&&Object.assign(s,{\"##hb_native_linkurl##\":r.link?.url,\"##hb_native_privacy##\":r.privacy},Object.fromEntries((r.assets||[]).flatMap((e=>{const n=Object.keys(t).find((t=>e[t]));return[n&&[`##hb_native_asset_id_${e.id}##`,e[n][t[n]]],e.link?.url&&[`##hb_native_asset_link_id_${e.id}##`,e.link.url]].filter((e=>e))})))),s=Object.entries(s).concat([[/##hb_native_asset_(link_)?id_\\d+##/g]]),function(e){return s.reduce(((e,[t,n])=>e.replaceAll(t,n||\"\")),e)}}(n,i);return d&&(d.innerHTML=u(d.innerHTML)),a(n,i,u,s).then((t=>{c.innerHTML=t,\"function\"==typeof s.postRenderAd&&s.postRenderAd({adId:n,...i}),s.document.querySelectorAll(\".pb-click\").forEach((t=>{const n=t.getAttribute(\"hb_native_asset_id\");t.addEventListener(\"click\",(()=>o(e,{action:\"click\",assetId:n})))})),o(e,{action:\"fireNativeImpressionTrackers\"}),\"complete\"===s.document.readyState?l():s.onload=l}))}}();" \ No newline at end of file +export const RENDERER = "(()=>{\"use strict\";const e=\"Prebid Native\",t={title:\"text\",data:\"value\",img:\"url\",video:\"vasttag\"};function n(e,t){return new Promise(((n,r)=>{const i=t.createElement(\"script\");i.onload=n,i.onerror=r,i.src=e,t.body.appendChild(i)}))}function r(e,t,r,i,o=n){const{rendererUrl:s,assets:a,ortb:d,adTemplate:c}=t,l=i.document;return s?o(s,l).then((()=>{if(\"function\"!=typeof i.renderAd)throw new Error(`Renderer from '${s}' does not define renderAd()`);const e=a||[];return e.ortb=d,i.renderAd(e)})):Promise.resolve(r(c??l.body.innerHTML))}window.render=function({adId:n,native:i},{sendMessage:o},s,a=r){const{head:d,body:c}=s.document,l=()=>o(e,{action:\"resizeNativeHeight\",height:c.offsetHeight,width:c.offsetWidth}),u=function(e,{assets:n=[],ortb:r,nativeKeys:i={}}){const o=Object.fromEntries(n.map((({key:e,value:t})=>[e,t])));let s=Object.fromEntries(Object.entries(i).flatMap((([t,n])=>{const r=o.hasOwnProperty(t)?o[t]:void 0;return[[`##${n}##`,r],[`${n}:${e}`,r]]})));return r&&Object.assign(s,{\"##hb_native_linkurl##\":r.link?.url,\"##hb_native_privacy##\":r.privacy},Object.fromEntries((r.assets||[]).flatMap((e=>{const n=Object.keys(t).find((t=>e[t]));return[n&&[`##hb_native_asset_id_${e.id}##`,e[n][t[n]]],e.link?.url&&[`##hb_native_asset_link_id_${e.id}##`,e.link.url]].filter((e=>e))})))),s=Object.entries(s).concat([[/##hb_native_asset_(link_)?id_\\d+##/g]]),function(e){return s.reduce(((e,[t,n])=>e.replaceAll(t,n||\"\")),e)}}(n,i);return d&&(d.innerHTML=u(d.innerHTML)),a(n,i,u,s).then((t=>{c.innerHTML=t,\"function\"==typeof s.postRenderAd&&s.postRenderAd({adId:n,...i}),s.document.querySelectorAll(\".pb-click\").forEach((t=>{const n=t.getAttribute(\"hb_native_asset_id\");t.addEventListener(\"click\",(()=>o(e,{action:\"click\",assetId:n})))})),o(e,{action:\"fireNativeImpressionTrackers\"}),\"complete\"===s.document.readyState?l():s.onload=l}))}})();" \ No newline at end of file diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index c9da876b292..c945619c667 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -1,6 +1,6 @@ import { deepAccess, isEmpty } from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' -import { BANNER } from '../src/mediaTypes.js' +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js' import { getGlobal } from '../src/prebidGlobal.js' import { ortbConverter } from '../libraries/ortbConverter/converter.js' @@ -8,14 +8,16 @@ const converter = ortbConverter({ context: { // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false - ttl: 30 // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + ttl: 30, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) }, imp(buildImp, bidRequest, context) { - const imp = buildImp(bidRequest, context); + const imp = buildImp(bidRequest, context) imp.tagid = bidRequest.adUnitCode - return imp; - } -}); + if (imp.ext) imp.ext.placementId = bidRequest.params.placementId + + return imp + }, +}) const BIDDER_CODE = 'nativo' const BIDDER_ENDPOINT = 'https://exchange.postrelease.com/prebid' @@ -24,12 +26,22 @@ const GVLID = 263 const TIME_TO_LIVE = 360 -const SUPPORTED_AD_TYPES = [BANNER] +const SUPPORTED_AD_TYPES = [BANNER, VIDEO, NATIVE] const FLOOR_PRICE_CURRENCY = 'USD' const PRICE_FLOOR_WILDCARD = '*' const localPbjsRef = getGlobal() +function getMediaType(accessObj) { + if (deepAccess(accessObj, 'mediaTypes.video')) { + return VIDEO + } else if (deepAccess(accessObj, 'mediaTypes.native')) { + return NATIVE + } else { + return BANNER + } +} + /** * Keep track of bid data by keys * @returns {Object} - Map of bid data that can be referenced by multiple keys @@ -122,8 +134,7 @@ export const spec = { */ isBidRequestValid: function (bid) { // We don't need any specific parameters to make a bid request - // If not parameters are supplied just verify it's the correct bidder code - if (!bid.params) return bid.bidder === BIDDER_CODE + if (!bid.params) return true // Check if any supplied parameters are invalid const hasInvalidParameters = Object.keys(bid.params).some((key) => { @@ -150,7 +161,10 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { // Get OpenRTB Data - const openRTBData = converter.toORTB({bidRequests: validBidRequests, bidderRequest}) + const openRTBData = converter.toORTB({ + bidRequests: validBidRequests, + bidderRequest, + }) const openRTBDataString = JSON.stringify(openRTBData) const requestData = new RequestData() @@ -201,7 +215,8 @@ export const spec = { let params = [ // Prebid version { - key: 'ntv_pbv', value: localPbjsRef.version + key: 'ntv_pbv', + value: localPbjsRef.version, }, // Prebid request id { key: 'ntv_pb_rid', value: bidderRequest.bidderRequestId }, @@ -278,19 +293,31 @@ export const spec = { }) } + // Add GPP params + if (bidderRequest.gppConsent) { + params.unshift({ + key: 'ntv_gpp_consent', + value: bidderRequest.gppConsent.gppString, + }) + } + // Add USP params if (bidderRequest.uspConsent) { // Put on the beginning of the qs param array params.unshift({ key: 'us_privacy', value: bidderRequest.uspConsent }) } - const qsParamStrings = [requestData.getRequestDataQueryString(), arrayToQS(params)] + const qsParamStrings = [ + requestData.getRequestDataQueryString(), + arrayToQS(params), + ] const requestUrl = buildRequestUrl(BIDDER_ENDPOINT, qsParamStrings) let serverRequest = { method: 'POST', url: requestUrl, data: openRTBDataString, + bidderRequest: bidderRequest, } return serverRequest @@ -320,9 +347,10 @@ export const spec = { // Step through and grab pertinent data let bidResponse, adUnit - seatbids.forEach((seatbid) => { + seatbids.forEach((seatbid, i) => { seatbid.bid.forEach((bid) => { adUnit = this.getAdUnitData(body.id, bid) + bidResponse = { requestId: adUnit.bidId, cpm: bid.price, @@ -337,10 +365,18 @@ export const spec = { meta: { advertiserDomains: bid.adomain, }, + mediaType: getMediaType(request.bidderRequest.bids[i]), } if (bid.ext) extData[bid.id] = bid.ext - + if (bidResponse.mediaType === VIDEO) { + bidResponse.vastUrl = bid.adm + } + if (bidResponse.mediaType === NATIVE) { + bidResponse.native = { + ortb: JSON.parse(bidResponse.ad), + } + } bidResponses.push(bidResponse) }) }) @@ -414,23 +450,27 @@ export const spec = { typeof response.body === 'string' ? JSON.parse(response.body) : response.body - } catch (err) { return } + } catch (err) { + return + } // Make sure we have valid content if (!body || !body.seatbid || body.seatbid.length === 0) return body.seatbid.forEach((seatbid) => { // Grab the syncs for each seatbid - seatbid.syncUrls.forEach((sync) => { - if (types[sync.type]) { - if (sync.url.trim() !== '') { - syncs.push({ - type: sync.type, - url: sync.url.replace('{GDPR_params}', params), - }) + if (seatbid.syncUrls) { + seatbid.syncUrls.forEach((sync) => { + if (types[sync.type]) { + if (sync.url.trim() !== '') { + syncs.push({ + type: sync.type, + url: sync.url.replace('{GDPR_params}', params), + }) + } } - } - }) + }) + } }) }) @@ -491,7 +531,9 @@ export class RequestData { getRequestDataQueryString() { if (this.bidRequestDataSources.length == 0) return - const queryParams = this.bidRequestDataSources.map(dataSource => dataSource.getRequestQueryString()).filter(queryString => queryString !== '') + const queryParams = this.bidRequestDataSources + .map((dataSource) => dataSource.getRequestQueryString()) + .filter((queryString) => queryString !== '') return queryParams.join('&') } } @@ -500,8 +542,10 @@ export class BidRequestDataSource { constructor() { this.type = 'BidRequestDataSource' } - processBidRequestData(bidRequest, bidderRequest) { } - getRequestQueryString() { return '' } + processBidRequestData(bidRequest, bidderRequest) {} + getRequestQueryString() { + return '' + } } export class UserEIDs extends BidRequestDataSource { @@ -540,7 +584,7 @@ QueryStringParam.prototype.toString = function () { export function encodeToBase64(value) { try { return btoa(JSON.stringify(value)) - } catch (err) { } + } catch (err) {} } export function parseFloorPriceData(bidRequest) { @@ -708,9 +752,13 @@ function getLargestSize(sizes, method = area) { * Build the final request url */ export function buildRequestUrl(baseUrl, qsParamStringArray = []) { - if (qsParamStringArray.length === 0 || !Array.isArray(qsParamStringArray)) return baseUrl + if (qsParamStringArray.length === 0 || !Array.isArray(qsParamStringArray)) { + return baseUrl + } - const nonEmptyQSParamStrings = qsParamStringArray.filter(qsParamString => qsParamString.trim() !== '') + const nonEmptyQSParamStrings = qsParamStringArray.filter( + (qsParamString) => qsParamString.trim() !== '' + ) if (nonEmptyQSParamStrings.length === 0) return baseUrl @@ -752,7 +800,7 @@ export function getPageUrlFromBidRequest(bidRequest) { try { const url = new URL(paramPageUrl) return url.href - } catch (err) { } + } catch (err) {} } export function hasProtocol(url) { diff --git a/modules/nativoBidAdapter.md b/modules/nativoBidAdapter.md index f83fb45b52e..515d87af28e 100644 --- a/modules/nativoBidAdapter.md +++ b/modules/nativoBidAdapter.md @@ -16,24 +16,91 @@ gulp serve --modules=nativoBidAdapter # Test Parameters +## Banner + +```js +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: 'nativo', + params: { + url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html', + }, + }, + ], + }, +] ``` + +## Video + +```js 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: 'nativo', - params: { - url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html' - } - }] - - } - ]; + { + code: 'ntvPlaceholder-1', + mediaTypes: { + video: { + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + playbackmethod: [1, 2], + skip: 1, + skipafter: 5, + }, + }, + video: { + divId: 'player', + }, + bids: [ + { + bidder: 'nativo', + params: { + url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html', + }, + }, + ], + }, +] +``` + +## Native +```js +var adUnits = [ + { + code: '/416881364/prebid-native-test-unit', + sizes: [[300, 250]], + mediaTypes: { + native: { + title: { + required: true, + }, + image: { + required: true, + }, + sponsoredBy: { + required: true, + }, + }, + }, + bids: [ + { + bidder: 'nativo', + params: { + url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html', + }, + }, + ], + }, +] ``` diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 75fb357b196..349051cb48e 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -221,6 +221,7 @@ describe('interpretResponse', function () { meta: { advertiserDomains: ['test.com'], }, + mediaType: 'banner', }, ] @@ -681,16 +682,24 @@ describe('hasProtocol', () => { describe('addProtocol', () => { it('www.testpage.com', () => { - expect(addProtocol('www.testpage.com')).to.be.equal('https://www.testpage.com') + expect(addProtocol('www.testpage.com')).to.be.equal( + 'https://www.testpage.com' + ) }) it('//www.testpage.com', () => { - expect(addProtocol('//www.testpage.com')).to.be.equal('https://www.testpage.com') + expect(addProtocol('//www.testpage.com')).to.be.equal( + 'https://www.testpage.com' + ) }) it('http://www.testpage.com', () => { - expect(addProtocol('http://www.testpage.com')).to.be.equal('http://www.testpage.com') + expect(addProtocol('http://www.testpage.com')).to.be.equal( + 'http://www.testpage.com' + ) }) it('https://www.testpage.com', () => { - expect(addProtocol('https://www.testpage.com')).to.be.equal('https://www.testpage.com') + expect(addProtocol('https://www.testpage.com')).to.be.equal( + 'https://www.testpage.com' + ) }) }) @@ -786,7 +795,7 @@ describe('RequestData', () => { describe('UserEIDs', () => { const userEids = new UserEIDs() - const eids = [{ 'testId': 1111 }] + const eids = [{ testId: 1111 }] describe('processBidRequestData', () => { it('Processes bid request without eids', () => { @@ -810,7 +819,7 @@ describe('UserEIDs', () => { expect(qs).to.include('ntv_pb_eid=') try { expect(JSON.parse(value)).to.be.equal(eids) - } catch (err) { } + } catch (err) {} }) }) }) @@ -828,12 +837,83 @@ describe('buildRequestUrl', () => { }) it('Returns baseUrl + QS params if QS strings passed', () => { - const url = buildRequestUrl(baseUrl, ['ntv_ptd=123456&ntv_test=true', 'ntv_foo=bar']) - expect(url).to.be.equal(`${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar`) + const url = buildRequestUrl(baseUrl, [ + 'ntv_ptd=123456&ntv_test=true', + 'ntv_foo=bar', + ]) + expect(url).to.be.equal( + `${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar` + ) }) it('Returns baseUrl + QS params if mixed QS strings passed', () => { - const url = buildRequestUrl(baseUrl, ['ntv_ptd=123456&ntv_test=true', '', '', 'ntv_foo=bar']) - expect(url).to.be.equal(`${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar`) + const url = buildRequestUrl(baseUrl, [ + 'ntv_ptd=123456&ntv_test=true', + '', + '', + 'ntv_foo=bar', + ]) + expect(url).to.be.equal( + `${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar` + ) + }) +}) + +describe('Prebid Video', function () { + it('should handle video bid requests', function () { + const videoBidRequest = { + bidder: 'nativo', + params: { + video: { + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + playbackmethod: [1, 2], + skip: 1, + skipafter: 5, + }, + }, + } + + const isValid = spec.isBidRequestValid(videoBidRequest) + expect(isValid).to.be.true + }) +}) + +describe('Prebid Native', function () { + it('should handle native bid requests', function () { + const nativeBidRequest = { + bidder: 'nativo', + params: { + native: { + title: { + required: true, + len: 80, + }, + image: { + required: true, + sizes: [150, 50], + }, + sponsoredBy: { + required: true, + }, + clickUrl: { + required: true, + }, + privacyLink: { + required: false, + }, + body: { + required: true, + }, + icon: { + required: true, + sizes: [50, 50], + }, + }, + }, + } + + const isValid = spec.isBidRequestValid(nativeBidRequest) + expect(isValid).to.be.true }) })