From 40ef0734084f6002f2c2a03e659d65fe094bdd69 Mon Sep 17 00:00:00 2001 From: uriw Date: Tue, 14 Mar 2017 19:45:34 +0200 Subject: [PATCH 1/4] - New Adaptor - Inneractive --- adapters.json | 1 + src/adapters/inneractive.js | 488 +++++++++++++++++++++++++ test/spec/adapters/inneractive_spec.js | 295 +++++++++++++++ 3 files changed, 784 insertions(+) create mode 100644 src/adapters/inneractive.js create mode 100644 test/spec/adapters/inneractive_spec.js diff --git a/adapters.json b/adapters.json index e9a65ac4fa6..6263431737d 100644 --- a/adapters.json +++ b/adapters.json @@ -20,6 +20,7 @@ "indexExchange", "kruxlink", "getintent", + "inneractive", "komoona", "lifestreet", "mantis", diff --git a/src/adapters/inneractive.js b/src/adapters/inneractive.js new file mode 100644 index 00000000000..0d94d089944 --- /dev/null +++ b/src/adapters/inneractive.js @@ -0,0 +1,488 @@ +import * as utils from '../utils'; +import Adapter from './adapter'; +import {ajax} from '../ajax'; +import bidManager from 'src/bidmanager'; +import bidFactory from 'src/bidfactory'; +import {STATUS} from 'src/constants'; +import {EVENTS} from 'src/constants'; +import {formatQS} from '../url'; + +/** + * @type {{IA_JS: string, ADAPTER_NAME: string, V: string, RECTANGLE_SIZE: {W: number, H: number}, SPOT_TYPES: {INTERSTITIAL: string, RECTANGLE: string, FLOATING: string, BANNER: string}, DISPLAY_AD: number, ENDPOINT_URL: string, EVENTS_ENDPOINT_URL: string, RESPONSE_HEADERS_NAME: {PRICING_VALUE: string, AD_H: string, AD_W: string}}} + */ +const CONSTANTS = { + ADAPTER_NAME: 'inneractive', + V: 'IA-JS-HB-PBJS-1.0', + RECTANGLE_SIZE:{W: 300, H: 250}, + + SPOT_TYPES: { + INTERSTITIAL: 'interstitial', + RECTANGLE: 'rectangle', + FLOATING: 'floating', + BANNER: 'banner' + }, + + DISPLAY_AD: 20, + ENDPOINT_URL: '//ad-tag.inner-active.mobi/simpleM2M/requestJsonAd', + EVENTS_ENDPOINT_URL: '//vast-events.inner-active.mobi/Event', + RESPONSE_HEADERS_NAME: { + PRICING_VALUE: 'X-IA-Pricing-Value', + AD_H: 'X-IA-Ad-Height', + AD_W: 'X-IA-Ad-Width' + } +}; + +let IaWindow; +try{ + IaWindow = window.top; +}catch(e){ + IaWindow = window; +} + +/** + * gloable util functions + * @type {{defaultsQsParams: {v: (string|string), page: string, mw: boolean, hb: string}, stringToCamel: (function(*)), objectToCamel: (function(*=))}} + */ +const Helpers = { + defaultsQsParams: {v: CONSTANTS.V,page: encodeURIComponent(utils.getTopWindowUrl()),mw: true, hb: 'prebidjs'}, + /** + * Change string format from underscore to camelcase (e.g., APP_ID to appId) + * @param str: string + * @returns string + */ + stringToCamel(str){ + if(str.indexOf('_') === -1){ + const first = str.charAt(0); + if(first !== first.toLowerCase()){ + str = str.toLowerCase(); + } + return str; + } + + str = str.toLowerCase(); + return str.replace(/(\_[a-z])/g, $1 => $1.toUpperCase().replace('_','')); + }, + + /** + * Change all object keys string format from underscore to camelcase (e.g., {'APP_ID' : ...} to {'appId' : ...}) + * @param params: object + * @returns object + */ + objectToCamel(params){ + Object.keys(params).forEach(key => { + const keyCamelCase = this.stringToCamel(key); + if(keyCamelCase !== key){ + params[keyCamelCase] = params[key]; + delete params[key]; + } + }); + return params; + } +}; + +/** + * Tracking pixels for events + * @type {{fire: (function(*=))}} + */ +const Tracker = { + /** + * Creates a tracking pixel + * @param urls: Array + */ + fire(urls){ + urls.forEach(url => url && ((new Image(1,1)).src = encodeURI(url))); + } +}; + +/** + * Analytics + * @type {{errorEventName: string, pageProtocol: string, getPageProtocol: (function(): string), getEventUrl: (function(*, *=)), reportEvent: (function(string, Object)), defaults: {v: (string|string), page: string, mw: boolean, hb: string}, eventQueryStringParams: (function(Object): string), createTrackingPixel: (function(string))}} + */ +const Reporter = { + /** + * @private + */ + errorEventName: 'HBPreBidError', + pageProtocol: '', + + /** + * Gets the page protocol based on the document.location.protocol + * The returned string is either http:// or https:// + * @returns {string} + */ + getPageProtocol(){ + if(!this.pageProtocol){ + this.pageProtocol = ('http:' === utils.getTopWindowLocation().protocol ? 'http:' : 'https:'); + } + return this.pageProtocol; + }, + + getEventUrl(evtName, extraDetails){ + let eventsEndpoint = CONSTANTS.EVENTS_ENDPOINT_URL + '?table=' + ((evtName === this.errorEventName) ? 'mbwError' : 'mbwEvent'); + let queryStringParams = this.eventQueryStringParams(extraDetails); + const appId = extraDetails && extraDetails.appId; + let queryStringParamsWithAID = `${queryStringParams}&aid=${appId}_${evtName}_other&evtName=${evtName}`; + return eventsEndpoint + '&' + queryStringParamsWithAID; + }, + + /** + * Reports an event to IA's servers. + * @param {string} evtName - event name as string. + * @param {object} extraDetails - e.g., a JS exception JSON object. + * @param shouldSendOnlyToNewEndpoint + */ + reportEvent(evtName, extraDetails) { + const url = this.getEventUrl(evtName, extraDetails); + this.createTrackingPixel(url); + }, + defaults: Helpers.defaultsQsParams, + + /** + * Ia Event Reporting Query String Parameters, not including App Id. + * @param {object} extraDetails - e.g., a JS exception JSON object. + * @return {string} IA event contcatenated queryString parameters. + */ + eventQueryStringParams(extraDetails) { + const toQS = Object.assign({}, this.defaults, {realAppId: extraDetails && extraDetails.appId, timestamp: Date.now()}); + return formatQS(toQS); + }, + + /** + * Creates a tracking pixel by prepending the page's protocol to the URL sent as the param. + * @param {string} urlWithoutProtocol - the URL to send the tracking pixel to, without the protocol as a prefix. + */ + createTrackingPixel(urlWithoutProtocol) { + Tracker.fire([this.getPageProtocol() + urlWithoutProtocol]); + } +}; + +/** + * Url generator - generates a request URL + * @type {{defaultsParams: *, serverParamNameBySettingParamName: {referrer: string, keywords: string, appId: string, portal: string, age: string, gender: string, isSecured: (boolean|null)}, toServerParams: (function(*)), unwantedValues: *[], getUrlParams: (function(*=))}} + */ +const Url = { + defaultsParams: Object.assign({}, Helpers.defaultsQsParams, {f: CONSTANTS.DISPLAY_AD,fs: false,ref: IaWindow.document.referrer}), + serverParamNameBySettingParamName: { + referrer: 'ref', + keywords: 'k', + appId: 'aid', + portal: 'po', + age: 'a', + gender: 'g', + }, + unwantedValues: ['', null, undefined], + + /** + * Maps publisher params to server params + * @param params: object {k:v} + * @returns object {k:v} + */ + toServerParams(params){ + const serverParams = {}; + for(const paramName in params){ + if(params.hasOwnProperty(paramName) && this.serverParamNameBySettingParamName.hasOwnProperty(paramName)){ + serverParams[this.serverParamNameBySettingParamName[paramName]] = params[paramName]; + }else{ + serverParams[paramName] = params[paramName]; + } + } + + serverParams.isSecured = Reporter.getPageProtocol() === 'https:' || null; + return serverParams; + }, + + /** + * Prepare querty string to ad server + * @param params: object {k:v} + * @returns : object {k:v} + */ + getUrlParams(params){ + const serverParams = this.toServerParams(params); + const toQueryString = Object.assign({}, this.defaultsParams, serverParams); + for(const paramName in toQueryString){ + if(toQueryString.hasOwnProperty(paramName) && this.unwantedValues.indexOf(toQueryString[paramName]) !== -1){ + delete toQueryString[paramName]; + } + } + toQueryString.fs = params.spotType === CONSTANTS.SPOT_TYPES.INTERSTITIAL; + + if(params.spotType === CONSTANTS.SPOT_TYPES.RECTANGLE){ + toQueryString.rw = CONSTANTS.RECTANGLE_SIZE.W; + toQueryString.rh = CONSTANTS.RECTANGLE_SIZE.H; + } + + if (typeof $$PREBID_GLOBAL$$ !== 'undefined') { + toQueryString.bco = $$PREBID_GLOBAL$$.cbTimeout || $$PREBID_GLOBAL$$.bidderTimeout; + } + + toQueryString.timestamp = Date.now(); + delete toQueryString.qa; + return toQueryString; + } +}; + +/** + * Http helper to extract metadata + * @type {{headers: *[], getBidHeaders: (function(*))}} + */ +const Http = { + headers: [ + CONSTANTS.RESPONSE_HEADERS_NAME.PRICING_VALUE, + CONSTANTS.RESPONSE_HEADERS_NAME.AD_H, + CONSTANTS.RESPONSE_HEADERS_NAME.AD_W + ], + + /** + * Extract headers data + * @param xhr: XMLHttpRequest + * @returns {} + */ + getBidHeaders(xhr){ + const headersData = {}; + this.headers.forEach(headerName => headersData[headerName] = xhr.getResponseHeader(headerName)); + return headersData; + } +}; + + +/** + * InnerActiveAdapter for requesting bids + * @class + */ +class InnerActiveAdapter{ + constructor(){ + this.iaAdapter = Adapter.createNew(CONSTANTS.ADAPTER_NAME); + this.bidByBidId = {}; + } + + /** + * validate if bid request is valid + * @param adSettings: object + * @returns {boolean} + * @private + */ + _isValidRequest(adSettings){ + if(adSettings && adSettings.appId && adSettings.spotType){ + return true; + } + utils.logError('bid requires appId'); + return false; + } + + /** + * Store the bids in a Map object (k: bidId, v: bid)to check later if won + * @param bid + * @returns bid object + * @private + */ + _storeBidRequestDetails(bid){ + this.bidByBidId[bid.bidId] = bid; + return bid; + } + + /** + * @param bidStatus: int ("STATUS": {"GOOD": 1,"NO_BID": 2}) + * @param bidResponse: object + * @returns {type[]} + * @private + */ + _getBidDetails(bidStatus, bidResponse, bidId){ + let bid = bidFactory.createBid(bidStatus, bidResponse); + bid.code = CONSTANTS.ADAPTER_NAME; + bid.bidderCode = bid.code; + if (bidStatus === STATUS.GOOD) { + bid = Object.assign(bid, bidResponse); + this._setBidCpm(bid, bidId); + } + return bid; + } + + _setBidCpm(bid, bidId){ + const storedBid = this.bidByBidId[bidId]; + if(storedBid){ + bid.cpm = storedBid.params && storedBid.params.qa && storedBid.params.qa.cpm || bid.cpm; + bid.cpm = (bid.cpm !== null && !isNaN(bid.cpm)) ? parseFloat(bid.cpm) : 0.0; + } + } + + /** + * Validate if response is valid + * @param responseAsJson : object + * @param headersData: {} + * @returns {boolean} + * @private + */ + _isValidBidResponse(responseAsJson, headersData){ + return (responseAsJson && responseAsJson.ad && responseAsJson.ad.html && headersData && headersData[CONSTANTS.RESPONSE_HEADERS_NAME.PRICING_VALUE] > 0); + } + + /** + * When response is received + * @param response: string(json format) + * @param xhr: XMLHttpRequest + * @param bidId: string + * @private + */ + _onResponse(response, xhr, bidId){ + const bid = this.bidByBidId[bidId]; + const [w, h] = bid.sizes[0]; + const size = {w, h}; + let responseAsJson; + const headersData = Http.getBidHeaders(xhr); + try { + responseAsJson = JSON.parse(response); + } catch (error) { + utils.logError(error); + } + + if (!this._isValidBidResponse(responseAsJson, headersData)) { + let errorMessage = `response failed for ${CONSTANTS.ADAPTER_NAME} adapter`; + utils.logError(errorMessage); + const passback = responseAsJson && responseAsJson.config && responseAsJson.config.passback; + if(passback) { + Tracker.fire([passback]); + } + Reporter.reportEvent('HBPreBidNoAd', bid.params); + return bidManager.addBidResponse(bid.placementCode, this._getBidDetails(STATUS.NO_BID)); + } + const bidResponse = { + cpm: headersData[CONSTANTS.RESPONSE_HEADERS_NAME.PRICING_VALUE]*1000, + width: parseFloat(headersData[CONSTANTS.RESPONSE_HEADERS_NAME.AD_W]) || size.w, + ad: this._getAd(responseAsJson.ad.html, responseAsJson.config.tracking, bid.params), + height: parseFloat(headersData[CONSTANTS.RESPONSE_HEADERS_NAME.AD_H]) || size.h + }; + const auctionBid = this._getBidDetails(STATUS.GOOD, bidResponse, bidId); + bid.adId = auctionBid.adId; + this.bidByBidId[bidId] = bid; + bidManager.addBidResponse(bid.placementCode, auctionBid); + } + + /** + * Returns the ad HTML template + * @param adHtml: string {ad server creative} + * @param tracking: object {impressions, clicks} + * @param bidParams: object + * @returns {string}: create template + * @private + */ + _getAd(adHtml, tracking, bidParams){ + + let impressionsHtml = ''; + if(tracking && Array.isArray(tracking.impressions)){ + let impressions = tracking.impressions; + impressions.push(Reporter.getEventUrl('HBPreBidImpression', bidParams, false)); + impressions.forEach(impression => impression && (impressionsHtml += utils.createTrackPixelHtml(impression))); + } + adHtml = impressionsHtml + adHtml.replace(/ + + + + +
${adHtml}
+ + + `; + return adTemplate; + } + /** + * Adjust bid params to ia-ad-server params + * @param bid: object + * @private + */ + _toIaBidParams(bid){ + const bidParamsWithCustomParams = Object.assign({}, bid.params, bid.params.customParams); + delete bidParamsWithCustomParams.customParams; + bid.params = Helpers.objectToCamel(bidParamsWithCustomParams); + } + + /** + * Prebid executes for stating an auction + * @param bidRequest: object + */ + callBids(bidRequest){ + const bids = bidRequest.bids || []; + bids.forEach(bid => this._toIaBidParams(bid)); + bids + .filter(bid => this._isValidRequest(bid.params)) + .map(bid => this._storeBidRequestDetails(bid)) + .forEach(bid => ajax(this._getEndpointUrl(bid.params), (response, xhr) => this._onResponse(response, xhr, bid.bidId), Url.getUrlParams(bid.params), {method: 'GET'})); + this._checkIfBidWon(); + } + + _getEndpointUrl(params){ + return params && params.qa && params.qa.url || Reporter.getPageProtocol() + CONSTANTS.ENDPOINT_URL; + } + + _getStoredBids(){ + const storedBids = []; + for(const bidId in this.bidByBidId){ + if(this.bidByBidId.hasOwnProperty(bidId)) { + storedBids.push(this.bidByBidId[bidId]); + } + } + return storedBids; + } + + /** + * Reports an analytics on bid no win when no ad is received + */ + _checkIfBidWon(){ + if (typeof $$PREBID_GLOBAL$$ === 'undefined') { + return; + } + const wonAdIds = []; + const numOfUnits = $$PREBID_GLOBAL$$.adUnits.length; + const _reportNoBidWon = () => this._getStoredBids().forEach(bid => !wonAdIds.includes(bid.adId) && Reporter.reportEvent('HBNoWin', bid.params)); + let timer = null; + $$PREBID_GLOBAL$$.onEvent(EVENTS.AUCTION_END, () => timer = setTimeout(_reportNoBidWon, $$PREBID_GLOBAL$$.bidderTimeout), null); + $$PREBID_GLOBAL$$.onEvent(EVENTS.BID_WON, (bid) => { + wonAdIds.push(bid.adId); + if(numOfUnits === wonAdIds.length){ + if(timer) { + clearTimeout(timer); + timer = null; + } + _reportNoBidWon(); + } + }, null); + } + + /** + * Return internal object - testing + * @returns {{Reporter: {errorEventName: string, pageProtocol: string, getPageProtocol: (function(): string), getEventUrl: (function(*, *=)), reportEvent: (function(string, Object)), defaults: {v: (string|string), page: string, mw: boolean, hb: string}, eventQueryStringParams: (function(Object): string), createTrackingPixel: (function(string))}}} + * @private + */ + static _getUtils(){ + return {Reporter}; + } + + /** + * Creates new instance of InnerActiveAdapter for prebid auction + * @returns {InnerActiveAdapter} + */ + static createNew(){ + return new InnerActiveAdapter(); + } +} +module.exports = InnerActiveAdapter; diff --git a/test/spec/adapters/inneractive_spec.js b/test/spec/adapters/inneractive_spec.js new file mode 100644 index 00000000000..819e01b0139 --- /dev/null +++ b/test/spec/adapters/inneractive_spec.js @@ -0,0 +1,295 @@ +/* globals context */ + +import {expect} from 'chai'; +import {default as InneractiveAdapter} from 'src/adapters/inneractive'; +import bidmanager from 'src/bidmanager'; + + +// Using plain-old-style functions, why? see: http://mochajs.org/#arrow-functions +describe('InneractiveAdapter', function () { + let adapter, + bidRequest; + + beforeEach(function () { + adapter = InneractiveAdapter.createNew(); + bidRequest = { + bidderCode: "inneractive", + bids: [ + { + bidder: "inneractive", + params: { + appId: "", + }, + placementCode: "div-gpt-ad-1460505748561-0", + sizes: [[300, 250], [300, 600]], + bidId: "507e8db167d219", + bidderRequestId: "49acc957f92917", + requestId: "51381cd0-c29c-405b-9145-20f60abb1e76" + }, + { + bidder: "inneractive", + params: { + noappId: "...", + }, + placementCode: "div-gpt-ad-1460505661639-0", + sizes: [[728, 90], [970, 90]], + bidId: "507e8db167d220", + bidderRequestId: "49acc957f92917", + requestId: "51381cd0-c29c-405b-9145-20f60abb1e76" + }, + { + bidder: "inneractive", + params: { + APP_ID: "Inneractive_AndroidHelloWorld_Android", + spotType: "rectangle", + customParams: { + Portal: 7002, + } + }, + placementCode: "div-gpt-ad-1460505748561-0", + sizes: [[320, 50], [300, 600]], + bidId: "507e8db167d221", + bidderRequestId: "49acc957f92917", + requestId: "51381cd0-c29c-405b-9145-20f60abb1e76" + }, + { + bidder: "inneractive", + params: { + appId: "Inneractive_IosHelloWorld_iPhone", + spotType: "banner", // Just for coverage considerations, no real impact in production + customParams: { + portal: 7001, + gender: '' + } + }, + placementCode: "div-gpt-ad-1460505661639-0", + sizes: [[728, 90], [970, 90]], + bidId: "507e8db167d222", + bidderRequestId: "49acc957f92917", + requestId: "51381cd0-c29c-405b-9145-20f60abb1e76" + }] + }; + }); + + describe('Reporter', function () { + context('on HBPreBidError event', function () { + it('should contain "mbwError" the inside event report url', function () { + const Reporter = InneractiveAdapter._getUtils().Reporter; + const extraDetailsParam = { + "appId": "CrunchMind_DailyDisclosure_other", + "spotType": "rectangle", + "portal": 7002 + }; + let eventReportUrl = Reporter.getEventUrl('HBPreBidError', extraDetailsParam); + expect(eventReportUrl).to.include('mbwError'); + }); + }); + }); + + describe('.createNew()', function () { + it('should return an instance of this adapter having a "callBids" method', function () { + expect(adapter) + .to.be.instanceOf(InneractiveAdapter).and + .to.have.property('callBids').and + .to.be.a('function'); + }); + }); + + describe('when sending out bid requests to the ad server', function () { + let bidRequests, + xhr; + + beforeEach(function () { + bidRequests = []; + xhr = sinon.useFakeXMLHttpRequest(); + xhr.onCreate = (request) => { + bidRequests.push(request); + }; + }); + + afterEach(function () { + xhr.restore(); + }); + + context('when there are no bid requests', function () { + it('should not issue a request', function () { + const Reporter = InneractiveAdapter._getUtils().Reporter; + Reporter.getEventUrl('HBPreBidError', { + "appId": "CrunchMind_DailyDisclosure_other", + "spotType": "rectangle", + "portal": 7002 + }); + + delete bidRequest.bids; + adapter.callBids(bidRequest); + + expect(bidRequests).to.be.empty; // jshint ignore:line + }); + }); + + context('when there is at least one bid request', function () { + it('should filter out invalid bids', function () { + const INVALID_BIDS_COUNT = 2; + sinon.spy(adapter, '_isValidRequest'); + adapter.callBids(bidRequest); + + + for (let id = 0; id < INVALID_BIDS_COUNT; id++) { + expect(adapter._isValidRequest.getCall(id).returned(false)).to.be.true; // jshint ignore:line + } + + adapter._isValidRequest.restore(); + }); + + it('should store all valid bids internally', function () { + adapter.callBids(bidRequest); + expect(Object.keys(adapter.bidByBidId).length).to.equal(2); + }); + + it('should issue ad requests to the ad server for every valid bid', function () { + adapter.callBids(bidRequest); + expect(bidRequests).to.have.lengthOf(2); + }); + }); + }); + + describe('when registering the bids that are returned with Prebid.js', function () { + const BID_DETAILS_ARG_INDEX = 1; + let server; + + beforeEach(function () { + sinon.stub(bidmanager, 'addBidResponse'); + server = sinon.fakeServer.create(); + }); + + afterEach(function () { + server.restore(); + bidmanager.addBidResponse.restore(); + }); + + context('when the bid is valid', function () { + let adServerResponse, + headers, + body; + + beforeEach(function () { + adServerResponse = { + headers: { + "X-IA-Ad-Height": 250, + "X-IA-Ad-Width": 300, + "X-IA-Error": "OK", + "X-IA-Pricing": "CPM", + "X-IA-Pricing-Currency": "USD", + "X-IA-Pricing-Value": 0.0005 + }, + body: { + ad: { + html: "
" + }, + config: { + tracking: { + impressions: [ + "http://event.inner-active.mobi/simpleM2M/reportEvent?eventArchetype=impress…pe=3&network=Inneractive_CS&acp=&pcp=&secure=false&rtb=false&houseAd=false" + ], + clicks: [ + "http://event.inner-active.mobi/simpleM2M/reportEvent?eventArchetype=richMed…pe=3&network=Inneractive_CS&acp=&pcp=&secure=false&rtb=false&houseAd=false", + "" + ], + passback: "http://event.inner-active.mobi/simpleM2M/reportEvent?eventArchetype=passbac…pe=3&network=Inneractive_CS&acp=&pcp=&secure=false&rtb=false&houseAd=false" + }, + moat: { + countryCode: "IL" + } + } + } + }; + headers = adServerResponse.headers; + body = JSON.stringify(adServerResponse.body); + }); + + it('should register bid responses with a status code of 1', function () { + server.respondWith([200, headers, body]); + adapter.callBids(bidRequest); + server.respond(); + + let firstRegisteredBidResponse = bidmanager.addBidResponse.firstCall.args[BID_DETAILS_ARG_INDEX]; + expect(firstRegisteredBidResponse) + .to.have.property('statusMessage', 'Bid available'); + }); + + it('should use the first element inside the bid request size array when no (width,height) is returned within the headers', function () { + delete headers['X-IA-Ad-Height']; + delete headers['X-IA-Ad-Width']; + server.respondWith([200, headers, body]); + adapter.callBids(bidRequest); + server.respond(); + + let firstRegisteredBidResponse = bidmanager.addBidResponse.firstCall.args[BID_DETAILS_ARG_INDEX]; + expect(firstRegisteredBidResponse).to.have.property('width', 320); + expect(firstRegisteredBidResponse).to.have.property('height', 50); + }); + }); + + context('when the bid is invalid', function () { + let passbackAdServerResponse, + headers, + body; + + beforeEach(function () { + passbackAdServerResponse = { + headers: { + "X-IA-Error": "House Ad", + "X-IA-Content": 600145, + "X-IA-Cid": 99999, + "X-IA-Publisher": 206536, + "Content-Type": "application/json; charset=UTF-8", + "X-IA-Session": 6512147119979250840, + "X-IA-AdNetwork": "inneractive360" + }, + body: { + "ad": { + "html": "" + }, + "config": { + "passback": "http://event.inner-active.mobi/simpleM2M/reportEvent?eventArchetype=passbac…pe=3&network=Inneractive_CS&acp=&pcp=&secure=false&rtb=false&houseAd=false" + } + } + }; + headers = passbackAdServerResponse.headers; + body = JSON.stringify(passbackAdServerResponse.body); + }); + + it('should register bid responses with a status code of 2', function () { + server.respondWith([200, headers, body]); + adapter.callBids(bidRequest); + server.respond(); + + let firstRegisteredBidResponse = bidmanager.addBidResponse.firstCall.args[BID_DETAILS_ARG_INDEX]; + expect(firstRegisteredBidResponse) + .to.have.property('statusMessage', 'Bid returned empty or error response'); + }); + + it('should handle responses from our server in case we had no ad to offer', function () { + const n = bidRequest.bids.length; + bidRequest.bids[n - 1].params.appId = "Komoona_InquisitrRectangle2_other"; + server.respondWith([200, headers, body]); + adapter.callBids(bidRequest); + server.respond(); + + let secondRegisteredBidResponse = bidmanager.addBidResponse.secondCall.args[BID_DETAILS_ARG_INDEX]; + expect(secondRegisteredBidResponse) + .to.have.property('statusMessage', 'Bid returned empty or error response'); + }); + + it('should handle JSON.parse errors', function () { + server.respondWith(''); + adapter.callBids(bidRequest); + server.respond(); + + const firstRegisteredBidResponse = bidmanager.addBidResponse.firstCall.args[BID_DETAILS_ARG_INDEX]; + expect(firstRegisteredBidResponse) + .to.have.property('statusMessage', 'Bid returned empty or error response'); + }); + }); + }); +}); From 4a98b4db0f5d91b962bc39a2908606447f09c13e Mon Sep 17 00:00:00 2001 From: uriw Date: Thu, 30 Mar 2017 15:58:37 +0300 Subject: [PATCH 2/4] refactor: removed checkIfBidWon call and method --- src/adapters/inneractive.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/adapters/inneractive.js b/src/adapters/inneractive.js index 0d94d089944..a8a6f56050c 100644 --- a/src/adapters/inneractive.js +++ b/src/adapters/inneractive.js @@ -427,7 +427,6 @@ class InnerActiveAdapter{ .filter(bid => this._isValidRequest(bid.params)) .map(bid => this._storeBidRequestDetails(bid)) .forEach(bid => ajax(this._getEndpointUrl(bid.params), (response, xhr) => this._onResponse(response, xhr, bid.bidId), Url.getUrlParams(bid.params), {method: 'GET'})); - this._checkIfBidWon(); } _getEndpointUrl(params){ @@ -444,30 +443,6 @@ class InnerActiveAdapter{ return storedBids; } - /** - * Reports an analytics on bid no win when no ad is received - */ - _checkIfBidWon(){ - if (typeof $$PREBID_GLOBAL$$ === 'undefined') { - return; - } - const wonAdIds = []; - const numOfUnits = $$PREBID_GLOBAL$$.adUnits.length; - const _reportNoBidWon = () => this._getStoredBids().forEach(bid => !wonAdIds.includes(bid.adId) && Reporter.reportEvent('HBNoWin', bid.params)); - let timer = null; - $$PREBID_GLOBAL$$.onEvent(EVENTS.AUCTION_END, () => timer = setTimeout(_reportNoBidWon, $$PREBID_GLOBAL$$.bidderTimeout), null); - $$PREBID_GLOBAL$$.onEvent(EVENTS.BID_WON, (bid) => { - wonAdIds.push(bid.adId); - if(numOfUnits === wonAdIds.length){ - if(timer) { - clearTimeout(timer); - timer = null; - } - _reportNoBidWon(); - } - }, null); - } - /** * Return internal object - testing * @returns {{Reporter: {errorEventName: string, pageProtocol: string, getPageProtocol: (function(): string), getEventUrl: (function(*, *=)), reportEvent: (function(string, Object)), defaults: {v: (string|string), page: string, mw: boolean, hb: string}, eventQueryStringParams: (function(Object): string), createTrackingPixel: (function(string))}}} From 423ffb36ce42499f67f563e4841976f7ae9aff2e Mon Sep 17 00:00:00 2001 From: uriw Date: Thu, 30 Mar 2017 16:44:56 +0300 Subject: [PATCH 3/4] refactor: also removing EVENTS reference from src/constants --- src/adapters/inneractive.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/adapters/inneractive.js b/src/adapters/inneractive.js index a8a6f56050c..68c395c8e47 100644 --- a/src/adapters/inneractive.js +++ b/src/adapters/inneractive.js @@ -4,7 +4,6 @@ import {ajax} from '../ajax'; import bidManager from 'src/bidmanager'; import bidFactory from 'src/bidfactory'; import {STATUS} from 'src/constants'; -import {EVENTS} from 'src/constants'; import {formatQS} from '../url'; /** From 663f547afbdcc34c3b9e25913002daec9fd46061 Mon Sep 17 00:00:00 2001 From: uriw Date: Sat, 1 Apr 2017 20:18:22 +0300 Subject: [PATCH 4/4] Changed referrer to match access recommendations --- src/adapters/inneractive.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/adapters/inneractive.js b/src/adapters/inneractive.js index 68c395c8e47..a4b24eeceec 100644 --- a/src/adapters/inneractive.js +++ b/src/adapters/inneractive.js @@ -31,11 +31,11 @@ const CONSTANTS = { } }; -let IaWindow; +let iaRef; try{ - IaWindow = window.top; + iaRef = window.top.document.referrer; }catch(e){ - IaWindow = window; + iaRef = window.document.referrer; } /** @@ -160,7 +160,7 @@ const Reporter = { * @type {{defaultsParams: *, serverParamNameBySettingParamName: {referrer: string, keywords: string, appId: string, portal: string, age: string, gender: string, isSecured: (boolean|null)}, toServerParams: (function(*)), unwantedValues: *[], getUrlParams: (function(*=))}} */ const Url = { - defaultsParams: Object.assign({}, Helpers.defaultsQsParams, {f: CONSTANTS.DISPLAY_AD,fs: false,ref: IaWindow.document.referrer}), + defaultsParams: Object.assign({}, Helpers.defaultsQsParams, {f: CONSTANTS.DISPLAY_AD,fs: false,ref: iaRef}), serverParamNameBySettingParamName: { referrer: 'ref', keywords: 'k',