From 07962d64170f79ee0945ba1a1e747228fb674bd6 Mon Sep 17 00:00:00 2001 From: Quentin Gallard Date: Tue, 30 Jan 2024 14:16:21 +0100 Subject: [PATCH] SmileWanted - Add Video Instream, Video Outstream and Native support (#10996) Co-authored-by: QuentinGallard --- modules/smilewantedBidAdapter.js | 116 ++++++++--- .../modules/smilewantedBidAdapter_spec.js | 186 +++++++++++++++++- 2 files changed, 272 insertions(+), 30 deletions(-) diff --git a/modules/smilewantedBidAdapter.js b/modules/smilewantedBidAdapter.js index 46584e54373..515aae0e092 100644 --- a/modules/smilewantedBidAdapter.js +++ b/modules/smilewantedBidAdapter.js @@ -1,8 +1,12 @@ -import { isArray, logError, logWarn, isFn, isPlainObject } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {deepAccess, deepClone, isArray, isFn, isPlainObject, logError, logWarn} from '../src/utils.js'; +import {Renderer} from '../src/Renderer.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {convertOrtbRequestToProprietaryNative, toOrtbNativeRequest, toLegacyResponse} from '../src/native.js'; + +const BIDDER_CODE = 'smilewanted'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -12,29 +16,50 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; const GVL_ID = 639; export const spec = { - code: 'smilewanted', - aliases: ['smile', 'sw'], + code: BIDDER_CODE, gvlid: GVL_ID, - supportedMediaTypes: [BANNER, VIDEO], + aliases: ['smile', 'sw'], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. * - * @param {object} bid The bid to validate. + * @param {BidRequest} 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.zoneId); + if (!bid.params || !bid.params.zoneId) { + return false; + } + + if (deepAccess(bid, 'mediaTypes.video')) { + const videoMediaTypesParams = deepAccess(bid, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bid, 'params.video', {}); + + const videoParams = { + ...videoMediaTypesParams, + ...videoBidderParams + }; + + if (!videoParams.context || ![INSTREAM, OUTSTREAM].includes(videoParams.context)) { + return false; + } + } + + return true; }, /** * Make a server request from the list of BidRequests. * * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: function(validBidRequests, bidderRequest) { + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + return validBidRequests.map(bid => { - var payload = { + const payload = { zoneId: bid.params.zoneId, currencyCode: config.getConfig('currency.adServerCurrency') || 'EUR', tagId: bid.adUnitCode, @@ -65,20 +90,41 @@ export const spec = { payload.bidfloor = bid.params.bidfloor; } - if (bidderRequest && bidderRequest.refererInfo) { + if (bidderRequest?.refererInfo) { payload.pageDomain = bidderRequest.refererInfo.page || ''; } - if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest?.gdprConsent) { payload.gdpr_consent = bidderRequest.gdprConsent.consentString; payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side } - if (bid && bid.userIdAsEids) { - payload.eids = bid.userIdAsEids; + payload.eids = bid?.userIdAsEids; + + const videoMediaType = deepAccess(bid, 'mediaTypes.video'); + const context = deepAccess(bid, 'mediaTypes.video.context'); + + if (bid.mediaType === 'video' || (videoMediaType && context === INSTREAM) || (videoMediaType && context === OUTSTREAM)) { + payload.context = context; + payload.videoParams = deepClone(videoMediaType); } - var payloadString = JSON.stringify(payload); + const nativeMediaType = deepAccess(bid, 'mediaTypes.native'); + + if (nativeMediaType) { + payload.context = 'native'; + payload.nativeParams = nativeMediaType; + let sizes = deepAccess(bid, 'mediaTypes.native.image.sizes', []); + + if (sizes.length > 0) { + const size = Array.isArray(sizes[0]) ? sizes[0] : sizes; + + payload.width = size[0] || payload.width; + payload.height = size[1] || payload.height; + } + } + + const payloadString = JSON.stringify(payload); return { method: 'POST', url: 'https://prebid.smilewanted.com', @@ -90,18 +136,21 @@ export const spec = { /** * Unpack the response from the server into a list of bids. * - * @param {*} serverResponse A successful response from the server. + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse.body) return []; const bidResponses = []; - var response = serverResponse.body; try { + const response = serverResponse.body; + const bidRequestData = JSON.parse(bidRequest.data); if (response) { const dealId = response.dealId || ''; const bidResponse = { - requestId: JSON.parse(bidRequest.data).bidId, + requestId: bidRequestData.bidId, cpm: response.cpm, width: response.width, height: response.height, @@ -113,14 +162,21 @@ export const spec = { ad: response.ad, }; - if (response.formatTypeSw == 'video_instream' || response.formatTypeSw == 'video_outstream') { + if (response.formatTypeSw === 'video_instream' || response.formatTypeSw === 'video_outstream') { bidResponse['mediaType'] = 'video'; bidResponse['vastUrl'] = response.ad; bidResponse['ad'] = null; + + if (response.formatTypeSw === 'video_outstream') { + bidResponse['renderer'] = newRenderer(bidRequestData, response); + } } - if (response.formatTypeSw == 'video_outstream') { - bidResponse['renderer'] = newRenderer(JSON.parse(bidRequest.data), response); + if (response.formatTypeSw === 'native') { + const nativeAdResponse = JSON.parse(response.ad); + const ortbNativeRequest = toOrtbNativeRequest(bidRequestData.nativeParams); + bidResponse['mediaType'] = 'native'; + bidResponse['native'] = toLegacyResponse(nativeAdResponse, ortbNativeRequest); } if (dealId.length > 0) { @@ -128,7 +184,7 @@ export const spec = { } bidResponse.meta = {}; - if (response.meta && response.meta.advertiserDomains && isArray(response.meta.advertiserDomains)) { + if (response.meta?.advertiserDomains && isArray(response.meta.advertiserDomains)) { bidResponse.meta.advertiserDomains = response.meta.advertiserDomains; } bidResponses.push(bidResponse); @@ -136,15 +192,18 @@ export const spec = { } catch (error) { logError('Error while parsing smilewanted response', error); } + return bidResponses; }, /** - * User syncs. + * Register the user sync pixels which should be dropped after the auction. * - * @param {*} syncOptions Publisher prebid configuration. - * @param {*} serverResponses A successful response from the server. - * @return {Syncs[]} An array of syncs that should be executed. + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} responses List of server's responses. + * @param {Object} gdprConsent The GDPR consent parameters + * @param {Object} uspConsent The USP consent parameters + * @return {UserSync[]} The user syncs which should be dropped. */ getUserSyncs: function(syncOptions, responses, gdprConsent, uspConsent) { let params = ''; @@ -177,7 +236,8 @@ export const spec = { /** * Create SmileWanted renderer - * @param requestId + * @param bidRequest + * @param bidResponse * @returns {*} */ function newRenderer(bidRequest, bidResponse) { diff --git a/test/spec/modules/smilewantedBidAdapter_spec.js b/test/spec/modules/smilewantedBidAdapter_spec.js index 22221dbe1ef..99c4034610f 100644 --- a/test/spec/modules/smilewantedBidAdapter_spec.js +++ b/test/spec/modules/smilewantedBidAdapter_spec.js @@ -93,7 +93,24 @@ const BID_RESPONSE_DISPLAY = { const VIDEO_INSTREAM_REQUEST = [{ code: 'video1', mediaTypes: { - video: {} + video: { + context: 'instream', + mimes: ['video/mp4'], + minduration: 0, + maxduration: 120, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + startdelay: 0, + placement: 1, + skip: 1, + skipafter: 10, + minbitrate: 10, + maxbitrate: 10, + delivery: [1], + playbackmethod: [2], + api: [1, 2], + linearity: 1, + playerSize: [640, 480] + } }, sizes: [ [640, 480] @@ -163,6 +180,99 @@ const BID_RESPONSE_VIDEO_OUTSTREAM = { } }; +const NATIVE_REQUEST = [{ + adUnitCode: 'native_300x250', + code: '/19968336/prebid_native_example_1', + bidId: '12345', + sizes: [ + [300, 250] + ], + mediaTypes: { + native: { + sendTargetingKeys: false, + title: { + required: true, + len: 140 + }, + image: { + required: true, + sizes: [300, 250] + }, + icon: { + required: false, + sizes: [50, 50] + }, + sponsoredBy: { + required: true + }, + body: { + required: true + }, + clickUrl: { + required: false + }, + privacyLink: { + required: false + }, + cta: { + required: false + }, + rating: { + required: false + }, + likes: { + required: false + }, + downloads: { + required: false + }, + price: { + required: false + }, + salePrice: { + required: false + }, + phone: { + required: false + }, + address: { + required: false + }, + desc2: { + required: false + }, + displayUrl: { + required: false + } + } + }, + bidder: 'smilewanted', + params: { + zoneId: 4, + }, + requestId: 'request_abcd1234', + ortb2Imp: { + ext: { + tid: 'trans_abcd1234', + } + }, +}]; + +const BID_RESPONSE_NATIVE = { + body: { + cpm: 3, + width: 300, + height: 250, + creativeId: 'crea_sw_1', + currency: 'EUR', + isNetCpm: true, + ttl: 300, + ad: '{"link":{"url":"https://www.smilewanted.com"},"assets":[{"id":0,"required":1,"title":{"len":50}},{"id":1,"required":1,"img":{"type":3,"w":150,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":2,"required":0,"img":{"type":1,"w":50,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":3,"required":1,"data":{"type":1,"value":"Smilewanted sponsor"}},{"id":4,"required":1,"data":{"type":2,"value":"Smilewanted Description"}}]}', + cSyncUrl: 'https://csync.smilewanted.com', + formatTypeSw: 'native' + } +}; + // Default params with optional ones describe('smilewantedBidAdapterTests', function () { it('SmileWanted - Verify build request', function () { @@ -195,6 +305,23 @@ describe('smilewantedBidAdapterTests', function () { expect(requestVideoInstreamContent.sizes[0]).to.have.property('w').and.to.equal(640); expect(requestVideoInstreamContent.sizes[0]).to.have.property('h').and.to.equal(480); expect(requestVideoInstreamContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestVideoInstreamContent).to.have.property('videoParams'); + expect(requestVideoInstreamContent.videoParams).to.have.property('context').and.to.equal('instream').and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('mimes').to.be.an('array').that.include('video/mp4').and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('minduration').and.to.equal(0).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('maxduration').and.to.equal(120).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('protocols').to.be.an('array').that.include.members([1, 2, 3, 4, 5, 6, 7, 8]).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('startdelay').and.to.equal(0).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('placement').and.to.equal(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('skip').and.to.equal(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('skipafter').and.to.equal(10).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('minbitrate').and.to.equal(10).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('maxbitrate').and.to.equal(10).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('delivery').to.be.an('array').that.include(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('playbackmethod').to.be.an('array').that.include(2).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('api').to.be.an('array').that.include.members([1, 2]).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('linearity').and.to.equal(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('playerSize').to.be.an('array').that.include.members([640, 480]).and.to.not.be.undefined; const requestVideoOutstream = spec.buildRequests(VIDEO_OUTSTREAM_REQUEST); expect(requestVideoOutstream[0]).to.have.property('url').and.to.equal('https://prebid.smilewanted.com'); @@ -206,6 +333,39 @@ describe('smilewantedBidAdapterTests', function () { expect(requestVideoOutstreamContent.sizes[0]).to.have.property('w').and.to.equal(640); expect(requestVideoOutstreamContent.sizes[0]).to.have.property('h').and.to.equal(480); expect(requestVideoOutstreamContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + + const requestNative = spec.buildRequests(NATIVE_REQUEST); + expect(requestNative[0]).to.have.property('url').and.to.equal('https://prebid.smilewanted.com'); + expect(requestNative[0]).to.have.property('method').and.to.equal('POST'); + const requestNativeContent = JSON.parse(requestNative[0].data); + expect(requestNativeContent).to.have.property('zoneId').and.to.equal(4); + expect(requestNativeContent).to.have.property('currencyCode').and.to.equal('EUR'); + expect(requestNativeContent).to.have.property('sizes'); + expect(requestNativeContent.sizes[0]).to.have.property('w').and.to.equal(300); + expect(requestNativeContent.sizes[0]).to.have.property('h').and.to.equal(250); + expect(requestNativeContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestNativeContent).to.have.property('context').and.to.equal('native').and.to.not.be.undefined; + expect(requestNativeContent).to.have.property('nativeParams'); + expect(requestNativeContent.nativeParams.title).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.title).to.have.property('len').and.to.equal(140); + expect(requestNativeContent.nativeParams.image).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.image).to.have.property('sizes').to.be.an('array').that.include.members([300, 250]).and.to.not.be.undefined; + expect(requestNativeContent.nativeParams.icon).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.icon).to.have.property('sizes').to.be.an('array').that.include.members([50, 50]).and.to.not.be.undefined; + expect(requestNativeContent.nativeParams.sponsoredBy).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.body).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.clickUrl).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.privacyLink).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.cta).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.rating).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.likes).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.downloads).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.price).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.salePrice).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.phone).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.address).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.desc2).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.displayUrl).to.have.property('required').and.to.equal(false); }); it('SmileWanted - Verify build request with referrer', function () { @@ -337,7 +497,7 @@ describe('smilewantedBidAdapterTests', function () { }).to.not.throw(); }); - it('SmileWanted - Verify parse response - Video Oustream', function () { + it('SmileWanted - Verify parse response - Video Outstream', function () { const request = spec.buildRequests(VIDEO_OUTSTREAM_REQUEST); const bids = spec.interpretResponse(BID_RESPONSE_VIDEO_OUTSTREAM, request[0]); expect(bids).to.have.lengthOf(1); @@ -360,6 +520,28 @@ describe('smilewantedBidAdapterTests', function () { }).to.not.throw(); }); + it('SmileWanted - Verify parse response - Native', function () { + const request = spec.buildRequests(NATIVE_REQUEST); + const bids = spec.interpretResponse(BID_RESPONSE_NATIVE, request[0]); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(3); + expect(bid.ad).to.equal('{"link":{"url":"https://www.smilewanted.com"},"assets":[{"id":0,"required":1,"title":{"len":50}},{"id":1,"required":1,"img":{"type":3,"w":150,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":2,"required":0,"img":{"type":1,"w":50,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":3,"required":1,"data":{"type":1,"value":"Smilewanted sponsor"}},{"id":4,"required":1,"data":{"type":2,"value":"Smilewanted Description"}}]}'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crea_sw_1'); + expect(bid.currency).to.equal('EUR'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(300); + expect(bid.requestId).to.equal(NATIVE_REQUEST[0].bidId); + + expect(function () { + spec.interpretResponse(BID_RESPONSE_NATIVE, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + it('SmileWanted - Verify bidder code', function () { expect(spec.code).to.equal('smilewanted'); });