diff --git a/modules/uniquestAnalyticsAdapter.js b/modules/uniquestAnalyticsAdapter.js new file mode 100644 index 00000000000..f91d74986e4 --- /dev/null +++ b/modules/uniquestAnalyticsAdapter.js @@ -0,0 +1,108 @@ +import {logError} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import adapterManager from '../src/adapterManager.js'; +import {EVENTS} from '../src/constants.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; + +const ADAPTER_CODE = 'uniquest'; +const BASE_URL = 'https://rcvp.ust-ad.com/'; +const AUCTION_END_URI = 'pbaae'; +const AD_RENDERED_URI = 'pbaars'; + +let sid; + +function sendEvent(event, uri) { + ajax( + BASE_URL + uri, + null, + JSON.stringify(event) + ); +} + +function adRenderSucceededHandler(eventType, args, pageUrl) { + const event = { + event_type: eventType, + url: pageUrl, + slot_id: sid, + bid: { + auction_id: args.bid?.auctionId, + creative_id: args.bid?.creativeId, + bidder: args.bid?.bidderCode, + media_type: args.bid?.mediaType, + size: args.bid?.size, + cpm: String(args.bid?.cpm), + currency: args.bid?.currency, + original_cpm: String(args.bid?.originalCpm), + original_currency: args.bid?.originalCurrency, + hb_pb: String(args.bid?.adserverTargeting.hb_pb), + bidding_time: args.bid?.timeToRespond, + ad_unit_code: args.bid?.adUnitCode + } + }; + sendEvent(event, AD_RENDERED_URI); +} + +function auctionEndHandler(eventType, args, pageUrl) { + if (args.bidsReceived.length > 0) { + const event = { + event_type: eventType, + url: pageUrl, + slot_id: sid, + bids: args.bidsReceived?.map(br => ({ + auction_id: br?.auctionId, + creative_id: br?.creativeId, + bidder: br?.bidder, + media_type: br?.mediaType, + size: br?.size, + cpm: String(br?.cpm), + currency: br?.currency, + original_cpm: String(br?.originalCpm), + original_currency: br?.originalCurrency, + hb_pb: String(br?.adserverTargeting.hb_pb), + bidding_time: br?.timeToRespond, + ad_unit_code: br?.adUnitCode + })) + }; + sendEvent(event, AUCTION_END_URI); + } +} + +let baseAdapter = adapter({analyticsType: 'endpoint'}); +let uniquestAdapter = Object.assign({}, baseAdapter, { + + enableAnalytics(config = {}) { + if (config.options && config.options.sid) { + sid = config.options.sid; + baseAdapter.enableAnalytics.call(this, config); + } else { + logError('Config not found. Analytics is disabled due.'); + } + }, + + disableAnalytics() { + sid = undefined; + baseAdapter.disableAnalytics.apply(this, arguments); + }, + + track({eventType, args}) { + const refererInfo = getRefererInfo(); + let pageUrl = refererInfo.page; + + switch (eventType) { + case EVENTS.AD_RENDER_SUCCEEDED: + adRenderSucceededHandler(eventType, args, pageUrl); + break; + case EVENTS.AUCTION_END: + auctionEndHandler(eventType, args, pageUrl); + break; + } + } +}); + +adapterManager.registerAnalyticsAdapter({ + adapter: uniquestAdapter, + code: ADAPTER_CODE +}); + +export default uniquestAdapter; diff --git a/modules/uniquestAnalyticsAdapter.md b/modules/uniquestAnalyticsAdapter.md new file mode 100644 index 00000000000..73e220ee926 --- /dev/null +++ b/modules/uniquestAnalyticsAdapter.md @@ -0,0 +1,22 @@ +# Overview + +``` +Module Name: UNIQUEST Analytics Adapter +Module Type: Analytics Adapter +Maintainer: prebid_info@muneee.co.jp +``` + +# Description + +Analytics exchange for UNIQUEST + +# Test Parameters + +``` +{ + provider: 'uniquest', + options: { + sid: 'ONhFoaQn', + } +} +``` diff --git a/modules/uniquestBidAdapter.js b/modules/uniquestBidAdapter.js new file mode 100644 index 00000000000..fa4b7c0e347 --- /dev/null +++ b/modules/uniquestBidAdapter.js @@ -0,0 +1,97 @@ +import {getBidIdParameter} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory').BidRequest} BidRequest + * @typedef {import('../src/auction').BidderRequest} BidderRequest + */ + +const BIDDER_CODE = 'uniquest'; +const ENDPOINT = 'https://adpb.ust-ad.com/hb/prebid'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.sid); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const bidRequests = []; + + for (let i = 0; i < validBidRequests.length; i++) { + let queryString = ''; + const request = validBidRequests[i]; + + const bid = request.bidId; + const sid = getBidIdParameter('sid', request.params); + const widths = request.sizes.map(size => size[0]).join(','); + const heights = request.sizes.map(size => size[1]).join(','); + const timeout = bidderRequest.timeout + + queryString = tryAppendQueryString(queryString, 'bid', bid); + queryString = tryAppendQueryString(queryString, 'sid', sid); + queryString = tryAppendQueryString(queryString, 'widths', widths); + queryString = tryAppendQueryString(queryString, 'heights', heights); + queryString = tryAppendQueryString(queryString, 'timeout', timeout); + + bidRequests.push({ + method: 'GET', + url: ENDPOINT, + data: queryString, + }); + } + return bidRequests; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, requests) { + const response = serverResponse.body; + + if (!response || Object.keys(response).length === 0) { + return [] + } + + const bid = { + requestId: response.request_id, + cpm: response.cpm, + currency: response.currency, + width: response.width, + height: response.height, + ad: response.ad, + creativeId: response.bid_id, + netRevenue: response.net_revenue, + mediaType: response.media_type, + ttl: response.ttl, + meta: { + advertiserDomains: response.meta && response.meta.advertiser_domains ? response.meta.advertiser_domains : [], + }, + }; + + return [bid]; + }, +}; + +registerBidder(spec); diff --git a/modules/uniquestBidAdapter.md b/modules/uniquestBidAdapter.md new file mode 100644 index 00000000000..699816f96e1 --- /dev/null +++ b/modules/uniquestBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: UNIQUEST Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid_info@muneee.co.jp +``` + +# Description +Connects to UNIQUEST exchange for bids. + +# Test Parameters +```js +var adUnits = [ + // Banner adUnit + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [ + [300, 300], + [300, 250], + [320, 100] + ] + } + }, + bids: [{ + bidder: 'uniquest', + params: { + sid: 'ONhFoaQn', // device is smartphone only + } + }] + } +]; +``` diff --git a/test/spec/modules/uniquestAnalyticsAdapter_spec.js b/test/spec/modules/uniquestAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..6064f42050c --- /dev/null +++ b/test/spec/modules/uniquestAnalyticsAdapter_spec.js @@ -0,0 +1,461 @@ +import uniquestAnalyticsAdapter from 'modules/uniquestAnalyticsAdapter.js'; +import {config} from 'src/config'; +import {EVENTS} from 'src/constants.js'; +import {server} from '../../mocks/xhr.js'; + +let events = require('src/events'); + +const SAMPLE_EVENTS = { + AUCTION_END: { + 'auctionId': 'uniq1234', + 'timestamp': 1733709113000, + 'auctionEnd': 1733709113500, + 'auctionStatus': 'completed', + 'metrics': { + 'someMetric': 1 + }, + 'adUnits': [ + { + 'code': '/12345678910/uniquest_1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 1, + 1 + ], + [ + 300, + 300 + ], + [ + 300, + 250 + ] + ] + } + }, + 'bids': [ + { + 'bidder': 'uniquest', + 'params': { + 'sid': '3pwnAHWX' + } + }, + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 12345678 + } + } + ], + 'sizes': [ + [ + 1, + 1 + ], + [ + 300, + 300 + ], + [ + 300, + 250 + ] + ], + 'transactionId': '12345678' + } + ], + 'adUnitCodes': [ + '/12345678910/uniquest_1' + ], + 'bidderRequests': [ + { + 'auctionId': '75e394d9', + 'auctionStart': 1733709113010, + 'bidderCode': 'uniquest', + 'bidderRequestId': '1207cb49191887', + 'bids': [ + { + 'adUnitCode': '/12345678910/uniquest_1', + 'auctionId': '75e394d9', + 'bidId': '206be9a13236af', + 'bidderRequestId': '1207cb49191887', + 'bidder': 'uniquest', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 1, + 1 + ], + [ + 300, + 300 + ], + [ + 300, + 250 + ] + ] + } + }, + + 'transactionId': '6b29369c', + 'sizes': [ + [ + 1, + 1 + ], + [ + 300, + 300 + ], + [ + 300, + 250 + ] + ], + 'src': 'client', + } + ], + 'timeout': 400, + 'refererInfo': { + 'page': 'http://test-pb.ust-ad.com/banner.html', + 'domain': 'test-pb.ust-ad.com', + 'referer': 'http://test-pb.ust-ad.com/banner.html', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://test-pb.ust-ad.com/banner.html' + ], + 'canonicalUrl': null + }, + 'start': 1733709113020 + }, + { + 'bidderCode': 'appnexus', + 'auctionId': '75e394d9', + 'bidderRequestId': '32b97f0a935422', + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 12345678 + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 1, + 1 + ], + [ + 300, + 300 + ], + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/12345678910/uniquest_1', + 'transactionId': '6b29369c', + 'sizes': [ + [ + 1, + 1 + ], + [ + 300, + 300 + ], + [ + 300, + 250 + ] + ], + 'bidId': '41badc0e164c758', + 'bidderRequestId': '32b97f0a935422', + 'auctionId': '75e394d9', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'device': { + 'mobile': 1 + } + } + } + ], + 'auctionStart': 1733709113010, + 'timeout': 400, + 'refererInfo': { + 'page': 'http://test-pb.ust-ad.com/banner.html', + 'domain': 'test-memo.wakaue.info', + 'referer': 'http://test-pb.ust-ad.com/banner.html', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://test-pb.ust-ad.com/banner.html' + ], + 'canonicalUrl': null + }, + 'start': 1733709113020 + } + ], + 'noBids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 12345678 + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 1, + 1 + ], + [ + 300, + 300 + ], + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/12345678910/uniquest_1', + 'transactionId': '6b29369c', + 'sizes': [ + [ + 1, + 1 + ], + [ + 300, + 300 + ], + [ + 300, + 250 + ] + ], + 'bidId': '41badc0e164c758', + 'bidderRequestId': '32b97f0a935422', + 'auctionId': '75e394d9', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ], + 'bidsReceived': [ + { + 'bidderCode': 'uniquest', + 'width': 300, + 'height': 300, + 'statusMessage': 'Bid available', + 'adId': '53c5a9c1947c57', + 'requestId': '4d9eec3fe27a43', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 2.73, + 'currency': 'JPY', + 'ad': ' ', + 'ttl': 300, + 'creativeId': '7806bcbb-a156-4ec4-872b-bd0d8e8bff34', + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [ + 'test-pb.ust-ad.com' + ] + }, + 'originalCpm': 2.73, + 'originalCurrency': 'JPY', + 'auctionId': '75e394d9', + 'responseTimestamp': 1733709113100, + 'requestTimestamp': 1733709113000, + 'bidder': 'uniquest', + 'adUnitCode': '/12345678910/uniquest_1', + 'timeToRespond': 100, + 'pbLg': '2.50', + 'pbMg': '2.70', + 'pbHg': '2.73', + 'pbAg': '2.70', + 'pbDg': '2.73', + 'pbCg': '', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'uniquest', + 'hb_adid': '53c5a9c1947c57', + 'hb_pb': '2.70', + 'hb_size': '300x300', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': 'test-pb.ust-ad.com' + } + } + ], + 'winningBids': [], + 'timeout': 400 + }, + AD_RENDER_SUCCEEDED: { + 'doc': { + 'location': { + 'href': 'http://test-pb.ust-ad.com/banner.html', + 'protocol': 'http:', + 'host': 'test-pb.ust-ad.com', + 'hostname': 'localhost', + 'port': '80', + 'pathname': '/page_banner.html', + 'hash': '', + 'origin': 'http://test-pb.ust-ad.com', + 'ancestorOrigins': { + '0': 'http://test-pb.ust-ad.com' + } + } + }, + 'bid': { + 'bidderCode': 'uniquest', + 'width': 300, + 'height': 300, + 'statusMessage': 'Bid available', + 'adId': '53c5a9c1947c57', + 'requestId': '4d9eec3fe27a43', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': '2.73', + 'currency': 'JPY', + 'ad': 'test_ad', + 'metrics': { + 'someMetric': 0 + }, + 'ttl': 300, + 'creativeId': '7806bcbb-a156-4ec4-872b-bd0d8e8bff34', + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [ + 'test-pb.ust-ad.com' + ] + }, + 'originalCpm': 2.73, + 'originalCurrency': 'JPY', + 'auctionId': '75e394d9', + 'responseTimestamp': 1733709113100, + 'requestTimestamp': 1733709113000, + 'bidder': 'uniquest', + 'adUnitCode': '12345678910/uniquest_1', + 'timeToRespond': 100, + 'size': '300x300', + 'adserverTargeting': { + 'hb_bidder': 'uniquest', + 'hb_adid': '53c5a9c1947c57', + 'hb_pb': '2.70', + 'hb_size': '300x300', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': 'test-pb.ust-ad.com' + }, + 'status': 'rendered', + 'params': [ + { + 'nonZetaParam': 'nonZetaValue' + } + ] + } + } +} + +describe('Uniquest Analytics Adapter', function () { + let sandbox; + let requests; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + requests = server.requests; + sandbox.stub(events, 'getEvents').returns([]); + }); + + afterEach(function () { + sandbox.restore(); + config.resetConfig(); + }); + + describe('handle events', function () { + beforeEach(function () { + uniquestAnalyticsAdapter.enableAnalytics({ + options: { + sid: 'ABCDE123', + } + }); + }); + + afterEach(function () { + uniquestAnalyticsAdapter.disableAnalytics(); + }); + + it('Handle events', function () { + this.timeout(1000); + + events.emit(EVENTS.AUCTION_END, SAMPLE_EVENTS.AUCTION_END); + events.emit(EVENTS.AD_RENDER_SUCCEEDED, SAMPLE_EVENTS.AD_RENDER_SUCCEEDED); + + // bids count + expect(requests.length).to.equal(2); + const auctionEnd = JSON.parse(requests[0].requestBody); + // event_type + expect(auctionEnd.event_type).to.eql(EVENTS.AUCTION_END); + // URL + expect(auctionEnd.url).to.eql(window.top.location.href); + // bid + expect(auctionEnd.bids).to.be.deep.equal([{ + auction_id: '75e394d9', + creative_id: '7806bcbb-a156-4ec4-872b-bd0d8e8bff34', + bidder: 'uniquest', + media_type: 'banner', + size: '300x250', + cpm: '2.73', + currency: 'JPY', + original_cpm: '2.73', + original_currency: 'JPY', + hb_pb: '2.70', + bidding_time: 100, + ad_unit_code: '/12345678910/uniquest_1', + }] + ); + + const auctionSucceeded = JSON.parse(requests[1].requestBody); + // event_type + expect(auctionSucceeded.event_type).to.eql(EVENTS.AD_RENDER_SUCCEEDED); + // URL + expect(auctionSucceeded.url).to.eql(window.top.location.href); + // bid + expect(auctionSucceeded.bid).to.be.deep.equal({ + auction_id: '75e394d9', + creative_id: '7806bcbb-a156-4ec4-872b-bd0d8e8bff34', + bidder: 'uniquest', + media_type: 'banner', + size: '300x300', + cpm: '2.73', + currency: 'JPY', + original_cpm: '2.73', + original_currency: 'JPY', + hb_pb: '2.70', + bidding_time: 100, + ad_unit_code: '12345678910/uniquest_1' + }); + }); + }); +}); diff --git a/test/spec/modules/uniquestBidAdapter_spec.js b/test/spec/modules/uniquestBidAdapter_spec.js new file mode 100644 index 00000000000..57051d33a43 --- /dev/null +++ b/test/spec/modules/uniquestBidAdapter_spec.js @@ -0,0 +1,104 @@ +import { expect } from 'chai'; +import { spec } from 'modules/uniquestBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +const ENDPOINT = 'https://adpb.ust-ad.com/hb/prebid'; + +describe('UniquestAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + const request = { + bidder: 'uniquest', + params: { + sid: 'sid_0001', + }, + }; + expect(spec.isBidRequestValid(request)).to.equal(true) + }) + + it('should return false when required params are not passed', function () { + expect(spec.isBidRequestValid({})).to.equal(false) + expect(spec.isBidRequestValid({ sid: '' })).to.equal(false) + }) + }) + + describe('buildRequest', function () { + const bids = [ + { + bidder: 'uniquest', + params: { + sid: 'sid_0001', + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 300], + [300, 250], + [320, 100], + ], + bidId: '259d7a594535852', + bidderRequestId: '247f62f777e5e4', + } + ]; + const bidderRequest = { + timeout: 1500, + } + it('sends bid request to ENDPOINT via GET', function () { + const requests = spec.buildRequests(bids, bidderRequest); + expect(requests[0].url).to.equal(ENDPOINT); + expect(requests[0].method).to.equal('GET'); + expect(requests[0].data).to.equal('bid=259d7a594535852&sid=sid_0001&widths=300%2C300%2C320&heights=300%2C250%2C100&timeout=1500&') + }) + }) + + describe('interpretResponse', function() { + it('should return a valid bid response', function () { + const serverResponse = { + request_id: '247f62f777e5e4', + cpm: 12.3, + currency: 'JPY', + width: 300, + height: 250, + bid_id: 'bid_0001', + deal_id: '', + net_revenue: false, + ttl: 300, + ad: '
', + media_type: 'banner', + meta: { + advertiser_domains: ['advertiser.com'], + }, + }; + const expectResponse = [{ + requestId: '247f62f777e5e4', + cpm: 12.3, + currency: 'JPY', + width: 300, + height: 250, + ad: '
', + creativeId: 'bid_0001', + netRevenue: false, + mediaType: 'banner', + ttl: 300, + meta: { + advertiserDomains: ['advertiser.com'], + } + }]; + const result = spec.interpretResponse({ body: serverResponse }, {}); + expect(result).to.have.lengthOf(1); + expect(result).to.deep.have.same.members(expectResponse); + }) + + it('should return an empty array to indicate no valid bids', function () { + const result = spec.interpretResponse({ body: {} }, {}) + expect(result).is.an('array').is.empty; + }) + }) +})