diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 46647b31c5..695929a12f 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -1,6 +1,6 @@ import { registerBidder } from "../src/adapters/bidderFactory.js"; import { getStorageManager } from "../src/storageManager.js"; -import { BANNER } from "../src/mediaTypes.js"; +import { BANNER, VIDEO } from "../src/mediaTypes.js"; import { getParameterByName, isNumber, @@ -22,6 +22,7 @@ import { getAdUnitElement } from '../src/utils/adUnits.js'; // ------------------------------------ const BIDDER_CODE = "cwire"; const CWID_KEY = "cw_cwid"; +const VAST_XML_REGEX = /^\s*(?:<\?xml[^>]*>\s*)?]/i; export const BID_ENDPOINT = "https://prebid.cwi.re/v1/bid"; export const EVENT_ENDPOINT = "https://prebid.cwi.re/v1/event"; @@ -169,10 +170,68 @@ function getCwExtension() { }; } +function getBidRequest(responseBid, request) { + return request?.bids?.find((bid) => ( + bid.bidId === responseBid.requestId || + bid.bidId === responseBid.bidId + )); +} + +function isVastXml(value) { + return typeof value === "string" && VAST_XML_REGEX.test(value); +} + +function isVideoResponse(responseBid, requestBid) { + const hasVast = ( + responseBid.vastXml || + responseBid.vastXML || + responseBid.vastUrl || + responseBid.vastURL || + isVastXml(responseBid.html) + ); + const hasVideoMediaType = ( + responseBid.mediaType === VIDEO || + responseBid.adType === VIDEO || + responseBid.type === VIDEO + ); + const requestSupportsVideo = !!requestBid?.mediaTypes?.video; + + return ( + hasVast || + (requestSupportsVideo && (hasVideoMediaType || !requestBid?.mediaTypes?.banner)) + ); +} + +function mapBidResponse(responseBid, request) { + const { html, vastXml, vastXML, vast, vastUrl, vastURL, ...rest } = responseBid; + const requestBid = getBidRequest(responseBid, request); + + if (isVideoResponse(responseBid, requestBid)) { + const vastXmlResponse = vastXml || vastXML || vast || (isVastXml(html) ? html : null); + const vastUrlResponse = vastUrl || vastURL; + + if (!vastXmlResponse && !vastUrlResponse) { + return null; + } + + return { + ...rest, + mediaType: VIDEO, + ...(vastXmlResponse && { vastXml: vastXmlResponse }), + ...(vastUrlResponse && { vastUrl: vastUrlResponse }), + }; + } + + return { + ...rest, + ad: html, + }; +} + export const spec = { code: BIDDER_CODE, gvlid: GVL_ID, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether the given bid request is valid. @@ -255,6 +314,7 @@ export const spec = { method: "POST", url: BID_ENDPOINT, data: payloadString, + bids: validBidRequests, }; }, /** @@ -271,12 +331,11 @@ export const spec = { } } - // Rename `html` response property to `ad` as used by prebid. - const bids = serverResponse.body?.bids.map(({ html, ...rest }) => ({ - ...rest, - ad: html, - })); - return bids || []; + // Rename banner `html` to `ad`; video responses must expose VAST. + const bids = (serverResponse.body?.bids || []) + .map((bid) => mapBidResponse(bid, bidRequest)) + .filter(Boolean); + return bids; }, onBidWon: function (bid) { diff --git a/modules/cwireBidAdapter.md b/modules/cwireBidAdapter.md index 1d4f3c039c..90e49db42c 100644 --- a/modules/cwireBidAdapter.md +++ b/modules/cwireBidAdapter.md @@ -27,6 +27,8 @@ Below, the list of C-WIRE params and where they can be set. ### adUnit configuration +#### Banner + ```javascript var adUnits = [ { @@ -65,6 +67,36 @@ var adUnits = [ ]; ``` +#### Video + +```javascript +var adUnits = [ + { + code: 'video_target_div_id', // REQUIRED + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + startdelay: 0, + placement: 1, + playbackmethod: [2], + api: [2], + linearity: 1 + } + }, + bids: [{ + bidder: 'cwire', + params: { + domainId: 1422, // required - number + placementId: 2211521, // optional - number + } + }] + } +]; +``` + ### URL parameters For debugging and testing purposes url parameters can be set. diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 6943618cf5..2ce256a37d 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -7,6 +7,7 @@ import sinon, { stub } from "sinon"; import { config } from "../../../src/config.js"; import * as autoplayLib from "../../../libraries/autoplayDetection/autoplay.js"; import * as adUnits from 'src/utils/adUnits'; +import { BANNER, VIDEO } from "../../../src/mediaTypes.js"; describe("C-WIRE bid adapter", () => { config.setConfig({ debug: true }); @@ -53,6 +54,31 @@ describe("C-WIRE bid adapter", () => { ], }, }; + const vastXml = ''; + const videoBidRequest = { + bidder: "cwire", + params: { + domainId: 4057, + }, + adUnitCode: "video-adunit-code", + mediaTypes: { + video: { + context: "instream", + playerSize: [640, 480], + mimes: ["video/mp4"], + protocols: [2, 3, 5, 6], + startdelay: 0, + placement: 1, + playbackmethod: [2], + api: [2], + linearity: 1, + }, + }, + bidId: "video-bid-id", + bidderRequestId: "video-request-id", + auctionId: "video-auction-id", + transactionId: "video-transaction-id", + }; beforeEach(function () { sandbox = sinon.createSandbox(); @@ -68,6 +94,10 @@ describe("C-WIRE bid adapter", () => { expect(spec.buildRequests).to.exist.and.to.be.a("function"); expect(spec.interpretResponse).to.exist.and.to.be.a("function"); }); + + it("supports banner and video media types", function () { + expect(spec.supportedMediaTypes).to.include.members([BANNER, VIDEO]); + }); }); describe("buildRequests", function () { it("sends bid request to ENDPOINT via POST", function () { @@ -75,6 +105,14 @@ describe("C-WIRE bid adapter", () => { expect(request.url).to.equal(BID_ENDPOINT); expect(request.method).to.equal("POST"); }); + + it("passes video mediaTypes to the bid endpoint", function () { + const request = spec.buildRequests([videoBidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.slots[0].mediaTypes.video).to.deep.equal(videoBidRequest.mediaTypes.video); + expect(request.bids[0]).to.deep.equal(videoBidRequest); + }); }); describe("buildRequests with given creative", function () { let utilsStub; @@ -318,6 +356,109 @@ describe("C-WIRE bid adapter", () => { expect(bids[0].ad).to.exist; }); + + it("keeps banner responses on the banner path even if a custom type field is present", function () { + const bidResponse = deepClone(response); + bidResponse.body.bids[0].type = VIDEO; + const bids = spec.interpretResponse(bidResponse, { bids: bidRequests }); + + expect(bids[0].ad).to.equal("

Hello world

"); + expect(bids[0].mediaType).to.not.equal(VIDEO); + }); + + it("maps VAST XML responses to video bids", function () { + const bidResponse = { + body: { + bids: [ + { + cpm: 5, + currency: "USD", + dimensions: [640, 480], + netRevenue: true, + creativeId: "video-creative", + requestId: videoBidRequest.bidId, + ttl: 360, + vastXml, + }, + ], + }, + }; + const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); + + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].vastXml).to.equal(vastXml); + expect(bids[0].ad).to.not.exist; + }); + + it("maps VAST URL responses to video bids", function () { + const vastUrl = "https://prebid.cwi.re/vast/video-ad.xml"; + const bidResponse = { + body: { + bids: [ + { + cpm: 5, + currency: "USD", + dimensions: [640, 480], + netRevenue: true, + creativeId: "video-creative", + requestId: videoBidRequest.bidId, + ttl: 360, + vastUrl, + }, + ], + }, + }; + const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); + + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].vastUrl).to.equal(vastUrl); + }); + + it("uses VAST XML from html when responding to a video-only request", function () { + const bidResponse = { + body: { + bids: [ + { + html: vastXml, + cpm: 5, + currency: "USD", + dimensions: [640, 480], + netRevenue: true, + creativeId: "video-creative", + requestId: videoBidRequest.bidId, + ttl: 360, + }, + ], + }, + }; + const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); + + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].vastXml).to.equal(vastXml); + expect(bids[0].ad).to.not.exist; + }); + + it("drops video responses that do not include VAST", function () { + const bidResponse = { + body: { + bids: [ + { + html: "

Hello world

", + cpm: 5, + currency: "USD", + dimensions: [640, 480], + netRevenue: true, + creativeId: "video-creative", + requestId: videoBidRequest.bidId, + ttl: 360, + }, + ], + }, + }; + const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); + + expect(bids).to.be.empty; + }); }); describe("add user-syncs", function () {