diff --git a/modules/adplusBidAdapter.js b/modules/adplusBidAdapter.js new file mode 100644 index 00000000000..c001781a792 --- /dev/null +++ b/modules/adplusBidAdapter.js @@ -0,0 +1,203 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +// #region Constants +export const BIDDER_CODE = 'adplus'; +export const ADPLUS_ENDPOINT = 'https://ssp.ad-plus.com.tr/server/headerBidding'; +export const DGID_CODE = 'adplus_dg_id'; +export const SESSION_CODE = 'adplus_s_id'; +export const storage = getStorageManager(undefined, BIDDER_CODE); +const COOKIE_EXP = 1000 * 60 * 60 * 24; // 1 day +// #endregion + +// #region Helpers +export function isValidUuid (uuid) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + uuid + ); +} + +function getSessionId() { + let sid = storage.cookiesAreEnabled() && storage.getCookie(SESSION_CODE); + + if ( + !sid || !isValidUuid(sid) + ) { + sid = utils.generateUUID(); + setSessionId(sid); + } + + return sid; +} + +function setSessionId(sid) { + if (storage.cookiesAreEnabled()) { + const expires = new Date(Date.now() + COOKIE_EXP).toISOString(); + + storage.setCookie(SESSION_CODE, sid, expires); + } +} +// #endregion + +// #region Bid request validation +function isBidRequestValid(bid) { + if (!bid) { + utils.logError(BIDDER_CODE, 'bid, can not be empty', bid); + return false; + } + + if (!bid.params) { + utils.logError(BIDDER_CODE, 'bid.params is required.'); + return false; + } + + if (!bid.params.adUnitId || typeof bid.params.adUnitId !== 'string') { + utils.logError( + BIDDER_CODE, + 'bid.params.adUnitId is missing or has wrong type.' + ); + return false; + } + + if (!bid.params.inventoryId || typeof bid.params.inventoryId !== 'string') { + utils.logError( + BIDDER_CODE, + 'bid.params.inventoryId is missing or has wrong type.' + ); + return false; + } + + if ( + !bid.mediaTypes || + !bid.mediaTypes[BANNER] || + !utils.isArray(bid.mediaTypes[BANNER].sizes) || + bid.mediaTypes[BANNER].sizes.length <= 0 || + !utils.isArrayOfNums(bid.mediaTypes[BANNER].sizes[0]) + ) { + utils.logError(BIDDER_CODE, 'Wrong or missing size parameters.'); + return false; + } + + return true; +} +// #endregion + +// #region Building the bid requests +/** + * + * @param {object} bid + * @returns + */ +function createBidRequest(bid) { + // Developer Params + const { + inventoryId, + adUnitId, + extraData, + yearOfBirth, + gender, + categories, + latitude, + longitude, + sdkVersion, + } = bid.params; + + return { + method: 'GET', + url: ADPLUS_ENDPOINT, + data: utils.cleanObj({ + bidId: bid.bidId, + inventoryId, + adUnitId, + adUnitWidth: bid.mediaTypes[BANNER].sizes[0][0], + adUnitHeight: bid.mediaTypes[BANNER].sizes[0][1], + extraData, + yearOfBirth, + gender, + categories, + latitude, + longitude, + sdkVersion: sdkVersion || '1', + session: getSessionId(), + interstitial: 0, + token: typeof window.top === 'object' && window.top[DGID_CODE] ? window.top[DGID_CODE] : undefined, + secure: window.location.protocol === 'https:' ? 1 : 0, + screenWidth: screen.width, + screenHeight: screen.height, + language: window.navigator.language || 'en-US', + pageUrl: window.location.href, + domain: window.location.hostname, + referrer: window.location.referrer, + }), + }; +} + +function buildRequests(validBidRequests, bidderRequest) { + return validBidRequests.map((req) => createBidRequest(req)); +} +// #endregion + +// #region Interpreting Responses +/** + * + * @param {HeaderBiddingResponse} responseData + * @param { object } bidParams + * @returns + */ +function createAdResponse(responseData, bidParams) { + return { + requestId: responseData.requestID, + cpm: responseData.cpm, + currency: responseData.currency, + width: responseData.width, + height: responseData.height, + creativeId: responseData.creativeID, + dealId: responseData.dealID, + netRevenue: responseData.netRevenue, + ttl: responseData.ttl, + ad: responseData.ad, + mediaType: responseData.mediaType, + meta: { + advertiserDomains: responseData.advertiserDomains, + primaryCatId: utils.isArray(responseData.categoryIDs) && responseData.categoryIDs.length > 0 + ? responseData.categoryIDs[0] : undefined, + secondaryCatIds: responseData.categoryIDs, + }, + }; +} + +function interpretResponse(response, request) { + // In case of empty response + if ( + response.body == null || + !utils.isArray(response.body) || + response.body.length === 0 + ) { + return []; + } + const bids = response.body.map((bid) => createAdResponse(bid)); + return bids; +} +// #endregion + +// #region Bidder +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onTimeout(timeoutData) { + utils.logError('Adplus adapter timed out for the auction.', timeoutData); + }, + onBidWon(bid) { + utils.logInfo( + `Adplus adapter won the auction. Bid id: ${bid.bidId}, Ad Unit Id: ${bid.adUnitId}, Inventory Id: ${bid.inventoryId}` + ); + }, +}; + +registerBidder(spec); +// #endregion diff --git a/modules/adplusBidAdapter.md b/modules/adplusBidAdapter.md new file mode 100644 index 00000000000..dce9e4a312f --- /dev/null +++ b/modules/adplusBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +Module Name: AdPlus Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: adplus.destek@yaani.com.tr + +# Description + +AdPlus Prebid.js Bidder Adapter. Only banner formats are supported. + +About us : https://ssp.ad-plus.com.tr/ + +# Test Parameters + +```javascript +var adUnits = [ + { + code: "div-adplus", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + ], + }, + }, + bids: [ + { + bidder: "adplus", + params: { + inventoryId: "-1", + adUnitId: "-3", + }, + }, + ], + }, +]; +``` diff --git a/test/spec/modules/adplusBidAdapter_spec.js b/test/spec/modules/adplusBidAdapter_spec.js new file mode 100644 index 00000000000..840d86c80f1 --- /dev/null +++ b/test/spec/modules/adplusBidAdapter_spec.js @@ -0,0 +1,213 @@ +import {expect} from 'chai'; +import {spec, BIDDER_CODE, ADPLUS_ENDPOINT, } from 'modules/adplusBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +describe('AplusBidAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.be.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + let validRequest = { + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + inventoryId: '30', + adUnitId: '1', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let validRequest = { + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + inventoryId: '30', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + + it('should return false when required param types are wrong', function () { + let validRequest = { + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + inventoryId: 30, + adUnitId: '1', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + + it('should return false when size is not exists', function () { + let validRequest = { + params: { + inventoryId: 30, + adUnitId: '1', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + + it('should return false when size is wrong', function () { + let validRequest = { + mediaTypes: { + banner: { + sizes: [[300]] + } + }, + params: { + inventoryId: 30, + adUnitId: '1', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let validRequest = [ + { + bidder: BIDDER_CODE, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + inventoryId: '-1', + adUnitId: '-3', + }, + bidId: '2bdcb0b203c17d' + }, + ]; + + let bidderRequest = { + refererInfo: { + referer: 'https://test.domain' + } + }; + + it('bidRequest HTTP method', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request[0].method).to.equal('GET'); + }); + + it('bidRequest url', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request[0].url).to.equal(ADPLUS_ENDPOINT); + }); + + it('tests bidRequest data is clean and has the right values', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + + expect(request[0].data.bidId).to.equal('2bdcb0b203c17d'); + expect(request[0].data.inventoryId).to.equal('-1'); + expect(request[0].data.adUnitId).to.equal('-3'); + expect(request[0].data.adUnitWidth).to.equal(300); + expect(request[0].data.adUnitHeight).to.equal(250); + expect(request[0].data.sdkVersion).to.equal('1'); + expect(typeof request[0].data.session).to.equal('string'); + expect(request[0].data.session).length(36); + expect(request[0].data.interstitial).to.equal(0); + expect(request[0].data).to.not.have.deep.property('extraData'); + expect(request[0].data).to.not.have.deep.property('yearOfBirth'); + expect(request[0].data).to.not.have.deep.property('gender'); + expect(request[0].data).to.not.have.deep.property('categories'); + expect(request[0].data).to.not.have.deep.property('latitude'); + expect(request[0].data).to.not.have.deep.property('longitude'); + }); + }); + + describe('interpretResponse', function () { + const requestData = { + language: window.navigator.language, + screenWidth: 1440, + screenHeight: 900, + sdkVersion: '1', + inventoryId: '-1', + adUnitId: '-3', + adUnitWidth: 300, + adUnitHeight: 250, + domain: 'tassandigi.com', + pageUrl: 'https%3A%2F%2Ftassandigi.com%2Fserafettin%2Fads.html', + interstitial: 0, + session: '1c02db03-5289-932a-93af-7b4022611fec', + token: '1c02db03-5289-937a-93df-7b4022611fec', + secure: 1, + bidId: '2bdcb0b203c17d', + }; + const bidRequest = { + 'method': 'GET', + 'url': ADPLUS_ENDPOINT, + 'data': requestData, + }; + + const bidResponse = { + body: [ + { + 'ad': '