diff --git a/src/debugging.js b/src/debugging.js new file mode 100644 index 000000000000..39ddc92beeed --- /dev/null +++ b/src/debugging.js @@ -0,0 +1,89 @@ + +import { config } from 'src/config'; +import { logMessage as utilsLogMessage, logWarn as utilsLogWarn } from 'src/utils'; +import { addBidResponse } from 'src/auction'; + +const OVERRIDE_KEY = '$$PREBID_GLOBAL$$:debugging'; + +export let boundHook; + +function logMessage(msg) { + utilsLogMessage('DEBUG: ' + msg); +} + +function logWarn(msg) { + utilsLogWarn('DEBUG: ' + msg); +} + +function enableOverrides(overrides, fromSession = false) { + config.setConfig({'debug': true}); + logMessage(`bidder overrides enabled${fromSession ? ' from session' : ''}`); + + if (boundHook) { + addBidResponse.removeHook(boundHook); + } + + boundHook = addBidResponseHook.bind(null, overrides); + addBidResponse.addHook(boundHook, 5); +} + +export function disableOverrides() { + if (boundHook) { + addBidResponse.removeHook(boundHook); + logMessage('bidder overrides disabled'); + } +} + +export function addBidResponseHook(overrides, adUnitCode, bid, next) { + if (Array.isArray(overrides.bidders) && overrides.bidders.indexOf(bid.bidderCode) === -1) { + logWarn(`bidder '${bid.bidderCode}' excluded from auction by bidder overrides`); + return; + } + + if (Array.isArray(overrides.bids)) { + overrides.bids.forEach(overrideBid => { + if (overrideBid.bidder && overrideBid.bidder !== bid.bidderCode) { + return; + } + if (overrideBid.adUnitCode && overrideBid.adUnitCode !== adUnitCode) { + return; + } + + bid = Object.assign({}, bid); + + Object.keys(overrideBid).filter(key => ['bidder', 'adUnitCode'].indexOf(key) === -1).forEach((key) => { + let value = overrideBid[key]; + logMessage(`bidder overrides changed '${adUnitCode}/${bid.bidderCode}' bid.${key} from '${bid[key]}' to '${value}'`); + bid[key] = value; + }); + }); + } + + next(adUnitCode, bid); +} + +export function getConfig(debugging) { + if (!debugging.enabled) { + disableOverrides(); + try { + window.sessionStorage.removeItem(OVERRIDE_KEY); + } catch (e) {} + } else { + try { + window.sessionStorage.setItem(OVERRIDE_KEY, JSON.stringify(debugging)); + } catch (e) {} + enableOverrides(debugging); + } +} +config.getConfig('debugging', ({debugging}) => getConfig(debugging)); + +export function sessionLoader() { + let overrides; + try { + overrides = JSON.parse(window.sessionStorage.getItem(OVERRIDE_KEY)); + } catch (e) { + } + if (overrides) { + enableOverrides(overrides, true); + } +} diff --git a/src/hook.js b/src/hook.js index 6c6cefdc56c7..fef62a37c3d7 100644 --- a/src/hook.js +++ b/src/hook.js @@ -60,6 +60,9 @@ export function createHook(type, fn, hookName) { }, removeHook: function(removeFn) { _hooks = _hooks.filter(hook => hook.fn === fn || hook.fn !== removeFn); + }, + hasHook: function(fn) { + return _hooks.some(hook => hook.fn === fn); } }; diff --git a/src/prebid.js b/src/prebid.js index d703df43bf57..d4fbe17a5014 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -9,6 +9,7 @@ import { config } from './config'; import { auctionManager } from './auctionManager'; import { targeting, getOldestBid, RENDERED, BID_TARGETING_SET } from './targeting'; import { createHook } from 'src/hook'; +import { sessionLoader } from 'src/debugging'; import includes from 'core-js/library/fn/array/includes'; const $$PREBID_GLOBAL$$ = getGlobal(); @@ -27,6 +28,9 @@ const eventValidators = { bidWon: checkDefinedPlacement }; +// initialize existing debugging sessions if present +sessionLoader(); + /* Public vars */ $$PREBID_GLOBAL$$.bidderSettings = $$PREBID_GLOBAL$$.bidderSettings || {}; diff --git a/test/spec/debugging_spec.js b/test/spec/debugging_spec.js new file mode 100644 index 000000000000..39b08075e060 --- /dev/null +++ b/test/spec/debugging_spec.js @@ -0,0 +1,141 @@ + +import { expect } from 'chai'; +import { sessionLoader, addBidResponseHook, getConfig, disableOverrides, boundHook } from 'src/debugging'; +import { addBidResponse } from 'src/auction'; +import { config } from 'src/config'; + +describe('bid overrides', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('initialization', () => { + beforeEach(() => { + sandbox.stub(config, 'setConfig'); + sandbox.stub(window.sessionStorage, 'setItem'); + sandbox.stub(window.sessionStorage, 'removeItem'); + }); + + afterEach(() => { + disableOverrides(); + }); + + it('should happen when enabled with setConfig', () => { + getConfig({ + enabled: true + }); + + expect(addBidResponse.hasHook(boundHook)).to.equal(true); + }); + + it('should happen when configuration found in sessionStorage', () => { + sandbox.stub(window.sessionStorage, 'getItem').returns('{"enabled": true}'); + + sessionLoader(); + expect(addBidResponse.hasHook(boundHook)).to.equal(true); + }); + + it('should not throw if sessionStorage is inaccessible', () => { + sandbox.stub(window.sessionStorage, 'getItem').throws(); + + expect(() => { + sessionLoader(); + }).not.to.throw(); + }); + }); + + describe('hook', () => { + let mockBids; + let bids; + + beforeEach(() => { + let baseBid = { + 'bidderCode': 'rubicon', + 'width': 970, + 'height': 250, + 'statusMessage': 'Bid available', + 'mediaType': 'banner', + 'source': 'client', + 'currency': 'USD', + 'cpm': 0.5, + 'ttl': 300, + 'netRevenue': false, + 'adUnitCode': '/19968336/header-bid-tag-0' + }; + mockBids = []; + mockBids.push(baseBid); + mockBids.push(Object.assign({}, baseBid, { + bidderCode: 'appnexus' + })); + + bids = []; + }); + + function run(overrides) { + mockBids.forEach(bid => { + addBidResponseHook(overrides, bid.adUnitCode, bid, (adUnitCode, bid) => { + bids.push(bid); + }) + }); + } + + it('should allow us to exclude bidders', () => { + run({ + enabled: true, + bidders: ['appnexus'] + }); + + expect(bids.length).to.equal(1); + expect(bids[0].bidderCode).to.equal('appnexus'); + }); + + it('should allow us to override all bids', () => { + run({ + enabled: true, + bids: [{ + cpm: 2 + }] + }); + + expect(bids.length).to.equal(2); + expect(bids[0].cpm).to.equal(2); + expect(bids[1].cpm).to.equal(2); + }); + + it('should allow us to override bids by bidder', () => { + run({ + enabled: true, + bids: [{ + bidder: 'rubicon', + cpm: 2 + }] + }); + + expect(bids.length).to.equal(2); + expect(bids[0].cpm).to.equal(2); + expect(bids[1].cpm).to.equal(0.5); + }); + + it('should allow us to override bids by adUnitCode', () => { + mockBids[1].adUnitCode = 'test'; + + run({ + enabled: true, + bids: [{ + adUnitCode: 'test', + cpm: 2 + }] + }); + + expect(bids.length).to.equal(2); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[1].cpm).to.equal(2); + }); + }); +});