From 6a4b80d6325aba7bbf3e58b3cecea8e85de0136a Mon Sep 17 00:00:00 2001 From: Chris Huie Date: Wed, 10 Aug 2022 11:01:14 -0700 Subject: [PATCH] ShowHeroes Bid Adapter: add new endpoint (#8816) * add ShowHeroes Adapter * ShowHeroes adapter - expanded outstream support * Revert "ShowHeroes adapter - expanded outstream support" This reverts commit bfcdb913b52012b5afbf95a84956b906518a4b51. * ShowHeroes adapter - expanded outstream support * ShowHeroes adapter - fixes (#4222) * ShowHeroes adapter - banner and outstream fixes (#4222) * ShowHeroes adapter - description and outstream changes (#4222) * ShowHeroes adapter - increase test coverage and small fix * ShowHeroes Adapter - naming convention issue * Mixed AdUnits declaration support * ITDEV-4723 PrebidJS adapter support with SupplyChain module object * ITDEV-4723 Fix tests * ITDEV-4723 New entry point * showheroes-bsBidAdapter: Add support for advertiserDomains * showheroes-bsBidAdapter: hotfix for outstream render * showheroes-bsBidAdapter: update renderer url * showheroes-bsBidAdapter: use only the necessary fields from the gdprConsent * ShowHeroes adapter - added a new endpoint * ShowHeroes adapter - unit tests * Update showheroes-bsBidAdapter.md * kick off tests Co-authored-by: Eldengrof Co-authored-by: veranevera Co-authored-by: Elizaveta Voziyanova <44549195+h2p4x8@users.noreply.github.com> --- modules/showheroes-bsBidAdapter.js | 178 +++++++++++++----- modules/showheroes-bsBidAdapter.md | 49 +++++ .../modules/showheroes-bsBidAdapter_spec.js | 175 ++++++++++++++++- 3 files changed, 352 insertions(+), 50 deletions(-) diff --git a/modules/showheroes-bsBidAdapter.js b/modules/showheroes-bsBidAdapter.js index c1987a32c80..98451ebbb2f 100644 --- a/modules/showheroes-bsBidAdapter.js +++ b/modules/showheroes-bsBidAdapter.js @@ -1,4 +1,11 @@ -import { deepAccess, getBidIdParameter, getWindowTop, logError } from '../src/utils.js'; +import { + deepAccess, + getBidIdParameter, + getWindowTop, + triggerPixel, + logInfo, + logError +} from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -7,6 +14,7 @@ import { loadExternalScript } from '../src/adloader.js'; const PROD_ENDPOINT = 'https://bs.showheroes.com/api/v1/bid'; const STAGE_ENDPOINT = 'https://bid-service.stage.showheroes.com/api/v1/bid'; +const VIRALIZE_ENDPOINT = 'https://ads.viralize.tv/prebid-sh/'; const PROD_PUBLISHER_TAG = 'https://static.showheroes.com/publishertag.js'; const STAGE_PUBLISHER_TAG = 'https://pubtag.stage.showheroes.com/publishertag.js'; const PROD_VL = 'https://video-library.showheroes.com'; @@ -26,12 +34,13 @@ export const spec = { aliases: ['showheroesBs'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function(bid) { - return !!bid.params.playerId; + return !!bid.params.playerId || !!bid.params.unitId; }, buildRequests: function(validBidRequests, bidderRequest) { let adUnits = []; - const pageURL = validBidRequests[0].params.contentPageUrl || bidderRequest.refererInfo.page; + const pageURL = validBidRequests[0].params.contentPageUrl || bidderRequest.refererInfo.referer; const isStage = !!validBidRequests[0].params.stage; + const isViralize = !!validBidRequests[0].params.unitId; const isOutstream = deepAccess(validBidRequests[0], 'mediaTypes.video.context') === 'outstream'; const isCustomRender = deepAccess(validBidRequests[0], 'params.outstreamOptions.customRender'); const isNodeRender = deepAccess(validBidRequests[0], 'params.outstreamOptions.slot') || deepAccess(validBidRequests[0], 'params.outstreamOptions.iframe'); @@ -40,12 +49,19 @@ export const spec = { const isBanner = !!validBidRequests[0].mediaTypes.banner || (isOutstream && !(isCustomRender || isNativeRender || isNodeRender)); const defaultSchain = validBidRequests[0].schain || {}; + const consentData = bidderRequest.gdprConsent || {}; + const gdprConsent = { + apiVersion: consentData.apiVersion || 2, + gdprApplies: consentData.gdprApplies || 0, + consentString: consentData.consentString || '', + } + validBidRequests.forEach((bid) => { const videoSizes = getVideoSizes(bid); const bannerSizes = getBannerSizes(bid); const vpaidMode = getBidIdParameter('vpaidMode', bid.params); - const makeBids = (type, size) => { + const makeBids = (type, size, isViralize) => { let context = ''; let streamType = 2; @@ -61,53 +77,79 @@ export const spec = { } } - const consentData = bidderRequest.gdprConsent || {}; - - const gdprConsent = { - apiVersion: consentData.apiVersion || 2, - gdprApplies: consentData.gdprApplies || 0, - consentString: consentData.consentString || '', - } - - return { + let rBid = { type: streamType, adUnitCode: bid.adUnitCode, bidId: bid.bidId, - mediaType: type, context: context, - playerId: getBidIdParameter('playerId', bid.params), auctionId: bidderRequest.auctionId, bidderCode: BIDDER_CODE, - gdprConsent: gdprConsent, start: +new Date(), timeout: 3000, - size: { - width: size[0], - height: size[1] - }, params: bid.params, - schain: bid.schain || defaultSchain, + schain: bid.schain || defaultSchain }; + + if (isViralize) { + rBid.unitId = getBidIdParameter('unitId', bid.params); + rBid.sizes = size; + rBid.mediaTypes = { + [type]: {'context': context} + }; + } else { + rBid.playerId = getBidIdParameter('playerId', bid.params); + rBid.mediaType = type; + rBid.size = { + width: size[0], + height: size[1] + }; + rBid.gdprConsent = gdprConsent; + } + + return rBid; }; - videoSizes.forEach((size) => { - adUnits.push(makeBids(VIDEO, size)); - }); + if (isViralize) { + if (videoSizes && videoSizes[0]) { + adUnits.push(makeBids(VIDEO, videoSizes, isViralize)); + } + if (bannerSizes && bannerSizes[0]) { + adUnits.push(makeBids(BANNER, bannerSizes, isViralize)); + } + } else { + videoSizes.forEach((size) => { + adUnits.push(makeBids(VIDEO, size)); + }); - bannerSizes.forEach((size) => { - adUnits.push(makeBids(BANNER, size)); - }); + bannerSizes.forEach((size) => { + adUnits.push(makeBids(BANNER, size)); + }); + } }); - return { - url: isStage ? STAGE_ENDPOINT : PROD_ENDPOINT, - method: 'POST', - options: {contentType: 'application/json', accept: 'application/json'}, - data: { + let endpointUrl; + let data; + + const QA = validBidRequests[0].params.qa || {}; + + if (isViralize) { + endpointUrl = VIRALIZE_ENDPOINT; + data = { + 'bidRequests': adUnits, + 'context': { + 'gdprConsent': gdprConsent, + 'schain': defaultSchain, + 'pageURL': QA.pageURL || encodeURIComponent(pageURL) + } + } + } else { + endpointUrl = isStage ? STAGE_ENDPOINT : PROD_ENDPOINT; + + data = { 'user': [], 'meta': { 'adapterVersion': 2, - 'pageURL': encodeURIComponent(pageURL), + 'pageURL': QA.pageURL || encodeURIComponent(pageURL), 'vastCacheEnabled': (!!config.getConfig('cache') && !isBanner && !outstreamOptions) || false, 'isDesktop': getWindowTop().document.documentElement.clientWidth > 700, 'xmlAndTag': !!(isOutstream && isCustomRender) || false, @@ -116,6 +158,13 @@ export const spec = { 'requests': adUnits, 'debug': validBidRequests[0].params.debug || false, } + } + + return { + url: QA.endpoint || endpointUrl, + method: 'POST', + options: {contentType: 'application/json', accept: 'application/json'}, + data: data }; }, interpretResponse: function(response, request) { @@ -149,33 +198,53 @@ export const spec = { } return syncs; }, + + onBidWon(bid) { + if (bid.callbacks) { + triggerPixel(bid.callbacks.won); + } + logInfo( + `Showheroes adapter won the auction. Bid id: ${bid.bidId || bid.requestId}` + ); + }, }; function createBids(bidRes, reqData) { - if (bidRes && (!Array.isArray(bidRes.bids) || bidRes.bids.length < 1)) { + if (!bidRes) { + return []; + } + const responseBids = bidRes.bids || bidRes.bidResponses; + if (!Array.isArray(responseBids) || responseBids.length < 1) { return []; } const bids = []; const bidMap = {}; - (reqData.requests || []).forEach((bid) => { + (reqData.requests || reqData.bidRequests || []).forEach((bid) => { bidMap[bid.bidId] = bid; }); - bidRes.bids.forEach(function (bid) { - const reqBid = bidMap[bid.bidId]; + responseBids.forEach(function (bid) { + const requestId = bid.bidId || bid.requestId; + const reqBid = bidMap[requestId]; const currentBidParams = reqBid.params; + const isViralize = !!reqBid.params.unitId; + const size = { + width: bid.width || bid.size.width, + height: bid.height || bid.size.height + }; + let bidUnit = {}; bidUnit.cpm = bid.cpm; - bidUnit.requestId = bid.bidId; + bidUnit.requestId = requestId; bidUnit.adUnitCode = reqBid.adUnitCode; bidUnit.currency = bid.currency; bidUnit.mediaType = bid.mediaType || VIDEO; bidUnit.ttl = TTL; - bidUnit.creativeId = 'c_' + bid.bidId; + bidUnit.creativeId = 'c_' + requestId; bidUnit.netRevenue = true; - bidUnit.width = bid.size.width; - bidUnit.height = bid.size.height; + bidUnit.width = size.width; + bidUnit.height = size.height; bidUnit.meta = { advertiserDomains: bid.adomain || [] }; @@ -185,24 +254,26 @@ function createBids(bidRes, reqData) { content: bid.vastXml, }; } - if (bid.vastTag) { - bidUnit.vastUrl = bid.vastTag; + if (bid.vastTag || bid.vastUrl) { + bidUnit.vastUrl = bid.vastTag || bid.vastUrl; } if (bid.mediaType === BANNER) { bidUnit.ad = getBannerHtml(bid, reqBid, reqData); } else if (bid.context === 'outstream') { const renderer = Renderer.install({ - id: bid.bidId, + id: requestId, url: 'https://static.showheroes.com/renderer.js', adUnitCode: reqBid.adUnitCode, config: { playerId: reqBid.playerId, - width: bid.size.width, - height: bid.size.height, + width: size.width, + height: size.height, vastUrl: bid.vastTag, vastXml: bid.vastXml, + ad: bid.ad, debug: reqData.debug, - isStage: !!reqData.meta.stage, + isStage: reqData.meta && !!reqData.meta.stage, + isViralize: isViralize, customRender: getBidIdParameter('customRender', currentBidParams.outstreamOptions), slot: getBidIdParameter('slot', currentBidParams.outstreamOptions), iframe: getBidIdParameter('iframe', currentBidParams.outstreamOptions), @@ -218,7 +289,12 @@ function createBids(bidRes, reqData) { } function outstreamRender(bid) { - const embedCode = createOutstreamEmbedCode(bid); + let embedCode; + if (bid.renderer.config.isViralize) { + embedCode = createOutstreamEmbedCodeV2(bid); + } else { + embedCode = createOutstreamEmbedCode(bid); + } if (typeof bid.renderer.config.customRender === 'function') { bid.renderer.config.customRender(bid, embedCode); } else { @@ -266,6 +342,12 @@ function createOutstreamEmbedCode(bid) { return fragment; } +function createOutstreamEmbedCodeV2(bid) { + const range = document.createRange(); + range.selectNode(document.getElementsByTagName('body')[0]); + return range.createContextualFragment(getBidIdParameter('ad', bid.renderer.config)); +} + function getBannerHtml (bid, reqBid, reqData) { const isStage = !!reqData.meta.stage; const urls = getEnvURLs(isStage); diff --git a/modules/showheroes-bsBidAdapter.md b/modules/showheroes-bsBidAdapter.md index cde652e9d83..a32a77a2525 100644 --- a/modules/showheroes-bsBidAdapter.md +++ b/modules/showheroes-bsBidAdapter.md @@ -125,3 +125,52 @@ Module that connects to ShowHeroes demand source to fetch bids. } ]; ``` + +# Test Parameters (V2) +``` + var adUnits = [ + { + code: 'video', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + } + }, + bids: [ + { + bidder: "showheroes-bs", + params: { + unitId: 'AACBWAcof-611K4U', + vpaidMode: true // by default is 'false' + } + } + ] + }, + { + code: 'video', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream', + } + }, + bids: [ + { + bidder: "showheroes-bs", + params: { + unitId: 'AACBTwsZVANd9NlB', + + outstreamOptions: { + // Required for the outstream renderer to exact node, one of + iframe: 'iframe_id', + // or + slot: 'slot_id' + } + } + } + ] + } + ]; +``` + diff --git a/test/spec/modules/showheroes-bsBidAdapter_spec.js b/test/spec/modules/showheroes-bsBidAdapter_spec.js index 69e8343dfc9..1aa7b132221 100644 --- a/test/spec/modules/showheroes-bsBidAdapter_spec.js +++ b/test/spec/modules/showheroes-bsBidAdapter_spec.js @@ -48,6 +48,18 @@ const bidRequestCommonParams = { 'auctionId': '43aa080090a47f', } +const bidRequestCommonParamsV2 = { + 'bidder': 'showheroes-bs', + 'params': { + 'unitId': 'AACBWAcof-611K4U', + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[640, 480]], + 'bidId': '38b373e1e31c18', + 'bidderRequestId': '12e3ade2543ba6', + 'auctionId': '43aa080090a47f', +} + const bidRequestVideo = { ...bidRequestCommonParams, ...{ @@ -72,6 +84,30 @@ const bidRequestOutstream = { } } +const bidRequestVideoV2 = { + ...bidRequestCommonParamsV2, + ...{ + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'instream', + } + } + } +} + +const bidRequestOutstreamV2 = { + ...bidRequestCommonParamsV2, + ...{ + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'outstream' + } + } + } +} + const bidRequestVideoVpaid = { ...bidRequestCommonParams, ...{ @@ -129,12 +165,19 @@ describe('shBidAdapter', function () { describe('isBidRequestValid', function () { it('should return true when required params found', function () { - const request = { + const requestV1 = { 'params': { 'playerId': '47427aa0-f11a-4d24-abca-1295a46a46cd', } } - expect(spec.isBidRequestValid(request)).to.equal(true) + expect(spec.isBidRequestValid(requestV1)).to.equal(true) + + const requestV2 = { + 'params': { + 'unitId': 'AACBTwsZVANd9NlB', + } + } + expect(spec.isBidRequestValid(requestV2)).to.equal(true) }) it('should return false when required params are not passed', function () { @@ -149,6 +192,9 @@ describe('shBidAdapter', function () { it('sends bid request to ENDPOINT via POST', function () { const request = spec.buildRequests([bidRequestVideo], bidderRequest) expect(request.method).to.equal('POST') + + const requestV2 = spec.buildRequests([bidRequestVideoV2], bidderRequest) + expect(requestV2.method).to.equal('POST') }) it('check sizes formats', function () { @@ -268,6 +314,30 @@ describe('shBidAdapter', function () { expect(payload2).to.have.property('type', 5); }) + it('should attach valid params to the payload when type is video (instream V2)', function () { + const request = spec.buildRequests([bidRequestVideoV2], bidderRequest) + const payload = request.data.bidRequests[0]; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('unitId', 'AACBWAcof-611K4U'); + expect(payload.mediaTypes).to.eql({ + [VIDEO]: { + 'context': 'instream' + } + }); + }) + + it('should attach valid params to the payload when type is video (outstream V2)', function () { + const request = spec.buildRequests([bidRequestOutstreamV2], bidderRequest) + const payload = request.data.bidRequests[0]; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('unitId', 'AACBWAcof-611K4U'); + expect(payload.mediaTypes).to.eql({ + [VIDEO]: { + 'context': 'outstream' + } + }); + }) + it('passes gdpr if present', function () { const request = spec.buildRequests([bidRequestVideo], {...bidderRequest, ...gdpr}) const payload = request.data.requests[0]; @@ -275,6 +345,13 @@ describe('shBidAdapter', function () { expect(payload.gdprConsent).to.eql(gdpr.gdprConsent) }) + it('passes gdpr if present (V2)', function () { + const request = spec.buildRequests([bidRequestVideoV2], {...bidderRequest, ...gdpr}) + const context = request.data.context; + expect(context).to.be.an('object'); + expect(context.gdprConsent).to.eql(gdpr.gdprConsent) + }) + it('passes schain object if present', function() { const request = spec.buildRequests([{ ...bidRequestVideo, @@ -284,6 +361,16 @@ describe('shBidAdapter', function () { expect(payload).to.be.an('object'); expect(payload.schain).to.eql(schain.schain); }) + + it('passes schain object if present (V2)', function() { + const request = spec.buildRequests([{ + ...bidRequestVideoV2, + ...schain + }], bidderRequest) + const context = request.data.context; + expect(context).to.be.an('object'); + expect(context.schain).to.eql(schain.schain); + }) }) describe('interpretResponse', function () { @@ -327,6 +414,39 @@ describe('shBidAdapter', function () { }], }; + const basicResponseV2 = { + 'requestId': '38b373e1e31c18', + 'adUnitCode': 'adunit-code-1', + 'cpm': 1, + 'currency': 'EUR', + 'width': 640, + 'height': 480, + 'advertiserDomain': [], + 'callbacks': { + 'won': ['https://api-n729.qa.viralize.com/track/?ver=15&session_id=01ecd03ce381505ccdeb88e555b05001&category=request_session&type=event&request_session_id=01ecd03ce381505ccdeb88e555b05001&label=prebid_won&reason=ok'] + }, + 'mediaType': 'video', + 'adomain': adomain, + }; + + const vastUrl = 'https://api-n729.qa.viralize.com/vast/?zid=AACBWAcof-611K4U&u=https://example.org/?foo=bar&gdpr=0&cs=XXXXXXXXXXXXXXXXXXXX&sid=01ecd03ce381505ccdeb88e555b05001&width=300&height=200&prebidmode=1' + + const responseVideoV2 = { + 'bidResponses': [{ + ...basicResponseV2, + 'context': 'instream', + 'vastUrl': vastUrl, + }], + }; + + const responseVideoOutstreamV2 = { + 'bidResponses': [{ + ...basicResponseV2, + 'context': 'outstream', + 'ad': '', + }], + }; + it('should get correct bid response when type is video', function () { const request = spec.buildRequests([bidRequestVideo], bidderRequest) const expectedResponse = [ @@ -356,6 +476,31 @@ describe('shBidAdapter', function () { expect(result).to.deep.equal(expectedResponse) }) + it('should get correct bid response when type is video (V2)', function () { + const request = spec.buildRequests([bidRequestVideoV2], bidderRequest) + const expectedResponse = [ + { + 'cpm': 1, + 'creativeId': 'c_38b373e1e31c18', + 'adUnitCode': 'adunit-code-1', + 'currency': 'EUR', + 'width': 640, + 'height': 480, + 'mediaType': 'video', + 'netRevenue': true, + 'vastUrl': vastUrl, + 'requestId': '38b373e1e31c18', + 'ttl': 300, + 'meta': { + 'advertiserDomains': adomain + } + } + ] + + const result = spec.interpretResponse({'body': responseVideoV2}, request) + expect(result).to.deep.equal(expectedResponse) + }) + it('should get correct bid response when type is banner', function () { const request = spec.buildRequests([bidRequestBanner], bidderRequest) @@ -396,6 +541,32 @@ describe('shBidAdapter', function () { expect(spots.length).to.equal(1) }) + it('should get correct bid response when type is outstream (slot V2)', function () { + const bidRequestV2 = JSON.parse(JSON.stringify(bidRequestOutstreamV2)); + const slotId = 'testSlot2' + bidRequestV2.params.outstreamOptions = { + slot: slotId + } + + const container = document.createElement('div') + container.setAttribute('id', slotId) + document.body.appendChild(container) + + const request = spec.buildRequests([bidRequestV2], bidderRequest) + + const result = spec.interpretResponse({'body': responseVideoOutstreamV2}, request) + const bid = result[0] + expect(bid).to.have.property('mediaType', VIDEO); + + const renderer = bid.renderer + expect(renderer).to.be.an('object') + expect(renderer.id).to.equal(bidRequestV2.bidId) + renderer.render(bid) + + const scripts = container.querySelectorAll('#testScript') + expect(scripts.length).to.equal(1) + }) + it('should get correct bid response when type is outstream (iframe)', function () { const bidRequest = JSON.parse(JSON.stringify(bidRequestOutstream)); const slotId = 'testIframe'