diff --git a/modules/rumbleBidAdapter.js b/modules/rumbleBidAdapter.js new file mode 100644 index 00000000000..b4be549d394 --- /dev/null +++ b/modules/rumbleBidAdapter.js @@ -0,0 +1,128 @@ +import { registerBidder } from "../src/adapters/bidderFactory.js"; +import { BANNER, VIDEO } from "../src/mediaTypes.js"; +import { config } from "../src/config.js"; +import { ortbConverter } from "../libraries/ortbConverter/converter.js"; +import { + deepSetValue, + deepAccess, + getBidIdParameter, + logError, + logWarn, + triggerPixel, + replaceAuctionPrice +} from "../src/utils.js"; + +const BIDDER_CODE = 'rumble'; +const ENDPOINT = 'https://a.ads.rmbl.ws/v1/sites/:id/ortb'; +const VERSION = '1.0.0'; + +function fillParameters(bid) { + const global = config.getConfig('rumble') || {}; + + bid.params = bid.params || {}; + + [ + 'publisherId', + 'siteId', + 'test', + ].forEach(function(k) { + if (bid.params[k]) { + return; + } + + if (global[k]) { + bid.params[k] = global[k]; + } + }) + + return bid.params; +} + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 60, + currency: "USD" + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const params = fillParameters(bidderRequest?.bids[0]) + + if (params?.test) { + deepSetValue(request, 'test', 1) + } + + deepSetValue(request, 'ext.adapter', { + version: VERSION, + name: 'prebidjs' + }) + + return request; + } +}) + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid: function (bid) { + fillParameters(bid) + + if (bid && typeof bid.params !== 'object') { + logError(BIDDER_CODE + ': params is not defined or is incorrect in the bidder settings.'); + return false; + } + + const required = ['publisherId', 'siteId']; + + for (let i = 0; i < required.length; i++) { + if (!getBidIdParameter(required[i], bid.params)) { + logError(BIDDER_CODE + `: ${required[i]} must be set as a bidder parameter`); + return false; + } + } + + const banner = deepAccess(bid, `mediaTypes.banner`); + const video = deepAccess(bid, `mediaTypes.video`); + + if (!banner && !video) { + logWarn(BIDDER_CODE + ': either banner or video mediaType must be provided') + return false; + } + + return true; + }, + buildRequests: function(bidRequests, bidderRequest) { + const publisherId = bidRequests[0].params.publisherId; + const siteId = bidRequests[0].params.siteId; + const zoneId = bidRequests[0].params.zoneId; + let endpoint = ENDPOINT.replace(':id', siteId) + "?pid=" + publisherId; + + if (zoneId) { + endpoint += "&a=" + zoneId; + } + + return bidRequests.map(bid => { + return { + url: endpoint, + method: 'POST', + data: converter.toORTB({bidRequests: [bid], bidderRequest}), + bidRequest: bid, + }; + }) + }, + interpretResponse(response, request) { + return converter.fromORTB({response: response.body, request: request.data}).bids; + }, + onBidWon: function(bid) { + if (bid.burl) { + triggerPixel(replaceAuctionPrice(bid.burl, bid.originalCpm || bid.cpm)); + } + + if (bid.nurl) { + triggerPixel(replaceAuctionPrice(bid.nurl, bid.originalCpm || bid.cpm)); + } + }, +}; + +registerBidder(spec); diff --git a/modules/rumbleBidAdapter.md b/modules/rumbleBidAdapter.md new file mode 100644 index 00000000000..b583633123e --- /dev/null +++ b/modules/rumbleBidAdapter.md @@ -0,0 +1,114 @@ +# Overview + + Module Name: Rumble Bidder Adapter + Module Type: Bidder Adapter + Maintainer: adtech@rumble.com + +## Description + +Connects to Rumble Advertising Center (`RAC` for short) for bids. + +## Configuration + +### Parameters +Rumble requires multiple parameters. These parameters may be set globally or per each ad unit. + +| Parameter | Global | AdUnit | Description | +|-------------|--------|--------|----------------------------------------------------| +| publisherId | x | x | Your RAC account publisher ID | +| siteId | x | x | The site ID you want to send requests | +| zoneId | | x | An optional zone ID that you want to send requests | +| test | x | x | An optional boolean flag for sending test requests | + +#### Global Configuration + +The global configuration is used to set parameters across all ad units instead of individually. + +```javascript +pbjs.setConfig({ + rumble: { + publisherId: 1, + siteId: 2, + } +}) +``` + +#### Ad-unit configuration + +All global configuration may be overridden by ad-unit configuration in addition to adding ad-unit only parameters. + +```javascript +let adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: "rumble", + params: { + publisherId: 1, + siteId: 1, + zoneId: 1, // optional + } + } + ] + } +]; +``` + +## Test Parameters + + +### Sample Display Ad Unit +```javascript +let adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: "rumble", + params: { + publisherId: 1, + siteId: 1, + zoneId: 1, // optional + test: true // only while testing + } + } + ] + } +] +``` + +### Sample Video Ad Unit +```javascript +let adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + mimes: ['video/mp4'], + } + }, + bids: [ + { + bidder: "rumble", + params: { + publisherId: 1, + siteId: 1, + zoneId: 1, // optional + test: true // only while testing + } + } + ] + } +] +``` diff --git a/test/spec/modules/rumbleBidAdapter_spec.js b/test/spec/modules/rumbleBidAdapter_spec.js new file mode 100644 index 00000000000..2122a0f7ffc --- /dev/null +++ b/test/spec/modules/rumbleBidAdapter_spec.js @@ -0,0 +1,125 @@ +import {spec, converter} from 'modules/rumbleBidAdapter.js'; +import { config } from '../../../src/config.js'; +import {BANNER} from "../../../src/mediaTypes.js"; +import {deepClone, getUniqueIdentifierStr} from "../../../src/utils.js"; +import {expect} from "chai"; + +const bidder = 'rumble'; + +describe('RumbleBidAdapter', function() { + describe('isBidRequestValid', function() { + const bidId = getUniqueIdentifierStr(); + const bidderRequestId = getUniqueIdentifierStr(); + + function newBid() { + return { + bidder, + bidId, + bidderRequestId, + params: { + publisherId: '123', + siteId: '321', + }, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + } + } + + it('should return true when all required parameters exist', function() { + expect(spec.isBidRequestValid(newBid())).to.equal(true); + }); + + it('should return false when publisherId is not present', function() { + let bid = newBid(); + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when siteId is not present', function() { + let bid = newBid(); + delete bid.params.siteId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if mediaTypes.banner or video is not present', function () { + let bid = newBid(); + delete bid.mediaTypes + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when global configuration is present', function() { + let bid = newBid(); + delete bid.params.publisherId; + delete bid.params.siteId; + + config.mergeConfig({ + rumble: { + publisherId: 1, + siteId: 1, + test: true, + } + }); + + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequests', function() { + let bidRequests = [{ + bidder: 'rumble', + params: { + publisherId: 1, + siteId: 2, + zoneId: 3, + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + sizes: [[300, 250]], + bidId: getUniqueIdentifierStr(), + bidderRequestId: getUniqueIdentifierStr(), + auctionId: getUniqueIdentifierStr(), + src: 'client', + bidRequestsCount: 1 + }]; + + let bidderRequest = { + bidderCode: 'rumble', + auctionId: getUniqueIdentifierStr(), + refererInfo: { + domain: 'localhost', + page: 'http://localhost/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + }, + ortb2: { + site: { + publisher: { + name: 'rumble' + } + } + } + }; + + function createRequests(bidRequests, bidderRequest) { + let cbr = deepClone(bidderRequest); + cbr.bids = bidRequests; + return spec.buildRequests(bidRequests, cbr); + } + + it('should validate request', function() { + let requests = createRequests(bidRequests, bidderRequest); + + expect(requests).to.have.lengthOf(bidRequests.length); + + requests.forEach(function(request, idx) { + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://a.ads.rmbl.ws/v1/sites/2/ortb?pid=1&a=3'); + expect(request.bidRequest).to.equal(bidRequests[idx]); + }); + }); + }); +});