From f385d0f19b9a683ed490a254a8bb0c99628d76f2 Mon Sep 17 00:00:00 2001 From: "Munehiro.Taguchi" Date: Thu, 30 Jan 2025 11:19:31 +0900 Subject: [PATCH] Add Allox Bid Adapter --- modules/alloxBidAdapter.js | 239 ++++++++++++++++++++++ modules/alloxBidAdapter.md | 72 +++++++ test/spec/modules/alloxBidAdapter_spec.js | 207 +++++++++++++++++++ 3 files changed, 518 insertions(+) create mode 100644 modules/alloxBidAdapter.js create mode 100644 modules/alloxBidAdapter.md create mode 100644 test/spec/modules/alloxBidAdapter_spec.js diff --git a/modules/alloxBidAdapter.js b/modules/alloxBidAdapter.js new file mode 100644 index 00000000000..74fec3036a2 --- /dev/null +++ b/modules/alloxBidAdapter.js @@ -0,0 +1,239 @@ +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { + createTrackPixelHtml, + deepAccess, + deepClone, + deepEqual, + generateUUID, + getWindowLocation, + isArray, + isEmpty, + isStr, +} from '../src/utils.js'; + +const BIDDER_CODE = 'allox'; +const ENDPOINT_URL = 'https://alxd.addlv.smt.docomo.ne.jp/1.0/w/xdp/web.json'; +const TIME_TO_LIVE = 30; +const DOCOMO_SOURCE = 'docomo.ne.jp'; +const NIDAN_ID_KEY = '__nidan_id'; +const STORAGE_KEY = '__allox_trackers'; +const STORAGE_TIMEOUT = 3000; + +const QUERY_REG = { + DEBUG: /[&|\?]allox_debug=(\d+)/, + STB: /[&|\?]stb=(\d+)/g, + TARGET: /[&|\?]allox_target=([^&]*)/, + TEST: /[&|\?]allox_test=([^&]*)/, +}; + +const TRACKER_TYPE = { + IMP: 1, + LOSE_NOTICE_WHEN_ALLOX_WIN: 101, + LOSE_NOTICE_WHEN_ALLOX_LOSE: 102 +}; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: TIME_TO_LIVE + }, + response(buildResponse, bidResponses, ortbResponse, context) { + let lurl; + let trackers; + ortbResponse.seatbid.forEach(seat => { + seat.bid.forEach(bid => { + lurl = bid.lurl; + trackers = deepAccess(bid, 'ext.trackers'); + }); + }); + const response = buildResponse(bidResponses, ortbResponse, context); + response.bids.forEach(bid => { + if (lurl) { + bid.lurl = lurl; + }; + if (trackers) { + bid.trackers = trackers; + }; + if (bid.mediaType === BANNER) { + const impTrackersHtml = trackers + .filter(tracker => tracker.type === TRACKER_TYPE.IMP) + .reduce((pre, cur) => `${pre}${createTrackPixelHtml(cur.url)}`, ''); + bid.ad += impTrackersHtml; + }; + }); + return response; + } +}); + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + + code: BIDDER_CODE, + aliases: ['alx'], + + isBidRequestValid: function(bidRequest) { + const alloxIds = getAlloxIds(); + bidRequest.params.nidanId = isEmpty(alloxIds.nidanId) ? bidRequest.params.nidanId || storage.getDataFromLocalStorage(NIDAN_ID_KEY) : alloxIds.nidanId; + bidRequest.params.daisyId = isEmpty(alloxIds.daisyId) ? bidRequest.params.daisyId : alloxIds.daisyId; + return !!bidRequest.params.placementId && includeNidanIdOrDaisyID(bidRequest.params); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + return validBidRequests.map(validBidRequest => { + const context = { mediaType: getMediaType(validBidRequest) }; + const ortbRequest = converter.toORTB({ + bidRequests: [validBidRequest], + bidderRequest: bidderRequest, + context: context + }); + + const serverRequest = { + p: validBidRequest.params.placementId, + t: `web_${generateUUID()}-${validBidRequest.bidId}`, + bw: window.innerWidth, + impid: validBidRequest.bidId, + n: validBidRequest.params.nidanId, + d: validBidRequest.params.daisyId + }; + + setRequestParamFromURLParameter(serverRequest, 'debug', QUERY_REG.DEBUG); + setRequestParamFromURLParameter(serverRequest, 'stb', QUERY_REG.STB); + setRequestParamFromURLParameter(serverRequest, 'upt', QUERY_REG.TARGET); + setRequestParamFromURLParameter(serverRequest, 'test', QUERY_REG.TEST); + + const request = { + method: 'POST', + url: ENDPOINT_URL, + data: deepClone(serverRequest), + options: { + withCredentials: false + }, + ortbRequest, + validBidRequest + }; + return request; + }); + }, + + interpretResponse: function(serverResponse, request) { + if (isEmpty(serverResponse.body)) return []; + + const bids = converter.fromORTB({ + response: serverResponse.body, + request: request.ortbRequest + }).bids; + + if (storage.localStorageIsEnabled()) { + const trackersValue = storage.getDataFromLocalStorage(STORAGE_KEY); + const trackers = trackersValue ? JSON.parse(trackersValue) : {}; + const now = new Date().getTime(); + Object.keys(trackers).forEach(k => { + if (now - trackers[k].date > STORAGE_TIMEOUT) { + delete trackers[k]; + } + }) + const trackersObj = isEmpty(serverResponse.body.seatbid) ? { trackers: deepAccess(serverResponse, 'body.ext.trackers') } : bids[0]; + trackers[request.validBidRequest.adUnitId] = extractTrackers(trackersObj); + storage.setDataInLocalStorage(STORAGE_KEY, JSON.stringify(trackers)); + } + return bids; + }, + + supportedMediaTypes: [BANNER] + +}; + +registerBidder(spec); + +function getAlloxIds() { + const userIds = config.getConfig('userSync.userIds') || []; + let alloxIds = {}; + + userIds.forEach(userId => { + const eids = delveArray(userId, 'params.eids'); + + eids.forEach(eid => { + const source = deepAccess(eid, 'source', null); + + if (deepEqual(source, DOCOMO_SOURCE)) { + const uids = delveArray(eid, 'uids'); + + uids.forEach(uid => { + if (uid.atype === 1) { + alloxIds.daisyId = uid.id; + } + if (uid.atype === 3) { + alloxIds.nidanId = uid.id; + } + }); + }; + }); + }); + + return alloxIds; +} + +function delveArray(obj, keypath) { + const result = deepAccess(obj, keypath, []); + return isArray(result) ? result : []; +} + +function convertStringToBoolean(value) { + return !!((isStr(value) && value.toLowerCase() === 'true')); +}; + +function getMediaType(validBidRequest) { + if (isEmpty(validBidRequest.mediaTypes)) return; + + const keyName = Object.keys(validBidRequest.mediaTypes)[0]; + switch (keyName) { + case 'banner': + return BANNER; + default: + return undefined; + }; +}; + +function getURLParameterValue(regexp) { + const match = getWindowLocation().href.match(regexp); + if (isEmpty(match)) return; + + let result; + switch (regexp) { + case QUERY_REG.DEBUG: + case QUERY_REG.TARGET: + result = isNaN(Number(match[1])) ? match[1] : Number(match[1]); + break; + case QUERY_REG.TEST: + result = Number(convertStringToBoolean(match[1])); + break; + default: + result = match.map((v) => { + const value = v.split('=')[1]; + return isNaN(Number(value)) ? value : Number(value); + }); + }; + return result; +}; + +function setRequestParamFromURLParameter(serverRequest, key, regexp) { + const parsedValue = getURLParameterValue(regexp); + if (parsedValue) { + serverRequest[key] = parsedValue; + }; +}; + +function extractTrackers({trackers, lurl}) { + const date = new Date().getTime(); + return {trackers, lurl, date}; +} + +function includeNidanIdOrDaisyID({nidanId, daisyId}) { + const testValue = getURLParameterValue(QUERY_REG.TEST); + return testValue ? true : (!!nidanId || !!daisyId) +} diff --git a/modules/alloxBidAdapter.md b/modules/alloxBidAdapter.md new file mode 100644 index 00000000000..9da69bfb31c --- /dev/null +++ b/modules/alloxBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: Allox Bidder Adapter +Module Type: Bidder Adapter +Maintainer: mi-allox-devbot@ml.nttdocomo.com +``` + +# Description +Connect to Allox for bids. +Allox bid adapter supports Banner. +The Allox bidding adapter requires setup and approval before use. Please contact mi-allox-devbot@ml.nttdocomo.com for more details. + +# Test Parameters + +```js +var adUnits = [ + // Banner adUnit + { + code: "test-banner-code", + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: "allox", + params: { + placementId: "examplePlacementId", // required + }, + }, + ], + }, +]; +``` + +# Configuration + +Access to local storage is required for Allox's Prebid adapter. Ensure that local storage access is enabled; otherwise, the adapter may not function properly. +```js +pbjs.bidderSettings = { + allox: { + storageAllowed: true + } +}; +``` + +# Modules to include in your build process + +When running the build command, include `alloxBidAdapter` as a module, as well as `alloxAnalyticsAdapter`. + +If a JSON file is being used to specify the bidder modules, add `"alloxBidAdapter"` +to the top-level array in that file. + +```json +[ + "alloxBidAdapter", + "alloxAnalyticsAdapter", + "fooBidAdapter", + "bazBidAdapter" +] +``` + +And then build. + +``` +gulp build --modules=modules.json +``` + +# Notes +- Allox will return a test-bid if "allox_test=true" is present in page URL diff --git a/test/spec/modules/alloxBidAdapter_spec.js b/test/spec/modules/alloxBidAdapter_spec.js new file mode 100644 index 00000000000..63af576fd1c --- /dev/null +++ b/test/spec/modules/alloxBidAdapter_spec.js @@ -0,0 +1,207 @@ +import { expect } from 'chai'; +import { spec } from 'modules/alloxBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; + +const ENDPOINT = 'https://alxd.addlv.smt.docomo.ne.jp/1.0/w/xdp/web.json'; + +const TRACKER_TYPE = { + IMP: 1, + LOSE_NOTICE_WHEN_ALLOX_WIN: 101, + LOSE_NOTICE_WHEN_ALLOX_LOSE: 102 +}; + +describe('alloxBidAdapter', function () { + const adapter = newBidder(spec); + + const mockOrtbResponses = { + banner: { + id: 'BID-1-ZIMP-4b309eae-504a-4252-a8a8-4c8ceee9791a', + seatbid: [ + { + bid: [ + { + id: '32a69c6ba388f110487f9d1e63f77b22d86e916b', + impid: '279d6048370632', + price: 1, + adid: '529833ce55314b19e8796116', + nurl: 'https://alxl-s.allox-s.allox.d2c.ne.jp/xdp/v1/notify/win', + lurl: 'https://alxl-s.allox-s.allox.d2c.ne.jp/xdp/v1/notify/lose?wprice=${ALLOX:AUCTION_PRICE}&wcur=${ALLOX:AUCTION_CURRENCY}', + adm: '
', + adomain: ['d2c.ne.jp'], + crid: '529833ce55314b19e8796116_1385706446', + w: 300, + h: 250, + ext: { + trackers: [ + { + type: 1, + url: 'https://alxm-s.addlv.smt.docomo.ne.jp/529833ce55314b19e8796116/imp', + method: 'GET' + }, + { + type: 1, + url: 'https://alxm-s.addlv.smt.docomo.ne.jp/prebid/imp', + method: 'GET' + }, + { + type: 101, + url: 'http://alxm-s.addlv.smt.docomo.ne.jp/529833ce55314b19e8796116/lose_101', + method: 'GET' + }, + { + type: 102, + url: 'https://alxl-s.allox-s.allox.d2c.ne.jp/xdp/v1/notify/lose_102?wprice=${ALLOX:AUCTION_PRICE}&wcur=${ALLOX:AUCTION_CURRENCY}', + method: 'GET' + } + ] + } + } + ] + } + ], + cur: 'JPY' + }, + nobid: { + id: 'BID-2-ZIMP-4b309eae-504a-4252-a8a8-4c8ceee9791a', + seatbid: [], + cur: '', + ext: { + trackers: [ + { + type: 102, + url: 'https://alxl-s.allox-s.allox.d2c.ne.jp/v1/log/trace/lose_102_NoAd?wprice=${ALLOX:AUCTION_PRICE}&wcur=${ALLOX:AUCTION_CURRENCY}', + method: 'GET' + } + ] + } + } + }; + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + bidder: 'allox', + params: { + placementId: '123456', + nidanId: '636e6a0e0207e4a0f1f0d86b5bb57f3539b43ee224b84cc938d1f5659ff0360337bb694f20cc7094', + } + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let mockBidRequests = [ + { + bidder: 'allox', + params: { + placementId: 'banner', + nidanId: '636e6a0e0207e4a0f1f0d86b5bb57f3539b43ee224b84cc938d1f5659ff0360337bb694f20cc7094', + daisyId: '11dsy1dAAferVBRo', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidId: '2cd616eeefbe04', + bidderRequestId: '164dfa54d9c956', + auctionId: 'aba03555-4802-4c45-9f15-05ffa8594cff', + } + ]; + + it('sends bid request to ENDPOINT via POST', function () { + const mockBidderRequest = { refererInfo: {} }; + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + const regexp = `web_[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}-${mockBidRequests[0].bidId}`; + expect(requests[0].url).to.equal(ENDPOINT); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].data).to.have.all.keys(['n', 'd', 'p', 't', 'bw', 'impid']); + expect(requests[0].data).to.have.property('t').that.match(new RegExp(regexp)); + expect(requests[0].data).to.have.property('p', mockBidRequests[0].params.placementId) + }); + }); + + describe('interpretResponse', function () { + const mockBidRequests = [ + { + bidder: 'allox', + params: { + placementId: 'banner', + nidanId: '636e6a0e0207e4a0f1f0d86b5bb57f3539b43ee224b84cc938d1f5659ff0360337bb694f20cc7094', + daisyId: '11dsy1dAAferVBRo', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidId: '279d6048370632', + bidderRequestId: '11acba431de115', + auctionId: 'aba03555-4802-4c45-9f15-05ffa8594cff', + } + ]; + const mockBidderRequest = { refererInfo: {} }; + + it('should get correct banner bid response', function () { + const expectedResponse = [ + { + requestId: '279d6048370632', + cpm: 1, + currency: 'JPY', + width: 300, + height: 250, + meta: { + advertiserDomains: [ + 'd2c.ne.jp' + ] + }, + ad: '
', + mediaType: 'banner', + creativeId: '529833ce55314b19e8796116_1385706446', + creative_id: '529833ce55314b19e8796116_1385706446', + seatBidId: '32a69c6ba388f110487f9d1e63f77b22d86e916b', + lurl: 'https://alxl-s.allox-s.allox.d2c.ne.jp/xdp/v1/notify/lose?wprice=${ALLOX:AUCTION_PRICE}&wcur=${ALLOX:AUCTION_CURRENCY}', + trackers: mockOrtbResponses.banner.seatbid[0].bid[0].ext.trackers, + netRevenue: true, + ttl: 30 + } + ]; + + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + const result = spec.interpretResponse({ body: mockOrtbResponses.banner }, requests[0]); + const bid = mockOrtbResponses.banner.seatbid[0].bid[0]; + + expect(result).to.have.lengthOf(1); + + expectedResponse[0].ad += utils.createTrackPixelHtml(bid.nurl) + bid.ext.trackers + .filter(tracker => tracker.type === TRACKER_TYPE.IMP) + .reduce((pre, cur) => `${pre}${utils.createTrackPixelHtml(cur.url)}`, ''); + + expect(result).to.deep.have.same.members(expectedResponse); + }); + + it('handles nobid responses', function () { + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + const result = spec.interpretResponse({ body: mockOrtbResponses.nobid }, requests[0]); + expect(result.length).to.equal(0); + }); + }); +});