diff --git a/modules/mantisBidAdapter.js b/modules/mantisBidAdapter.js new file mode 100644 index 00000000000..7247a236e71 --- /dev/null +++ b/modules/mantisBidAdapter.js @@ -0,0 +1,206 @@ +import {registerBidder} from 'src/adapters/bidderFactory'; + +function inIframe() { + try { + return window.self !== window.top && !window.mantis_link; + } catch (e) { + return true; + } +} + +function isDesktop(ignoreTouch) { + var supportsTouch = !ignoreTouch && ('ontouchstart' in window || navigator.msMaxTouchPoints); + if (inIframe()) { + return !supportsTouch; + } + var width = window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth; + return !supportsTouch && (!width || width >= (window.mantis_breakpoint || 768)); +} + +function storeUuid(uuid) { + if (window.mantis_uuid) { + return false; + } + window.mantis_uuid = uuid; + if (window.localStorage) { + try { + window.localStorage.setItem('mantis:uuid', uuid); + } catch (ex) { + } + } +} + +function isSendable(val) { + if (val === null || val === undefined) { + return false; + } + if (typeof val === 'string') { + return !(!val || /^\s*$/.test(val)); + } + if (typeof val === 'number') { + return !isNaN(val); + } + return true; +} + +function isObject(value) { + return Object.prototype.toString.call(value) === '[object Object]'; +} + +function isAmp() { + return typeof window.context === 'object' && (window.context.tagName === 'AMP-AD' || window.context.tagName === 'AMP-EMBED'); +} + +function isSecure() { + return document.location.protocol === 'https:'; +} + +function isArray(value) { + return Object.prototype.toString.call(value) === '[object Array]'; +} + +function jsonToQuery(data, chain, form) { + if (!data) { + return null; + } + var parts = form || []; + for (var key in data) { + var queryKey = key; + if (chain) { + queryKey = chain + '[' + key + ']'; + } + var val = data[key]; + if (isArray(val)) { + for (var index = 0; index < val.length; index++) { + var akey = queryKey + '[' + index + ']'; + var aval = val[index]; + if (isObject(aval)) { + jsonToQuery(aval, akey, parts); + } else if (isSendable(aval)) { + parts.push(akey + '=' + encodeURIComponent(aval)); + } + } + } else if (isObject(val) && val != data) { + jsonToQuery(val, queryKey, parts); + } else if (isSendable(val)) { + parts.push(queryKey + '=' + encodeURIComponent(val)); + } + } + return parts.join('&'); +} + +function buildMantisUrl(path, data, domain) { + var params = { + referrer: document.referrer, + tz: new Date().getTimezoneOffset(), + buster: new Date().getTime(), + secure: isSecure() + }; + if (!inIframe() || isAmp()) { + params.mobile = !isAmp() && isDesktop(true) ? 'false' : 'true'; + } + if (window.mantis_uuid) { + params.uuid = window.mantis_uuid; + } else if (window.localStorage) { + var localUuid = window.localStorage.getItem('mantis:uuid'); + if (localUuid) { + params.uuid = localUuid; + } + } + if (!inIframe()) { + try { + params.title = window.top.document.title; + params.referrer = window.top.document.referrer; + params.url = window.top.document.location.href; + } catch (ex) { + } + } else { + params.iframe = true; + } + if (isAmp()) { + params.amp = true; + if (!params.url && window.context.canonicalUrl) { + params.url = window.context.canonicalUrl; + } + if (!params.url && window.context.location) { + params.url = window.context.location.href; + } + if (!params.referrer && window.context.referrer) { + params.referrer = window.context.referrer; + } + } + Object.keys(data || {}).forEach(function (key) { + params[key] = data[key]; + }); + var query = jsonToQuery(params); + return (window.mantis_domain === undefined ? domain || 'https://mantodea.mantisadnetwork.com' : window.mantis_domain) + path + '?' + query; +} + +const spec = { + code: 'mantis', + supportedMediaTypes: ['banner', 'video', 'native'], + isBidRequestValid: function (bid) { + return !!(bid.params.property && (bid.params.code || bid.params.zoneId || bid.params.zone)); + }, + buildRequests: function (validBidRequests) { + var property = null; + validBidRequests.some(function (bid) { + if (bid.params.property) { + property = bid.params.property; + return true; + } + }); + const query = { + bids: validBidRequests.map(function (bid) { + return { + bidId: bid.bidId, + config: bid.params, + sizes: bid.sizes.map(function (size) { + return {width: size[0], height: size[1]}; + }) + }; + }), + property: property, + version: 2 + }; + return { + method: 'GET', + url: buildMantisUrl('/prebid/display', query) + '&foo', + data: '' + }; + }, + interpretResponse: function (serverResponse) { + storeUuid(serverResponse.uuid); + return serverResponse.body.ads.map(function (ad) { + return { + requestId: ad.bid, + cpm: ad.cpm, + width: ad.width, + height: ad.height, + ad: ad.html, + ttl: 86400, + creativeId: ad.view, + netRevenue: true, + currency: 'USD' + }; + }); + }, + getUserSyncs: function (syncOptions) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: buildMantisUrl('/prebid/iframe') + }]; + } + if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: buildMantisUrl('/prebid/pixel') + }]; + } + } +}; + +export {spec}; + +registerBidder(spec); diff --git a/modules/mantisBidAdapter.md b/modules/mantisBidAdapter.md new file mode 100644 index 00000000000..d8896c71514 --- /dev/null +++ b/modules/mantisBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +Module Name: MANTIS Ad Network Bid Adapter + +Module Type: Bidder Adapter + +Maintainer: paris@mantisadnetwork.com + +# Description + +Module that connects to MANTIS's demand sources + +# Test Parameters +```javascript + var adUnits = [ + { + code: 'test', + sizes: [[300, 250]], + bids: [ + { + bidder: 'mantis', + params: { + property: 'demo', + zone: 'zone' + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/mantisBidAdapter_spec.js b/test/spec/modules/mantisBidAdapter_spec.js new file mode 100644 index 00000000000..e2cd4df9a07 --- /dev/null +++ b/test/spec/modules/mantisBidAdapter_spec.js @@ -0,0 +1,178 @@ +import {expect} from 'chai'; +import {spec} from 'modules/mantisBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; + +describe('MantisAdapter', () => { + const adapter = newBidder(spec); + + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'mantis', + 'params': { + 'property': '10433394', + 'zone': 'zone' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + let bidRequests = [ + { + 'bidder': 'mantis', + 'params': { + 'property': '10433394', + 'zone': 'zone' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('domain override', () => { + window.mantis_domain = 'http://foo'; + const request = spec.buildRequests(bidRequests); + + expect(request.url).to.include('http://foo'); + + delete window.mantis_domain; + }); + + it('standard request', () => { + const request = spec.buildRequests(bidRequests); + + expect(request.url).to.include('property=10433394'); + expect(request.url).to.include('bids[0][bidId]=30b31c1838de1e'); + expect(request.url).to.include('bids[0][config][zone]=zone'); + expect(request.url).to.include('bids[0][sizes][0][width]=300'); + expect(request.url).to.include('bids[0][sizes][0][height]=250'); + expect(request.url).to.include('bids[0][sizes][1][width]=300'); + expect(request.url).to.include('bids[0][sizes][1][height]=600'); + }); + + it('use window uuid', () => { + window.mantis_uuid = 'foo'; + + const request = spec.buildRequests(bidRequests); + + expect(request.url).to.include('uuid=foo'); + + delete window.mantis_uuid; + }); + + it('use storage uuid', () => { + window.localStorage.setItem('mantis:uuid', 'bar'); + + const request = spec.buildRequests(bidRequests); + + expect(request.url).to.include('uuid=bar'); + + window.localStorage.removeItem('mantis:uuid'); + }); + + it('detect amp', () => { + var oldContext = window.context; + + window.context = {}; + window.context.tagName = 'AMP-AD'; + window.context.canonicalUrl = 'foo'; + + const request = spec.buildRequests(bidRequests); + + expect(request.url).to.include('amp=true'); + expect(request.url).to.include('url=foo'); + + delete window.context.tagName; + delete window.context.canonicalUrl; + + window.context = oldContext; + }); + }); + + describe('getUserSyncs', () => { + it('iframe', () => { + let result = spec.getUserSyncs({ + iframeEnabled: true + }); + + expect(result[0].type).to.equal('iframe'); + expect(result[0].url).to.include('https://mantodea.mantisadnetwork.com/prebid/iframe'); + }); + + it('pixel', () => { + let result = spec.getUserSyncs({ + pixelEnabled: true + }); + + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.include('https://mantodea.mantisadnetwork.com/prebid/pixel'); + }); + }); + + describe('interpretResponse', () => { + it('display ads returned', () => { + let response = { + body: { + ads: [ + { + bid: 'bid', + cpm: 1, + view: 'view', + width: 300, + height: 250, + html: '' + } + ] + } + }; + + let expectedResponse = [ + { + requestId: 'bid', + cpm: 1, + width: 300, + height: 250, + ttl: 86400, + ad: '', + creativeId: 'view', + netRevenue: true, + currency: 'USD' + } + ]; + let bidderRequest; + + let result = spec.interpretResponse(response, {bidderRequest}); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + + it('no ads returned', () => { + let response = { + body: { + ads: [] + } + }; + let bidderRequest; + + let result = spec.interpretResponse(response, {bidderRequest}); + expect(result.length).to.equal(0); + }); + }); +});