diff --git a/README.md b/README.md index 3f754667928..c6fb5bc4d9c 100644 --- a/README.md +++ b/README.md @@ -1,200 +1,202 @@ -[![Build Status](https://travis-ci.org/prebid/Prebid.js.svg?branch=master)](https://travis-ci.org/prebid/Prebid.js) -[![Percentage of issues still open](http://isitmaintained.com/badge/open/prebid/Prebid.js.svg)](http://isitmaintained.com/project/prebid/Prebid.js "Percentage of issues still open") -[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/prebid/Prebid.js.svg)](http://isitmaintained.com/project/prebid/Prebid.js "Average time to resolve an issue") -[![Code Climate](https://codeclimate.com/github/prebid/Prebid.js/badges/gpa.svg)](https://codeclimate.com/github/prebid/Prebid.js) - -Prebid.js -======== - -> Setup and manage header bidding advertising partners without writing code or confusing line items. Prebid.js is open source and free. - -Many SSPs, bidders, and publishers have all contributed to this project. - -Check out the overview and documentation at http://prebid.org. - -No more week-long development. Header bidding is made easy by prebid.js :) - -**Table of Contents** - -- [Prebid.js](#) - - [Usage](#usage) - - [Download the latest released code](#download-the-latest-released-code) - - [Example code](#example-code) - - [API](#api) - - [Contribute](#contribute) - - [Add a Bidder Adapter](#add-a-bidder-adapter) - - [install](#install) - - [Build](#build) - - [Configure](#configure) - - [Run](#run) - - -Usage ----------- -Download the integration example [here](https://github.com/prebid/Prebid.js/blob/master/integrationExamples/gpt/pbjs_example_gpt.html). - -### Download the latest released code ### -[See the releases page here](https://github.com/prebid/Prebid.js/releases) and download a copy. - -### Example code ### - -**Include the prebid.js library** -Note that you need to host `prebid.js` locally or on a CDN and update the reference in the code snippet below for `cdn.host.com/prebid.min.js -```javascript -(function () { - var d = document; - var pbs = d.createElement('script'); - pbs.type = 'text/javascript'; - //replace with your CDN hosted version. HTTPS is strongly recommended. - pbs.src = '//cdn.host.com/prebid.min.js'; - var target = d.getElementsByTagName('script')[0]; - target.parentNode.insertBefore(pbs, target); -})(); -``` - -**Setup ad units** -```javascript -pbjs.que.push(function(){ - var adUnits = [{ - code: '{id}', - sizes: [[300, 250], [300, 600]], - bids: [ - { - bidder: 'appnexus', - params: { - placementId: '{id}' - } - } - ] - }]; - //add the adUnits - pbjs.addAdUnits(adUnits); -}); -``` - -**Request Bids** -```javascript -pbjs.que.push(function(){ - pbjs.requestBids({ - bidsBackHandler: function(bidResponses) { - //do stuff when the bids are back - } - }) -}); -``` - -**See Console Debug Errors during testing** -By default console errors are suppressed. To enabled add `?pbjs_debug=true` to the end of the URL - for testing. - -API ----------- -Full Developer API reference: - -[Click here to access the API](http://prebid.org/dev-docs/publisher-api-reference.html) - -Contribute ----------- -**Note:** You need to have at least `node.js 4.x` or greater installed to be able to run the gulp build commands. - -### Add a Bidder Adapter ### -Follow the [guide outlined here](http://prebid.org/dev-docs/bidder-adaptor.html) to add an adapter. - -### Install ### - $ npm install - -If you experience errors, after a version update, try a fresh install: - - $ rm -rf ./node_modules && npm cache clean && npm install - -### Build ### - $ gulp build - -Runs code quality checks, generates prebid.js files and creates zip archive distributable: - - `./build/dev/prebid.js` Full source code for dev and debug - `./build/dev/prebid.js.map` Source map for dev and debug - `./build/dist/prebid.js` Minified production code - `./prebid.js_.zip` Distributable - -Code quality is defined by `./.jscs` and `./.jshint` files and errors are reported in the -terminal. The build will continue with quality errors, however. If you are contributing code -you can configure your editor with the provided .jscs and .jshint settings. - -### Configure ### -Edit example file `./integrationExamples/gpt/pbjs_example_gpt.html`: - -1. Change `{id}` values appropriately to set up ad units and bidders. - -1. Set path for prebid.js in your example file: - #####Development `pbs.src = './build/dev/prebid.js';` ##### - ```javascript - (function() { - var d = document, pbs = d.createElement('script'), pro = d.location.protocol; - pbs.type = 'text/javascript'; - pbs.src = ((pro === 'https:') ? 'https' : 'http') + ':./build/dev/prebid.js'; - var target = document.getElementsByTagName('head')[0]; - target.insertBefore(pbs, target.firstChild); - })(); - ``` - #####Test/Deploy (default) `pbs.src = './build/dist/prebid.js';`##### - ```javascript - (function() { - var d = document, pbs = d.createElement('script'), pro = d.location.protocol; - pbs.type = 'text/javascript'; - pbs.src = ((pro === 'https:') ? 'https' : 'http') + './build/dist/prebid.js'; - var target = document.getElementsByTagName('head')[0]; - target.insertBefore(pbs, target.firstChild); - })(); - ``` -1. (optional optimization) Edit `./package.json` to set the adapters you want to build with: - - ```json - "adapters": [ - "adform", - "admedia", - "aol", - "appnexus", - "indexExchange", - "openx", - "pubmatic", - "pulsepoint", - "rubicon", - "rubiconLegacy", - "sovrn", - "springserve", - "yieldbot" - ] - ``` - -### Run ### - - $ gulp serve - -This runs code quality checks, generates prebid files and starts a webserver at -`http://localhost:9999` serving from project root. Navigate to your example implementation to test, -and if your prebid.js file is sourced from the `./build/dev` directory you will have sourcemaps -available in browser developer tools. - -Navigate to `http://localhost:9999/integrationExamples/gpt/pbjs_example_gpt.html` to run the -example file. - -Navigate to `http://localhost:9999/build/coverage/karma_html/report` to view a test coverage report. - -A watch is also in place that will run continuous tests in the terminal as you edit code and -tests. - -### Unit Test In the Browser ### - $ gulp test --watch - -This will run tests and keep the Karma test browser open. If your prebid.js file is sourced from -the build/dev directory you will also have sourcemaps available when viewing browser developer -tools. Access the Karma debug page at: -`http://localhost:9876/debug.html` -View console for test results and developer tools to set breakpoints in source code. - -Detailed code coverage reporting can be generated explicitly with `$ gulp test --coverage` and -results found in `./build/coverage`. - -### Supported Browsers ### -Prebid.js is supported on IE9+ and modern browsers. +[![Build Status](https://travis-ci.org/prebid/Prebid.js.svg?branch=master)](https://travis-ci.org/prebid/Prebid.js) +[![Percentage of issues still open](http://isitmaintained.com/badge/open/prebid/Prebid.js.svg)](http://isitmaintained.com/project/prebid/Prebid.js "Percentage of issues still open") +[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/prebid/Prebid.js.svg)](http://isitmaintained.com/project/prebid/Prebid.js "Average time to resolve an issue") +[![Code Climate](https://codeclimate.com/github/prebid/Prebid.js/badges/gpa.svg)](https://codeclimate.com/github/prebid/Prebid.js) + +Prebid.js +======== + +> Setup and manage header bidding advertising partners without writing code or confusing line items. Prebid.js is open source and free. + +Many SSPs, bidders, and publishers have all contributed to this project. [20+ Bidders](https://github.com/prebid/Prebid.js/tree/master/src/adapters) are supported by Prebid.js now. + +Check out the overview and documentation at http://prebid.org. + +No more week-long development. Header bidding is made easy by prebid.js :) + +**Table of Contents** + +- [Prebid.js](#) + - [Usage](#usage) + - [Download the latest released code](#download-the-latest-released-code) + - [Example code](#example-code) + - [API](#api) + - [Contribute](#contribute) + - [Add a Bidder Adapter](#add-a-bidder-adapter) + - [install](#install) + - [Build](#build) + - [Configure](#configure) + - [Run](#run) + - [Unit Test In the Browser](#unit-test-in-the-browser) + - [Supported Browsers](#supported-browsers) + + +Usage +---------- +Download the integration example [here](https://github.com/prebid/Prebid.js/blob/master/integrationExamples/gpt/pbjs_example_gpt.html). + +### Download the latest released code ### +[See the releases page here](https://github.com/prebid/Prebid.js/releases) and download a copy. + +### Example code ### + +**Include the prebid.js library** +Note that you need to host `prebid.js` locally or on a CDN and update the reference in the code snippet below for `cdn.host.com/prebid.min.js` +```javascript +(function () { + var d = document; + var pbs = d.createElement('script'); + pbs.type = 'text/javascript'; + //replace with your CDN hosted version. HTTPS is strongly recommended. + pbs.src = '//cdn.host.com/prebid.min.js'; + var target = d.getElementsByTagName('script')[0]; + target.parentNode.insertBefore(pbs, target); +})(); +``` + +**Setup ad units** +```javascript +pbjs.que.push(function(){ + var adUnits = [{ + code: '{id}', + sizes: [[300, 250], [300, 600]], + bids: [ + { + bidder: 'appnexus', + params: { + placementId: '{id}' + } + } + ] + }]; + //add the adUnits + pbjs.addAdUnits(adUnits); +}); +``` + +**Request Bids** +```javascript +pbjs.que.push(function(){ + pbjs.requestBids({ + bidsBackHandler: function(bidResponses) { + //do stuff when the bids are back + } + }) +}); +``` + +**See Console Debug Errors during testing** +By default console errors are suppressed. To enabled add `?pbjs_debug=true` to the end of the URL + for testing. + +API +---------- +Full Developer API reference: + +[Click here to access the API](http://prebid.org/dev-docs/publisher-api-reference.html) + +Contribute +---------- +**Note:** You need to have at least `node.js 4.x` or greater installed to be able to run the gulp build commands. + +### Add a Bidder Adapter ### +Follow the [guide outlined here](http://prebid.org/dev-docs/bidder-adaptor.html) to add an adapter. + +### Install ### + $ npm install + +If you experience errors, after a version update, try a fresh install: + + $ rm -rf ./node_modules && npm cache clean && npm install + +### Build ### + $ gulp build + +Runs code quality checks, generates prebid.js files and creates zip archive distributable: + + `./build/dev/prebid.js` Full source code for dev and debug + `./build/dev/prebid.js.map` Source map for dev and debug + `./build/dist/prebid.js` Minified production code + `./prebid.js_.zip` Distributable + +Code quality is defined by `./.jscs` and `./.jshint` files and errors are reported in the +terminal. The build will continue with quality errors, however. If you are contributing code +you can configure your editor with the provided .jscs and .jshint settings. + +### Configure ### +Edit example file `./integrationExamples/gpt/pbjs_example_gpt.html`: + +1. Change `{id}` values appropriately to set up ad units and bidders. + +1. Set path for prebid.js in your example file: + #####Development `pbs.src = './build/dev/prebid.js';` ##### + ```javascript + (function() { + var d = document, pbs = d.createElement('script'), pro = d.location.protocol; + pbs.type = 'text/javascript'; + pbs.src = ((pro === 'https:') ? 'https' : 'http') + ':./build/dev/prebid.js'; + var target = document.getElementsByTagName('head')[0]; + target.insertBefore(pbs, target.firstChild); + })(); + ``` + #####Test/Deploy (default) `pbs.src = './build/dist/prebid.js';`##### + ```javascript + (function() { + var d = document, pbs = d.createElement('script'), pro = d.location.protocol; + pbs.type = 'text/javascript'; + pbs.src = ((pro === 'https:') ? 'https' : 'http') + './build/dist/prebid.js'; + var target = document.getElementsByTagName('head')[0]; + target.insertBefore(pbs, target.firstChild); + })(); + ``` +1. (optional optimization) Edit `./package.json` to set the adapters you want to build with: + + ```json + "adapters": [ + "adform", + "admedia", + "aol", + "appnexus", + "indexExchange", + "openx", + "pubmatic", + "pulsepoint", + "rubicon", + "rubiconLegacy", + "sovrn", + "springserve", + "yieldbot" + ] + ``` + +### Run ### + + $ gulp serve + +This runs code quality checks, generates prebid files and starts a webserver at +`http://localhost:9999` serving from project root. Navigate to your example implementation to test, +and if your prebid.js file is sourced from the `./build/dev` directory you will have sourcemaps +available in browser developer tools. + +Navigate to `http://localhost:9999/integrationExamples/gpt/pbjs_example_gpt.html` to run the +example file. + +Navigate to `http://localhost:9999/build/coverage/karma_html/report` to view a test coverage report. + +A watch is also in place that will run continuous tests in the terminal as you edit code and +tests. + +### Unit Test In the Browser ### + $ gulp test --watch + +This will run tests and keep the Karma test browser open. If your prebid.js file is sourced from +the build/dev directory you will also have sourcemaps available when viewing browser developer +tools. Access the Karma debug page at: +`http://localhost:9876/debug.html` +View console for test results and developer tools to set breakpoints in source code. + +Detailed code coverage reporting can be generated explicitly with `$ gulp test --coverage` and +results found in `./build/coverage`. + +### Supported Browsers ### +Prebid.js is supported on IE9+ and modern browsers. diff --git a/package.json b/package.json index 3bdfcdd4933..7e29e1879fb 100644 --- a/package.json +++ b/package.json @@ -1,123 +1,123 @@ -{ - "name": "prebid.js", - "version": "0.9.2", - "description": "Header Bidding Management Library", - "main": "src/prebid.js", - "scripts": { - "test": "gulp test" - }, - "repository": { - "type": "git", - "url": "https://github.com/prebid/Prebid.js.git" - }, - "adapters": [ - "aardvark", - "adequant", - "adform", - "admedia", - "aol", - "appnexus", - "indexExchange", - "kruxlink", - "openx", - "pubmatic", - "pulsepoint", - "rubicon", - "sekindo", - "sonobi", - "sovrn", - "springserve", - "triplelift", - "yieldbot", - "nginad", - "brightcom", - "wideorbit", - { - "appnexus": { - "alias": "brealtime" - } - }, - { - "appnexus": { - "alias": "pagescience" - } - } - ], - "author": "the prebid.js contributors", - "license": "Apache-2.0", - "devDependencies": { - "babel-core": "^6.5.2", - "babel-loader": "^6.2.3", - "babel-plugin-transform-object-assign": "^6.8.0", - "babel-preset-es2015": "^6.5.0", - "block-loader": "^2.1.0", - "chai": "^3.3.0", - "chai-as-promised": "^5.1.0", - "clone": "^0.1.17", - "del": "^2.2.0", - "es5-shim": "^4.5.2", - "eslint": "^1.9.0", - "express": "^4.10.2", - "fuzzyset.js": "0.0.1", - "gulp": "^3.8.7", - "gulp-babel": "^6.1.2", - "gulp-clean": "^0.3.1", - "gulp-concat": "^2.6.0", - "gulp-connect": "^2.0.6", - "gulp-header": "^1.7.1", - "gulp-jscs": "^3.0.2", - "gulp-jsdoc-to-markdown": "^1.2.1", - "gulp-jshint": "^1.8.4", - "gulp-karma": "0.0.4", - "gulp-rename": "^1.2.0", - "gulp-replace": "^0.4.0", - "gulp-uglify": "^0.3.1", - "gulp-util": "^3.0.0", - "gulp-webdriver": "^1.0.1", - "gulp-zip": "^3.1.0", - "istanbul": "^0.3.2", - "istanbul-instrumenter-loader": "^0.1.2", - "jquery": "1.11.1", - "json-loader": "^0.5.1", - "karma": "^0.13.2", - "karma-babel-preprocessor": "^6.0.1", - "karma-browserstack-launcher": "^1.0.1", - "karma-chai": "^0.1.0", - "karma-chrome-launcher": "^0.2.0", - "karma-coverage": "^0.2.6", - "karma-es5-shim": "https://github.com/pokehanai/karma-es5-shim/archive/v2.1.0.tar.gz", - "karma-expect": "^1.1.0", - "karma-firefox-launcher": "^0.1.3", - "karma-html-reporter": "^0.2.7", - "karma-ie-launcher": "^0.1.5", - "karma-junit-reporter": "^0.3.8", - "karma-mocha": "^0.2.0", - "karma-nyan-reporter": "0.2.2", - "karma-opera-launcher": "^0.1.0", - "karma-phantomjs-launcher": "^0.2.1", - "karma-requirejs": "^0.2.2", - "karma-safari-launcher": "^0.1.1", - "karma-sauce-launcher": "^0.2.10", - "karma-script-launcher": "^0.1.0", - "karma-sinon": "^1.0.4", - "karma-webpack": "^1.5.1", - "localtunnel": "^1.3.0", - "lodash": "^2.4.1", - "mocha": "^1.21.4", - "open": "0.0.5", - "phantomjs": "^1.9.18", - "querystringify": "0.0.3", - "raw-loader": "^0.5.1", - "redis": "^0.12.1", - "requirejs": "^2.1.20", - "run-sequence": "^1.1.4", - "sinon": "^1.12.1", - "string-replace-webpack-plugin": "0.0.3", - "uglify-js": "^2.4.15", - "url-parse": "^1.0.5", - "webpack": "^1.12.3", - "webpack-stream": "^3.1.0", - "yargs": "^1.3.1" - }, - "dependencies": {} -} +{ + "name": "prebid.js", + "version": "0.10.0", + "description": "Header Bidding Management Library", + "main": "src/prebid.js", + "scripts": { + "test": "gulp test" + }, + "repository": { + "type": "git", + "url": "https://github.com/prebid/Prebid.js.git" + }, + "adapters": [ + "aardvark", + "adequant", + "adform", + "admedia", + "aol", + "appnexus", + "indexExchange", + "kruxlink", + "openx", + "pubmatic", + "pulsepoint", + "rubicon", + "sekindo", + "sonobi", + "sovrn", + "springserve", + "triplelift", + "yieldbot", + "nginad", + "brightcom", + "wideorbit", + { + "appnexus": { + "alias": "brealtime" + } + }, + { + "appnexus": { + "alias": "pagescience" + } + } + ], + "author": "the prebid.js contributors", + "license": "Apache-2.0", + "devDependencies": { + "babel-core": "^6.5.2", + "babel-loader": "^6.2.3", + "babel-plugin-transform-object-assign": "^6.8.0", + "babel-preset-es2015": "^6.5.0", + "block-loader": "^2.1.0", + "chai": "^3.3.0", + "chai-as-promised": "^5.1.0", + "clone": "^0.1.17", + "del": "^2.2.0", + "es5-shim": "^4.5.2", + "eslint": "^1.9.0", + "express": "^4.10.2", + "fuzzyset.js": "0.0.1", + "gulp": "^3.8.7", + "gulp-babel": "^6.1.2", + "gulp-clean": "^0.3.1", + "gulp-concat": "^2.6.0", + "gulp-connect": "^2.0.6", + "gulp-header": "^1.7.1", + "gulp-jscs": "^3.0.2", + "gulp-jsdoc-to-markdown": "^1.2.1", + "gulp-jshint": "^1.8.4", + "gulp-karma": "0.0.4", + "gulp-rename": "^1.2.0", + "gulp-replace": "^0.4.0", + "gulp-uglify": "^0.3.1", + "gulp-util": "^3.0.0", + "gulp-webdriver": "^1.0.1", + "gulp-zip": "^3.1.0", + "istanbul": "^0.3.2", + "istanbul-instrumenter-loader": "^0.1.2", + "jquery": "1.11.1", + "json-loader": "^0.5.1", + "karma": "^0.13.2", + "karma-babel-preprocessor": "^6.0.1", + "karma-browserstack-launcher": "^1.0.1", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^0.2.0", + "karma-coverage": "^0.2.6", + "karma-es5-shim": "https://github.com/pokehanai/karma-es5-shim/archive/v2.1.0.tar.gz", + "karma-expect": "^1.1.0", + "karma-firefox-launcher": "^0.1.3", + "karma-html-reporter": "^0.2.7", + "karma-ie-launcher": "^0.1.5", + "karma-junit-reporter": "^0.3.8", + "karma-mocha": "^0.2.0", + "karma-nyan-reporter": "0.2.2", + "karma-opera-launcher": "^0.1.0", + "karma-phantomjs-launcher": "^0.2.1", + "karma-requirejs": "^0.2.2", + "karma-safari-launcher": "^0.1.1", + "karma-sauce-launcher": "^0.2.10", + "karma-script-launcher": "^0.1.0", + "karma-sinon": "^1.0.4", + "karma-webpack": "^1.5.1", + "localtunnel": "^1.3.0", + "lodash": "^2.4.1", + "mocha": "^1.21.4", + "open": "0.0.5", + "phantomjs": "^1.9.18", + "querystringify": "0.0.3", + "raw-loader": "^0.5.1", + "redis": "^0.12.1", + "requirejs": "^2.1.20", + "run-sequence": "^1.1.4", + "sinon": "^1.12.1", + "string-replace-webpack-plugin": "0.0.3", + "uglify-js": "^2.4.15", + "url-parse": "^1.0.5", + "webpack": "^1.12.3", + "webpack-stream": "^3.1.0", + "yargs": "^1.3.1" + }, + "dependencies": {} +} diff --git a/src/adapters/adform.js b/src/adapters/adform.js index 479dfd03824..bfaf1de20c1 100644 --- a/src/adapters/adform.js +++ b/src/adapters/adform.js @@ -5,170 +5,170 @@ var bidfactory = require('../bidfactory.js'); function AdformAdapter() { - return { - callBids: _callBids - }; - - function _callBids(params) { - var callbackName = '_adf_' + utils.getUniqueIdentifierStr(); - var bid; - var noDomain = true; - var bids = params.bids; - var request = []; - var adxDomain; - - var singleRequest = {}, - singleParam, - singleParams = [ - 'url' - ]; - - for (var i = 0, l = bids.length; i < l; i++) { - bid = bids[i]; - adxDomain = bid.params.adxDomain || bid.adxDomain; - - if (adxDomain && noDomain) { - noDomain = false; - request.unshift('//' + adxDomain + '/adx/?rp=4'); - } - - request.push(formRequestUrl(bid.params)); - - for (var j = 0, k = singleParams.length; j < k; j++) { - singleParam = singleParams[ j ]; - if ( bid.params[ singleParam ] ) { - singleRequest[ singleParam ] = bid.params[ singleParam ]; - } - } - } - - if (noDomain) { - request.unshift('//adx.adform.net/adx/?rp=4'); - } - - for (var j = 0, k = singleParams.length; j < k; j++) { - singleParam = singleParams[ j ]; - if ( singleRequest[ singleParam ] ) { - request.push( singleParam + '=' + encodeURIComponent( singleRequest[ singleParam ] ) ); - } + return { + callBids: _callBids + }; + + function _callBids(params) { + var callbackName = '_adf_' + utils.getUniqueIdentifierStr(); + var bid; + var noDomain = true; + var bids = params.bids; + var request = []; + var adxDomain; + var i, j, k, l; + + var singleRequest = {}, + singleParam, + singleParams = [ + 'url' + ]; + + for (i = 0, l = bids.length; i < l; i++) { + bid = bids[i]; + adxDomain = bid.params.adxDomain || bid.adxDomain; + + if (adxDomain && noDomain) { + noDomain = false; + request.unshift('//' + adxDomain + '/adx/?rp=4'); + } + + request.push(formRequestUrl(bid.params)); + + for (j = 0, k = singleParams.length; j < k; j++) { + singleParam = singleParams[ j ]; + if ( bid.params[ singleParam ] ) { + singleRequest[ singleParam ] = bid.params[ singleParam ]; } - - pbjs[callbackName] = handleCallback(bids); - request.push('callback=pbjs.' + callbackName); - - adloader.loadScript(request.join('&')); + } } - function formRequestUrl(reqData) { - var key; - var url = []; - - var validProps = [ - 'mid', 'inv', 'pdom', 'mname', 'mkw', 'mkv', 'cat', 'bcat', 'bcatrt', 'adv', 'advt', 'cntr', 'cntrt', 'maxp', - 'minp', 'sminp', 'w', 'h', 'pb', 'pos', 'cturl', 'iturl', 'cttype', 'hidedomain', 'cdims', 'test' - ]; - - for (var i = 0, l = validProps.length; i < l; i++) { - key = validProps[i]; - if (reqData.hasOwnProperty(key)) - url.push(key, '=', reqData[key], '&'); - } - - return encode64(url.join('')); + if (noDomain) { + request.unshift('//adx.adform.net/adx/?rp=4'); } - function handleCallback(bids) { - return function handleResponse(adItems) { - var bidObject, bidder = 'adform', adItem, bid; - for(var i = 0, l = adItems.length; i < l; i++){ - adItem = adItems[i]; - bid = bids[i]; - if (adItem && adItem.response === 'banner' && - verifySize(adItem, bid.sizes)) { - - bidObject = bidfactory.createBid(1); - bidObject.bidderCode = bidder; - bidObject.cpm = adItem.win_bid; - bidObject.cur = adItem.win_cur; - bidObject.ad = adItem.banner; - bidObject.width = adItem.width; - bidObject.height = adItem.height; - bidmanager.addBidResponse(bid.placementCode, bidObject); - } else { - bidObject = bidfactory.createBid(2); - bidObject.bidderCode = bidder; - bidmanager.addBidResponse(bid.placementCode, bidObject); - } - } - }; - - function verifySize(adItem, validSizes) { - for (var j = 0, k = validSizes.length; j < k; j++) { - if (adItem.width === validSizes[j][0] && - adItem.height === validSizes[j][1]) { - return true; - } - } - return false; - } + for (i = 0, l = singleParams.length; i < l; i++) { + singleParam = singleParams[ i ]; + if ( singleRequest[ singleParam ] ) { + request.push( singleParam + '=' + encodeURIComponent( singleRequest[ singleParam ] ) ); + } } - function encode64(input) { - var out = []; - var chr1, chr2, chr3, enc1, enc2, enc3, enc4; - var i = 0; - var _keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_='; - - input = utf8_encode(input); - - while (i < input.length) { - - chr1 = input.charCodeAt(i++); - chr2 = input.charCodeAt(i++); - chr3 = input.charCodeAt(i++); - - enc1 = chr1 >> 2; - enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; - - if (isNaN(chr2)) { - enc3 = enc4 = 64; - } else if (isNaN(chr3)) { - enc4 = 64; - } - out.push(_keyStr.charAt(enc1), _keyStr.charAt(enc2)); - if (enc3 !== 64) - out.push(_keyStr.charAt(enc3)); - if (enc4 !== 64) - out.push(_keyStr.charAt(enc4)); - } + pbjs[callbackName] = handleCallback(bids); + request.push('callback=pbjs.' + callbackName); - return out.join(''); - } + adloader.loadScript(request.join('&')); + } - function utf8_encode(string) { - string = string.replace(/\r\n/g, '\n'); - var utftext = ''; + function formRequestUrl(reqData) { + var key; + var url = []; - for (var n = 0; n < string.length; n++) { + var validProps = [ + 'mid', 'inv', 'pdom', 'mname', 'mkw', 'mkv', 'cat', 'bcat', 'bcatrt', 'adv', 'advt', 'cntr', 'cntrt', 'maxp', + 'minp', 'sminp', 'w', 'h', 'pb', 'pos', 'cturl', 'iturl', 'cttype', 'hidedomain', 'cdims', 'test' + ]; - var c = string.charCodeAt(n); + for (var i = 0, l = validProps.length; i < l; i++) { + key = validProps[i]; + if (reqData.hasOwnProperty(key)) { + url.push(key, '=', reqData[key], '&'); + } + } - if (c < 128) { - utftext += String.fromCharCode(c); - } else if ((c > 127) && (c < 2048)) { - utftext += String.fromCharCode((c >> 6) | 192); - utftext += String.fromCharCode((c & 63) | 128); - } else { - utftext += String.fromCharCode((c >> 12) | 224); - utftext += String.fromCharCode(((c >> 6) & 63) | 128); - utftext += String.fromCharCode((c & 63) | 128); - } + return encode64(url.join('')); + } + + function handleCallback(bids) { + return function handleResponse(adItems) { + var bidObject, bidder = 'adform', adItem, bid; + for(var i = 0, l = adItems.length; i < l; i++){ + adItem = adItems[i]; + bid = bids[i]; + if (adItem && adItem.response === 'banner' && + verifySize(adItem, bid.sizes)) { + + bidObject = bidfactory.createBid(1); + bidObject.bidderCode = bidder; + bidObject.cpm = adItem.win_bid; + bidObject.cur = adItem.win_cur; + bidObject.ad = adItem.banner; + bidObject.width = adItem.width; + bidObject.height = adItem.height; + bidmanager.addBidResponse(bid.placementCode, bidObject); + } else { + bidObject = bidfactory.createBid(2); + bidObject.bidderCode = bidder; + bidmanager.addBidResponse(bid.placementCode, bidObject); } + } + }; - return utftext; + function verifySize(adItem, validSizes) { + for (var j = 0, k = validSizes.length; j < k; j++) { + if (adItem.width === validSizes[j][0] && + adItem.height === validSizes[j][1]) { + return true; + } + } + return false; + } + } + + function encode64(input) { + var out = []; + var chr1, chr2, chr3, enc1, enc2, enc3, enc4; + var i = 0; + var _keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_='; + + input = utf8_encode(input); + + while (i < input.length) { + chr1 = input.charCodeAt(i++); + chr2 = input.charCodeAt(i++); + chr3 = input.charCodeAt(i++); + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + out.push(_keyStr.charAt(enc1), _keyStr.charAt(enc2)); + if (enc3 !== 64) + out.push(_keyStr.charAt(enc3)); + if (enc4 !== 64) { + out.push(_keyStr.charAt(enc4)); + } } + return out.join(''); + } + + function utf8_encode(string) { + string = string.replace(/\r\n/g, '\n'); + var utftext = ''; + + for (var n = 0; n < string.length; n++) { + var c = string.charCodeAt(n); + + if (c < 128) { + utftext += String.fromCharCode(c); + } else if ((c > 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + } + + return utftext; + } } diff --git a/src/adapters/indexExchange.js b/src/adapters/indexExchange.js index 82dc4e1502e..39c417e8197 100644 --- a/src/adapters/indexExchange.js +++ b/src/adapters/indexExchange.js @@ -1,488 +1,492 @@ -//Factory for creating the bidderAdaptor -// jshint ignore:start -var utils = require('../utils.js'); -var bidfactory = require('../bidfactory.js'); -var bidmanager = require('../bidmanager.js'); -var adloader = require('../adloader.js'); - -var ADAPTER_NAME = 'INDEXEXCHANGE'; -var ADAPTER_CODE = 'indexExchange'; - -var cygnus_index_parse_res = function () { -}; - -window.cygnus_index_args = {}; - -var cygnus_index_adunits = [[728, 90], [120, 600], [300, 250], [160, 600], [336, 280], [234, 60], [300, 600], [300, 50], [320, 50], [970, 250], [300, 1050], [970, 90], [180, 150]]; // jshint ignore:line - -var cygnus_index_start = function () { - window.index_slots = []; - - window.cygnus_index_args.parseFn = cygnus_index_parse_res; - var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; - var meta = { - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"': '\\"', - '\\': '\\\\' - }; - - function escapeCharacter(character) { - var escaped = meta[character]; - if (typeof escaped === 'string') { - return escaped; - } else { - return '\\u' + ('0000' + character.charCodeAt(0).toString(16)).slice(-4); - } - } - - function quote(string) { - escapable.lastIndex = 0; - if (escapable.test(string)) { - return string.replace(escapable, escapeCharacter); - } else { - return string; - } - } - - function OpenRTBRequest(siteID, parseFn, timeoutDelay) { - this.initialized = false; - if (typeof siteID !== 'number' || siteID % 1 !== 0 || siteID < 0) { - throw 'Invalid Site ID'; - } - - if (typeof timeoutDelay === 'number' && timeoutDelay % 1 === 0 && timeoutDelay >= 0) { - this.timeoutDelay = timeoutDelay; - } - - this.siteID = siteID; - this.impressions = []; - this._parseFnName = undefined; - if (top === self) { - this.sitePage = location.href; - this.topframe = 1; - } else { - this.sitePage = document.referrer; - this.topframe = 0; - } - - if (typeof parseFn !== 'undefined') { - if (typeof parseFn === 'function') { - this._parseFnName = 'cygnus_index_args.parseFn'; - } else { - throw 'Invalid jsonp target function'; - } - } - - if (typeof _IndexRequestData.requestCounter === 'undefined') { - _IndexRequestData.requestCounter = Math.floor(Math.random() * 256); - } else { - _IndexRequestData.requestCounter = (_IndexRequestData.requestCounter + 1) % 256; - } - - this.requestID = String((new Date().getTime() % 2592000) * 256 + _IndexRequestData.requestCounter + 256); - this.initialized = true; - } - - OpenRTBRequest.prototype.serialize = function () { - var json = '{"id":' + this.requestID + ',"site":{"page":"' + quote(this.sitePage) + '"'; - if (typeof document.referrer === 'string') { - json += ',"ref":"' + quote(document.referrer) + '"'; - } - - json += '},"imp":['; - for (var i = 0; i < this.impressions.length; i++) { - var impObj = this.impressions[i]; - var ext = []; - json += '{"id":"' + impObj.id + '", "banner":{"w":' + impObj.w + ',"h":' + impObj.h + ',"topframe":' + String(this.topframe) + '}'; - if (typeof impObj.bidfloor === 'number') { - json += ',"bidfloor":' + impObj.bidfloor; - if (typeof impObj.bidfloorcur === 'string') { - json += ',"bidfloorcur":"' + quote(impObj.bidfloorcur) + '"'; - } - } - - if (typeof impObj.slotID === 'string' && (!impObj.slotID.match(/^\s*$/))) { - ext.push('"sid":"' + quote(impObj.slotID) + '"'); - } - - if (typeof impObj.siteID === 'number') { - ext.push('"siteID":' + impObj.siteID); - } - - if (ext.length > 0) { - json += ',"ext": {' + ext.join() + '}'; - } - - if (i + 1 === this.impressions.length) { - json += '}'; - } else { - json += '},'; - } - } - - json += ']}'; - return json; - }; - - OpenRTBRequest.prototype.setPageOverride = function (sitePageOverride) { - if (typeof sitePageOverride === 'string' && (!sitePageOverride.match(/^\s*$/))) { - this.sitePage = sitePageOverride; - return true; - } else { - return false; - } - }; - - OpenRTBRequest.prototype.addImpression = function (width, height, bidFloor, bidFloorCurrency, slotID, siteID) { - var impObj = { - id: String(this.impressions.length + 1) - }; - if (typeof width !== 'number' || width <= 1) { - return null; - } - - if (typeof height !== 'number' || height <= 1) { - return null; - } - - if ((typeof slotID === 'string' || typeof slotID === 'number') && String(slotID).length <= 50) { - impObj.slotID = String(slotID); - } - - impObj.w = width; - impObj.h = height; - if (bidFloor !== undefined && typeof bidFloor !== 'number') { - return null; - } - - if (typeof bidFloor === 'number') { - if (bidFloor < 0) { - return null; - } - - impObj.bidfloor = bidFloor; - if (bidFloorCurrency !== undefined && typeof bidFloorCurrency !== 'string') { - return null; - } - - impObj.bidfloorcur = bidFloorCurrency; - } - - if (typeof siteID !== 'undefined') { - if (typeof siteID === 'number' && siteID % 1 === 0 && siteID >= 0) { - impObj.siteID = siteID; - } else { - return null; - } - } - - this.impressions.push(impObj); - return impObj.id; - }; - - OpenRTBRequest.prototype.buildRequest = function () { - if (this.impressions.length === 0 || this.initialized !== true) { - return; - } - - var jsonURI = encodeURIComponent(this.serialize()); - var scriptSrc = window.location.protocol === 'https:' ? 'https://as-sec.casalemedia.com' : 'http://as.casalemedia.com'; - scriptSrc += '/headertag?v=9&x3=1&fn=cygnus_index_parse_res&s=' + this.siteID + '&r=' + jsonURI; - if (typeof this.timeoutDelay === 'number' && this.timeoutDelay % 1 === 0 && this.timeoutDelay >= 0) { - scriptSrc += '&t=' + this.timeoutDelay; - } - - return scriptSrc; - }; - - try { - if (typeof cygnus_index_args === 'undefined' || typeof cygnus_index_args.siteID === 'undefined' || typeof cygnus_index_args.slots === 'undefined') { - return; - } - - if (typeof window._IndexRequestData === 'undefined') { - window._IndexRequestData = {}; - window._IndexRequestData.impIDToSlotID = {}; - window._IndexRequestData.reqOptions = {}; - } - - var req = new OpenRTBRequest(cygnus_index_args.siteID, cygnus_index_args.parseFn, cygnus_index_args.timeout); - if (cygnus_index_args.url && typeof cygnus_index_args.url === 'string') { - req.setPageOverride(cygnus_index_args.url); - } - - _IndexRequestData.impIDToSlotID[req.requestID] = {}; - _IndexRequestData.reqOptions[req.requestID] = {}; - var slotDef, impID; - - for (var i = 0; i < cygnus_index_args.slots.length; i++) { - slotDef = cygnus_index_args.slots[i]; - - impID = req.addImpression(slotDef.width, slotDef.height, slotDef.bidfloor, slotDef.bidfloorcur, slotDef.id, slotDef.siteID); - if (impID) { - _IndexRequestData.impIDToSlotID[req.requestID][impID] = String(slotDef.id); - } - } - - if (typeof cygnus_index_args.targetMode === 'number') { - _IndexRequestData.reqOptions[req.requestID].targetMode = cygnus_index_args.targetMode; - } - - if (typeof cygnus_index_args.callback === 'function') { - _IndexRequestData.reqOptions[req.requestID].callback = cygnus_index_args.callback; - } - - return req.buildRequest(); - } catch (e) { - utils.logError('Error calling index adapter', ADAPTER_NAME, e); - } -}; - -var IndexExchangeAdapter = function IndexExchangeAdapter() { - var slotIdMap = {}; - var requiredParams = [ - /* 0 */ - 'id', - /* 1 */ - 'siteID' - ]; - var firstAdUnitCode = ''; - - function _callBids(request) { - var bidArr = request.bids; - - if (!utils.hasValidBidRequest(bidArr[0].params, requiredParams, ADAPTER_NAME)) { - return; - } - - //Our standard is to always bid for all known slots. - cygnus_index_args.slots = []; - - var expectedBids = 0; - - //Grab the slot level data for cygnus_index_args - for (var i = 0; i < bidArr.length; i++) { - var bid = bidArr[i]; - var sizeID = 0; - - expectedBids++; - - // Expecting nested arrays for sizes - if (!(bid.sizes[0] instanceof Array)) { - bid.sizes = [bid.sizes]; - } - - // Create index slots for all bids and sizes - for (var j = 0; j < bid.sizes.length; j++) { - var validSize = false; - for (var k = 0; k < cygnus_index_adunits.length; k++) { - if (bid.sizes[j][0] === cygnus_index_adunits[k][0] && - bid.sizes[j][1] === cygnus_index_adunits[k][1]) { - validSize = true; - break; - } - } - - if (!validSize) { - continue; - } - - if (bid.params.timeout && typeof cygnus_index_args.timeout === 'undefined') { - cygnus_index_args.timeout = bid.params.timeout; - } - - - var siteID = Number(bid.params.siteID); - if (!siteID) { - continue; - } - if (siteID && typeof cygnus_index_args.siteID === 'undefined') { - cygnus_index_args.siteID = siteID; - } - - if (utils.hasValidBidRequest(bid.params, requiredParams, ADAPTER_NAME)) { - firstAdUnitCode = bid.placementCode; - var slotID = bid.params[requiredParams[0]]; - slotIdMap[slotID] = bid; - - sizeID++; - var size = { - width: bid.sizes[j][0], - height: bid.sizes[j][1] - }; - - var slotName = slotID + '_' + sizeID; - - //Doesn't need the if(primary_request) conditional since we are using the mergeSlotInto function which is safe - cygnus_index_args.slots = mergeSlotInto({ - id: slotName, - width: size.width, - height: size.height, - siteID: siteID || cygnus_index_args.siteID - }, cygnus_index_args.slots); - - if (bid.params.tier2SiteID) { - var tier2SiteID = Number(bid.params.tier2SiteID); - if (typeof tier2SiteID !== 'undefined' && !tier2SiteID) { - continue; - } - - cygnus_index_args.slots = mergeSlotInto({ - id: 'T1_' + slotName, - width: size.width, - height: size.height, - siteID: tier2SiteID - }, cygnus_index_args.slots); - } - - if (bid.params.tier3SiteID) { - var tier3SiteID = Number(bid.params.tier3SiteID); - if (typeof tier3SiteID !== 'undefined' && !tier3SiteID) { - continue; - } - - cygnus_index_args.slots = mergeSlotInto({ - id: 'T2_' + slotName, - width: size.width, - height: size.height, - siteID: tier3SiteID - }, cygnus_index_args.slots); - } - } - } - } - - if (cygnus_index_args.slots.length > 20) { - utils.logError('Too many unique sizes on slots, will use the first 20.', ADAPTER_NAME); - } - - //bidmanager.setExpectedBidsCount(ADAPTER_CODE, expectedBids); - adloader.loadScript(cygnus_index_start()); - - var responded = false; - - // Handle response - window.cygnus_index_ready_state = function () { - if (responded) { - return; - } - responded = true; - - try { - var indexObj = _IndexRequestData.targetIDToBid; - var lookupObj = cygnus_index_args; - - // Grab all the bids for each slot - for (var adSlotId in slotIdMap) { - var bidObj = slotIdMap[adSlotId]; - var adUnitCode = bidObj.placementCode; - - var bids = []; - - // Grab the bid for current slot - for (var cpmAndSlotId in indexObj) { - var match = /(T\d_)?(.+)_(\d+)_(\d+)/.exec(cpmAndSlotId); - var tier = match[1] || ''; - var slotID = match[2]; - var sizeID = match[3]; - var currentCPM = match[4]; - - var slotName = slotID + '_' + sizeID; - var slotObj = getSlotObj(cygnus_index_args, tier + slotName); - - // Bid is for the current slot - if (slotID === adSlotId) { - var bid = bidfactory.createBid(1); - bid.cpm = currentCPM / 100; - bid.ad = indexObj[cpmAndSlotId][0]; - bid.ad_id = adSlotId; - bid.bidderCode = ADAPTER_CODE; - bid.width = slotObj.width; - bid.height = slotObj.height; - bid.siteID = slotObj.siteID; - - bids.push(bid); - } - } - - var currentBid = undefined; - - //Pick the highest bidding price for this slot - if (bids.length > 0) { - // Choose the highest bid - for (var i = 0; i < bids.length; i++) { - var bid = bids[i]; - if (typeof currentBid === 'undefined') { - currentBid = bid; - continue; - } - - if (bid.cpm > currentBid.cpm) { - currentBid = bid; - } - } - - // No bids for expected bid, pass bid - } else { - var bid = bidfactory.createBid(2); - bid.bidderCode = ADAPTER_CODE; - currentBid = bid; - } - - bidmanager.addBidResponse(adUnitCode, currentBid); - } - } catch (e) { - utils.logError('Error calling index adapter', ADAPTER_NAME, e); - logErrorBidResponse(); - } - - //slotIdMap is used to determine which slots will be bid on in a given request. - //Therefore it needs to be blanked after the request is handled, else we will submit 'bids' for the wrong ads. - slotIdMap={}; - }; - } - - /* - Function in order to add a slot into the list if it hasn't been created yet, else it returns the same list. - */ - function mergeSlotInto(slot,slotList){ - for(var i = 0; i < slotList.length; i++){ - if(slot.id === slotList[i].id){ - return slotList; - } - } - slotList.push(slot); - return slotList; - } - - function getSlotObj(obj, id) { - var arr = obj.slots; - var returnObj = {}; - utils._each(arr, function (value) { - if (value.id === id) { - returnObj = value; - } - }); - - return returnObj; - } - - function logErrorBidResponse() { - //no bid response - var bid = bidfactory.createBid(2); - bid.bidderCode = ADAPTER_CODE; - - //log error to first add unit - bidmanager.addBidResponse(firstAdUnitCode, bid); - } - - return { - callBids: _callBids - }; -}; - -module.exports = IndexExchangeAdapter; +//Factory for creating the bidderAdaptor +// jshint ignore:start +var utils = require('../utils.js'); +var bidfactory = require('../bidfactory.js'); +var bidmanager = require('../bidmanager.js'); +var adloader = require('../adloader.js'); + +var ADAPTER_NAME = 'INDEXEXCHANGE'; +var ADAPTER_CODE = 'indexExchange'; + +var cygnus_index_parse_res = function () { +}; + +window.cygnus_index_args = {}; + +var cygnus_index_adunits = [[728, 90], [120, 600], [300, 250], [160, 600], [336, 280], [234, 60], [300, 600], [300, 50], [320, 50], [970, 250], [300, 1050], [970, 90], [180, 150]]; // jshint ignore:line + +var cygnus_index_start = function () { + window.index_slots = []; + + window.cygnus_index_args.parseFn = cygnus_index_parse_res; + var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + var meta = { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }; + + function escapeCharacter(character) { + var escaped = meta[character]; + if (typeof escaped === 'string') { + return escaped; + } else { + return '\\u' + ('0000' + character.charCodeAt(0).toString(16)).slice(-4); + } + } + + function quote(string) { + escapable.lastIndex = 0; + if (escapable.test(string)) { + return string.replace(escapable, escapeCharacter); + } else { + return string; + } + } + + function OpenRTBRequest(siteID, parseFn, timeoutDelay) { + this.initialized = false; + if (typeof siteID !== 'number' || siteID % 1 !== 0 || siteID < 0) { + throw 'Invalid Site ID'; + } + + if (typeof timeoutDelay === 'number' && timeoutDelay % 1 === 0 && timeoutDelay >= 0) { + this.timeoutDelay = timeoutDelay; + } + + this.siteID = siteID; + this.impressions = []; + this._parseFnName = undefined; + if (top === self) { + this.sitePage = location.href; + this.topframe = 1; + } else { + this.sitePage = document.referrer; + this.topframe = 0; + } + + if (typeof parseFn !== 'undefined') { + if (typeof parseFn === 'function') { + this._parseFnName = 'cygnus_index_args.parseFn'; + } else { + throw 'Invalid jsonp target function'; + } + } + + if (typeof _IndexRequestData.requestCounter === 'undefined') { + _IndexRequestData.requestCounter = Math.floor(Math.random() * 256); + } else { + _IndexRequestData.requestCounter = (_IndexRequestData.requestCounter + 1) % 256; + } + + this.requestID = String((new Date().getTime() % 2592000) * 256 + _IndexRequestData.requestCounter + 256); + this.initialized = true; + } + + OpenRTBRequest.prototype.serialize = function () { + var json = '{"id":' + this.requestID + ',"site":{"page":"' + quote(this.sitePage) + '"'; + if (typeof document.referrer === 'string') { + json += ',"ref":"' + quote(document.referrer) + '"'; + } + + json += '},"imp":['; + for (var i = 0; i < this.impressions.length; i++) { + var impObj = this.impressions[i]; + var ext = []; + json += '{"id":"' + impObj.id + '", "banner":{"w":' + impObj.w + ',"h":' + impObj.h + ',"topframe":' + String(this.topframe) + '}'; + if (typeof impObj.bidfloor === 'number') { + json += ',"bidfloor":' + impObj.bidfloor; + if (typeof impObj.bidfloorcur === 'string') { + json += ',"bidfloorcur":"' + quote(impObj.bidfloorcur) + '"'; + } + } + + if (typeof impObj.slotID === 'string' && (!impObj.slotID.match(/^\s*$/))) { + ext.push('"sid":"' + quote(impObj.slotID) + '"'); + } + + if (typeof impObj.siteID === 'number') { + ext.push('"siteID":' + impObj.siteID); + } + + if (ext.length > 0) { + json += ',"ext": {' + ext.join() + '}'; + } + + if (i + 1 === this.impressions.length) { + json += '}'; + } else { + json += '},'; + } + } + + json += ']}'; + return json; + }; + + OpenRTBRequest.prototype.setPageOverride = function (sitePageOverride) { + if (typeof sitePageOverride === 'string' && (!sitePageOverride.match(/^\s*$/))) { + this.sitePage = sitePageOverride; + return true; + } else { + return false; + } + }; + + OpenRTBRequest.prototype.addImpression = function (width, height, bidFloor, bidFloorCurrency, slotID, siteID) { + var impObj = { + id: String(this.impressions.length + 1) + }; + if (typeof width !== 'number' || width <= 1) { + return null; + } + + if (typeof height !== 'number' || height <= 1) { + return null; + } + + if ((typeof slotID === 'string' || typeof slotID === 'number') && String(slotID).length <= 50) { + impObj.slotID = String(slotID); + } + + impObj.w = width; + impObj.h = height; + if (bidFloor !== undefined && typeof bidFloor !== 'number') { + return null; + } + + if (typeof bidFloor === 'number') { + if (bidFloor < 0) { + return null; + } + + impObj.bidfloor = bidFloor; + if (bidFloorCurrency !== undefined && typeof bidFloorCurrency !== 'string') { + return null; + } + + impObj.bidfloorcur = bidFloorCurrency; + } + + if (typeof siteID !== 'undefined') { + if (typeof siteID === 'number' && siteID % 1 === 0 && siteID >= 0) { + impObj.siteID = siteID; + } else { + return null; + } + } + + this.impressions.push(impObj); + return impObj.id; + }; + + OpenRTBRequest.prototype.buildRequest = function () { + if (this.impressions.length === 0 || this.initialized !== true) { + return; + } + + var jsonURI = encodeURIComponent(this.serialize()); + var scriptSrc = window.location.protocol === 'https:' ? 'https://as-sec.casalemedia.com' : 'http://as.casalemedia.com'; + scriptSrc += '/headertag?v=9&x3=1&fn=cygnus_index_parse_res&s=' + this.siteID + '&r=' + jsonURI; + if (typeof this.timeoutDelay === 'number' && this.timeoutDelay % 1 === 0 && this.timeoutDelay >= 0) { + scriptSrc += '&t=' + this.timeoutDelay; + } + + return scriptSrc; + }; + + try { + if (typeof cygnus_index_args === 'undefined' || typeof cygnus_index_args.siteID === 'undefined' || typeof cygnus_index_args.slots === 'undefined') { + return; + } + + if (typeof window._IndexRequestData === 'undefined') { + window._IndexRequestData = {}; + window._IndexRequestData.impIDToSlotID = {}; + window._IndexRequestData.reqOptions = {}; + } + + var req = new OpenRTBRequest(cygnus_index_args.siteID, cygnus_index_args.parseFn, cygnus_index_args.timeout); + if (cygnus_index_args.url && typeof cygnus_index_args.url === 'string') { + req.setPageOverride(cygnus_index_args.url); + } + + _IndexRequestData.impIDToSlotID[req.requestID] = {}; + _IndexRequestData.reqOptions[req.requestID] = {}; + var slotDef, impID; + + for (var i = 0; i < cygnus_index_args.slots.length; i++) { + slotDef = cygnus_index_args.slots[i]; + + impID = req.addImpression(slotDef.width, slotDef.height, slotDef.bidfloor, slotDef.bidfloorcur, slotDef.id, slotDef.siteID); + if (impID) { + _IndexRequestData.impIDToSlotID[req.requestID][impID] = String(slotDef.id); + } + } + + if (typeof cygnus_index_args.targetMode === 'number') { + _IndexRequestData.reqOptions[req.requestID].targetMode = cygnus_index_args.targetMode; + } + + if (typeof cygnus_index_args.callback === 'function') { + _IndexRequestData.reqOptions[req.requestID].callback = cygnus_index_args.callback; + } + + return req.buildRequest(); + } catch (e) { + utils.logError('Error calling index adapter', ADAPTER_NAME, e); + } +}; + +var IndexExchangeAdapter = function IndexExchangeAdapter() { + var slotIdMap = {}; + var requiredParams = [ + /* 0 */ + 'id', + /* 1 */ + 'siteID' + ]; + var firstAdUnitCode = ''; + + function _callBids(request) { + var bidArr = request.bids; + + if (!utils.hasValidBidRequest(bidArr[0].params, requiredParams, ADAPTER_NAME)) { + return; + } + + //Our standard is to always bid for all known slots. + cygnus_index_args.slots = []; + + var expectedBids = 0; + + //Grab the slot level data for cygnus_index_args + for (var i = 0; i < bidArr.length; i++) { + var bid = bidArr[i]; + var sizeID = 0; + + expectedBids++; + + // Expecting nested arrays for sizes + if (!(bid.sizes[0] instanceof Array)) { + bid.sizes = [bid.sizes]; + } + + // Create index slots for all bids and sizes + for (var j = 0; j < bid.sizes.length; j++) { + var validSize = false; + for (var k = 0; k < cygnus_index_adunits.length; k++) { + if (bid.sizes[j][0] === cygnus_index_adunits[k][0] && + bid.sizes[j][1] === cygnus_index_adunits[k][1]) { + validSize = true; + break; + } + } + + if (!validSize) { + continue; + } + + if (bid.params.timeout && typeof cygnus_index_args.timeout === 'undefined') { + cygnus_index_args.timeout = bid.params.timeout; + } + + + var siteID = Number(bid.params.siteID); + if (!siteID) { + continue; + } + if (siteID && typeof cygnus_index_args.siteID === 'undefined') { + cygnus_index_args.siteID = siteID; + } + + if (utils.hasValidBidRequest(bid.params, requiredParams, ADAPTER_NAME)) { + firstAdUnitCode = bid.placementCode; + var slotID = bid.params[requiredParams[0]]; + slotIdMap[slotID] = bid; + + sizeID++; + var size = { + width: bid.sizes[j][0], + height: bid.sizes[j][1] + }; + + var slotName = slotID + '_' + sizeID; + + //Doesn't need the if(primary_request) conditional since we are using the mergeSlotInto function which is safe + cygnus_index_args.slots = mergeSlotInto({ + id: slotName, + width: size.width, + height: size.height, + siteID: siteID || cygnus_index_args.siteID + }, cygnus_index_args.slots); + + if (bid.params.tier2SiteID) { + var tier2SiteID = Number(bid.params.tier2SiteID); + if (typeof tier2SiteID !== 'undefined' && !tier2SiteID) { + continue; + } + + cygnus_index_args.slots = mergeSlotInto({ + id: 'T1_' + slotName, + width: size.width, + height: size.height, + siteID: tier2SiteID + }, cygnus_index_args.slots); + } + + if (bid.params.tier3SiteID) { + var tier3SiteID = Number(bid.params.tier3SiteID); + if (typeof tier3SiteID !== 'undefined' && !tier3SiteID) { + continue; + } + + cygnus_index_args.slots = mergeSlotInto({ + id: 'T2_' + slotName, + width: size.width, + height: size.height, + siteID: tier3SiteID + }, cygnus_index_args.slots); + } + } + } + } + + if (cygnus_index_args.slots.length > 20) { + utils.logError('Too many unique sizes on slots, will use the first 20.', ADAPTER_NAME); + } + + //bidmanager.setExpectedBidsCount(ADAPTER_CODE, expectedBids); + adloader.loadScript(cygnus_index_start()); + + var responded = false; + + // Handle response + window.cygnus_index_ready_state = function () { + if (responded) { + return; + } + responded = true; + + try { + var indexObj = _IndexRequestData.targetIDToBid; + var lookupObj = cygnus_index_args; + + // Grab all the bids for each slot + for (var adSlotId in slotIdMap) { + var bidObj = slotIdMap[adSlotId]; + var adUnitCode = bidObj.placementCode; + + var bids = []; + + // Grab the bid for current slot + for (var cpmAndSlotId in indexObj) { + var match = /(T\d_)?(.+)_(\d+)_(\d+)/.exec(cpmAndSlotId); + var tier = match[1] || ''; + var slotID = match[2]; + var sizeID = match[3]; + var currentCPM = match[4]; + + var slotName = slotID + '_' + sizeID; + var slotObj = getSlotObj(cygnus_index_args, tier + slotName); + + // Bid is for the current slot + if (slotID === adSlotId) { + var bid = bidfactory.createBid(1); + bid.cpm = currentCPM / 100; + bid.ad = indexObj[cpmAndSlotId][0]; + bid.ad_id = adSlotId; + bid.bidderCode = ADAPTER_CODE; + bid.width = slotObj.width; + bid.height = slotObj.height; + bid.siteID = slotObj.siteID; + + bids.push(bid); + } + } + + var currentBid = undefined; + + //Pick the highest bidding price for this slot + if (bids.length > 0) { + // Choose the highest bid + for (var i = 0; i < bids.length; i++) { + var bid = bids[i]; + if (typeof currentBid === 'undefined') { + currentBid = bid; + continue; + } + + if (bid.cpm > currentBid.cpm) { + currentBid = bid; + } + } + + // No bids for expected bid, pass bid + } else { + var bid = bidfactory.createBid(2); + bid.bidderCode = ADAPTER_CODE; + currentBid = bid; + } + + bidmanager.addBidResponse(adUnitCode, currentBid); + } + } catch (e) { + utils.logError('Error calling index adapter', ADAPTER_NAME, e); + logErrorBidResponse(); + } + finally { + // ensure that previous targeting mapping is cleared + _IndexRequestData.targetIDToBid = {}; + } + + //slotIdMap is used to determine which slots will be bid on in a given request. + //Therefore it needs to be blanked after the request is handled, else we will submit 'bids' for the wrong ads. + slotIdMap={}; + }; + } + + /* + Function in order to add a slot into the list if it hasn't been created yet, else it returns the same list. + */ + function mergeSlotInto(slot,slotList){ + for(var i = 0; i < slotList.length; i++){ + if(slot.id === slotList[i].id){ + return slotList; + } + } + slotList.push(slot); + return slotList; + } + + function getSlotObj(obj, id) { + var arr = obj.slots; + var returnObj = {}; + utils._each(arr, function (value) { + if (value.id === id) { + returnObj = value; + } + }); + + return returnObj; + } + + function logErrorBidResponse() { + //no bid response + var bid = bidfactory.createBid(2); + bid.bidderCode = ADAPTER_CODE; + + //log error to first add unit + bidmanager.addBidResponse(firstAdUnitCode, bid); + } + + return { + callBids: _callBids + }; +}; + +module.exports = IndexExchangeAdapter; diff --git a/src/bidmanager.js b/src/bidmanager.js index c763d85cfa4..6984ce7b9c0 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -1,398 +1,398 @@ -import { uniques } from './utils'; - -var CONSTANTS = require('./constants.json'); -var utils = require('./utils.js'); -var events = require('./events'); - -var objectType_function = 'function'; - -var externalCallbackByAdUnitArr = []; -var externalCallbackArr = []; -var externalOneTimeCallback = null; -var _granularity = CONSTANTS.GRANULARITY_OPTIONS.MEDIUM; -var defaultBidderSettingsMap = {}; - -const _lgPriceCap = 5.00; -const _mgPriceCap = 20.00; -const _hgPriceCap = 20.00; - -/** - * Returns a list of bidders that we haven't received a response yet - * @return {array} [description] - */ -exports.getTimedOutBidders = function () { - return pbjs._bidsRequested - .map(getBidderCode) - .filter(uniques) - .filter(bidder => pbjs._bidsReceived - .map(getBidders) - .filter(uniques) - .indexOf(bidder) < 0); -}; - -function timestamp() { return new Date().getTime(); } - -function getBidderCode(bidSet) { - return bidSet.bidderCode; -} - -function getBidders(bid) { - return bid.bidder; -} - -function bidsBackAdUnit(adUnitCode) { - const requested = pbjs.adUnits.find(unit => unit.code === adUnitCode).bids.length; - const received = pbjs._bidsReceived.filter(bid => bid.adUnitCode === adUnitCode).length; - return requested === received; -} - -function add(a, b) { - return a + b; -} - -function bidsBackAll() { - const requested = pbjs._bidsRequested.map(bidSet => bidSet.bids.length).reduce(add); - const received = pbjs._bidsReceived.length; - return requested === received; -} - -exports.bidsBackAll = function() { - return bidsBackAll(); -}; - -function getBidSetForBidder(bidder) { - return pbjs._bidsRequested.find(bidSet => bidSet.bidderCode === bidder); -} - -/* - * This function should be called to by the bidder adapter to register a bid response - */ -exports.addBidResponse = function (adUnitCode, bid) { - if (bid) { - Object.assign(bid, { - responseTimestamp: timestamp(), - requestTimestamp: getBidSetForBidder(bid.bidderCode).start, - cpm: bid.cpm || 0, - bidder: bid.bidderCode, - adUnitCode - }); - bid.timeToRespond = bid.responseTimestamp - bid.requestTimestamp; - - //emit the bidAdjustment event before bidResponse, so bid response has the adjusted bid value - events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bid); - - //emit the bidResponse event - events.emit(CONSTANTS.EVENTS.BID_RESPONSE, adUnitCode, bid); - - //append price strings - const priceStringsObj = getPriceBucketString(bid.cpm, bid.height, bid.width); - bid.pbLg = priceStringsObj.low; - bid.pbMg = priceStringsObj.med; - bid.pbHg = priceStringsObj.high; - bid.pbAg = priceStringsObj.auto; - bid.pbDg = priceStringsObj.dense; - - //if there is any key value pairs to map do here - var keyValues = {}; - if (bid.bidderCode && bid.cpm !== 0) { - keyValues = getKeyValueTargetingPairs(bid.bidderCode, bid); - - if (bid.dealId) { - keyValues[`hb_deal_${bid.bidderCode}`] = bid.dealId; - } - - bid.adserverTargeting = keyValues; - } - - pbjs._bidsReceived.push(bid); - } - - if (bidsBackAdUnit(bid.adUnitCode)) { - triggerAdUnitCallbacks(bid.adUnitCode); - } - - if (bidsBackAll()) { - this.executeCallback(); - } - - if (bid.timeToRespond > pbjs.bidderTimeout) { - - events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, this.getTimedOutBidders()); - this.executeCallback(); - } -}; - -function getKeyValueTargetingPairs(bidderCode, custBidObj) { - var keyValues = {}; - var bidder_settings = pbjs.bidderSettings || {}; - - //1) set the keys from "standard" setting or from prebid defaults - if (custBidObj && bidder_settings) { - if (!bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]) { - bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] = { - adserverTargeting: [ - { - key: 'hb_bidder', - val: function (bidResponse) { - return bidResponse.bidderCode; - } - }, { - key: 'hb_adid', - val: function (bidResponse) { - return bidResponse.adId; - } - }, { - key: 'hb_pb', - val: function (bidResponse) { - if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.AUTO) { - return bidResponse.pbAg; - } else if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.DENSE) { - return bidResponse.pbDg; - } else if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.LOW) { - return bidResponse.pbLg; - } else if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.MEDIUM) { - return bidResponse.pbMg; - } else if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.HIGH) { - return bidResponse.pbHg; - } - } - }, { - key: 'hb_size', - val: function (bidResponse) { - return bidResponse.size; - - } - } - ] - }; - } - - setKeys(keyValues, bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD], custBidObj); - } - - //2) set keys from specific bidder setting override if they exist - if (bidderCode && custBidObj && bidder_settings && bidder_settings[bidderCode] && bidder_settings[bidderCode][CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]) { - setKeys(keyValues, bidder_settings[bidderCode], custBidObj); - custBidObj.alwaysUseBid = bidder_settings[bidderCode].alwaysUseBid; - } - - //2) set keys from standard setting. NOTE: this API doesn't seem to be in use by any Adapter - else if (defaultBidderSettingsMap[bidderCode]) { - setKeys(keyValues, defaultBidderSettingsMap[bidderCode], custBidObj); - custBidObj.alwaysUseBid = defaultBidderSettingsMap[bidderCode].alwaysUseBid; - } - - return keyValues; -} - -exports.getKeyValueTargetingPairs = function() { - return getKeyValueTargetingPairs(...arguments); -}; - -function setKeys(keyValues, bidderSettings, custBidObj) { - var targeting = bidderSettings[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]; - custBidObj.size = custBidObj.getSize(); - - utils._each(targeting, function (kvPair) { - var key = kvPair.key; - var value = kvPair.val; - - if (keyValues[key]) { - utils.logWarn('The key: ' + key + ' is getting ovewritten'); - } - - if (utils.isFn(value)) { - try { - keyValues[key] = value(custBidObj); - } catch (e) { - utils.logError('bidmanager', 'ERROR', e); - } - } else { - keyValues[key] = value; - } - }); - - return keyValues; -} - -exports.setPriceGranularity = function setPriceGranularity(granularity) { - var granularityOptions = CONSTANTS.GRANULARITY_OPTIONS; - if (Object.keys(granularityOptions).filter(option => granularity === granularityOptions[option])) { - _granularity = granularity; - } else { - utils.logWarn('Prebid Warning: setPriceGranularity was called with invalid setting, using' + - ' `medium` as default.'); - _granularity = CONSTANTS.GRANULARITY_OPTIONS.MEDIUM; - } -}; - -exports.registerDefaultBidderSetting = function (bidderCode, defaultSetting) { - defaultBidderSettingsMap[bidderCode] = defaultSetting; -}; - -exports.executeCallback = function () { - if (externalCallbackArr.called !== true) { - processCallbacks(externalCallbackArr); - externalCallbackArr.called = true; - } - - //execute one time callback - if (externalOneTimeCallback) { - processCallbacks([externalOneTimeCallback]); - externalOneTimeCallback = null; - } - - pbjs.clearAuction(); -}; - -function triggerAdUnitCallbacks(adUnitCode) { - //todo : get bid responses and send in args - var params = [adUnitCode]; - processCallbacks(externalCallbackByAdUnitArr, params); -} - -function processCallbacks(callbackQueue) { - var i; - if (utils.isArray(callbackQueue)) { - for (i = 0; i < callbackQueue.length; i++) { - var func = callbackQueue[i]; - func.call(pbjs, pbjs._bidsReceived.reduce(groupByPlacement, {})); - } - } -} - -/** - * groupByPlacement is a reduce function that converts an array of Bid objects - * to an object with placement codes as keys, with each key representing an object - * with an array of `Bid` objects for that placement - * @param prev previous value as accumulator object - * @param item current array item - * @param idx current index - * @param arr the array being reduced - * @returns {*} as { [adUnitCode]: { bids: [Bid, Bid, Bid] } } - */ -function groupByPlacement(prev, item, idx, arr) { - // this uses a standard "array to map" operation that could be abstracted further - if (item.adUnitCode in Object.keys(prev)) { - // if the adUnitCode key is present in the accumulator object, continue - return prev; - } else { - // otherwise add the adUnitCode key to the accumulator object and set to an object with an - // array of Bids for that adUnitCode - prev[item.adUnitCode] = { - bids: arr.filter(bid => bid.adUnitCode === item.adUnitCode) - }; - return prev; - } -} - -/** - * Add a one time callback, that is discarded after it is called - * @param {Function} callback [description] - */ -exports.addOneTimeCallback = function (callback) { - externalOneTimeCallback = callback; -}; - -exports.addCallback = function (id, callback, cbEvent) { - callback.id = id; - if (CONSTANTS.CB.TYPE.ALL_BIDS_BACK === cbEvent) { - externalCallbackArr.push(callback); - } else if (CONSTANTS.CB.TYPE.AD_UNIT_BIDS_BACK === cbEvent) { - externalCallbackByAdUnitArr.push(callback); - } -}; - -//register event for bid adjustment -events.on(CONSTANTS.EVENTS.BID_ADJUSTMENT, function (bid) { - adjustBids(bid); -}); - -function adjustBids(bid) { - var code = bid.bidderCode; - var bidPriceAdjusted = bid.cpm; - if (code && pbjs.bidderSettings && pbjs.bidderSettings[code]) { - if (typeof pbjs.bidderSettings[code].bidCpmAdjustment === objectType_function) { - try { - bidPriceAdjusted = pbjs.bidderSettings[code].bidCpmAdjustment.call(null, bid.cpm); - } - catch (e) { - utils.logError('Error during bid adjustment', 'bidmanager.js', e); - } - } - } - - if (bidPriceAdjusted !== 0) { - bid.cpm = bidPriceAdjusted; - } -} - -function getPriceBucketString(cpm) { - var cpmFloat = 0; - var returnObj = { - low: '', - med: '', - high: '', - auto: '', - dense: '' - }; - try { - cpmFloat = parseFloat(cpm); - if (cpmFloat) { - //round to closest .5 - if (cpmFloat > _lgPriceCap) { - returnObj.low = _lgPriceCap.toFixed(2); - } else { - returnObj.low = (Math.floor(cpm * 2) / 2).toFixed(2); - } - - //round to closest .1 - if (cpmFloat > _mgPriceCap) { - returnObj.med = _mgPriceCap.toFixed(2); - } else { - returnObj.med = (Math.floor(cpm * 10) / 10).toFixed(2); - } - - //round to closest .01 - if (cpmFloat > _hgPriceCap) { - returnObj.high = _hgPriceCap.toFixed(2); - } else { - returnObj.high = (Math.floor(cpm * 100) / 100).toFixed(2); - } - - // round auto default sliding scale - if (cpmFloat <= 5) { - // round to closest .05 - returnObj.auto = (Math.floor(cpm * 20) / 20).toFixed(2); - } else if (cpmFloat <= 10) { - // round to closest .10 - returnObj.auto = (Math.floor(cpm * 10) / 10).toFixed(2); - } else if (cpmFloat <= 20) { - // round to closest .50 - returnObj.auto = (Math.floor(cpm * 2) / 2).toFixed(2); - } else { - // cap at 20.00 - returnObj.auto = '20.00'; - } - - // dense mode - if (cpmFloat <= 3) { - // round to closest .01 - returnObj.dense = (Math.floor(cpm * 100) / 100).toFixed(2); - } else if (cpmFloat <= 8) { - // round to closest .05 - returnObj.dense = (Math.floor(cpm * 20) / 20).toFixed(2); - } else if (cpmFloat <= 20) { - // round to closest .50 - returnObj.dense = (Math.floor(cpm * 2) / 2).toFixed(2); - } else { - // cap at 20.00 - returnObj.dense = '20.00'; - } - } - } catch (e) { - this.logError('Exception parsing CPM :' + e.message); - } - - return returnObj; -} +import { uniques } from './utils'; + +var CONSTANTS = require('./constants.json'); +var utils = require('./utils.js'); +var events = require('./events'); + +var objectType_function = 'function'; + +var externalCallbackByAdUnitArr = []; +var externalCallbackArr = []; +var externalOneTimeCallback = null; +var _granularity = CONSTANTS.GRANULARITY_OPTIONS.MEDIUM; +var defaultBidderSettingsMap = {}; + +const _lgPriceCap = 5.00; +const _mgPriceCap = 20.00; +const _hgPriceCap = 20.00; + +/** + * Returns a list of bidders that we haven't received a response yet + * @return {array} [description] + */ +exports.getTimedOutBidders = function () { + return pbjs._bidsRequested + .map(getBidderCode) + .filter(uniques) + .filter(bidder => pbjs._bidsReceived + .map(getBidders) + .filter(uniques) + .indexOf(bidder) < 0); +}; + +function timestamp() { return new Date().getTime(); } + +function getBidderCode(bidSet) { + return bidSet.bidderCode; +} + +function getBidders(bid) { + return bid.bidder; +} + +function bidsBackAdUnit(adUnitCode) { + const requested = pbjs.adUnits.find(unit => unit.code === adUnitCode).bids.length; + const received = pbjs._bidsReceived.filter(bid => bid.adUnitCode === adUnitCode).length; + return requested === received; +} + +function add(a, b) { + return a + b; +} + +function bidsBackAll() { + const requested = pbjs._bidsRequested.map(bidSet => bidSet.bids.length).reduce(add); + const received = pbjs._bidsReceived.length; + return requested === received; +} + +exports.bidsBackAll = function() { + return bidsBackAll(); +}; + +function getBidSetForBidder(bidder) { + return pbjs._bidsRequested.find(bidSet => bidSet.bidderCode === bidder) || { start: null }; +} + +/* + * This function should be called to by the bidder adapter to register a bid response + */ +exports.addBidResponse = function (adUnitCode, bid) { + if (bid) { + Object.assign(bid, { + responseTimestamp: timestamp(), + requestTimestamp: getBidSetForBidder(bid.bidderCode).start, + cpm: bid.cpm || 0, + bidder: bid.bidderCode, + adUnitCode + }); + bid.timeToRespond = bid.responseTimestamp - bid.requestTimestamp; + + //emit the bidAdjustment event before bidResponse, so bid response has the adjusted bid value + events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bid); + + //emit the bidResponse event + events.emit(CONSTANTS.EVENTS.BID_RESPONSE, adUnitCode, bid); + + //append price strings + const priceStringsObj = getPriceBucketString(bid.cpm, bid.height, bid.width); + bid.pbLg = priceStringsObj.low; + bid.pbMg = priceStringsObj.med; + bid.pbHg = priceStringsObj.high; + bid.pbAg = priceStringsObj.auto; + bid.pbDg = priceStringsObj.dense; + + //if there is any key value pairs to map do here + var keyValues = {}; + if (bid.bidderCode && bid.cpm !== 0) { + keyValues = getKeyValueTargetingPairs(bid.bidderCode, bid); + + if (bid.dealId) { + keyValues[`hb_deal_${bid.bidderCode}`] = bid.dealId; + } + + bid.adserverTargeting = keyValues; + } + + pbjs._bidsReceived.push(bid); + } + + if (bidsBackAdUnit(bid.adUnitCode)) { + triggerAdUnitCallbacks(bid.adUnitCode); + } + + if (bidsBackAll()) { + this.executeCallback(); + } + + if (bid.timeToRespond > pbjs.bidderTimeout) { + + events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, this.getTimedOutBidders()); + this.executeCallback(); + } +}; + +function getKeyValueTargetingPairs(bidderCode, custBidObj) { + var keyValues = {}; + var bidder_settings = pbjs.bidderSettings || {}; + + //1) set the keys from "standard" setting or from prebid defaults + if (custBidObj && bidder_settings) { + if (!bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]) { + bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] = { + adserverTargeting: [ + { + key: 'hb_bidder', + val: function (bidResponse) { + return bidResponse.bidderCode; + } + }, { + key: 'hb_adid', + val: function (bidResponse) { + return bidResponse.adId; + } + }, { + key: 'hb_pb', + val: function (bidResponse) { + if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.AUTO) { + return bidResponse.pbAg; + } else if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.DENSE) { + return bidResponse.pbDg; + } else if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.LOW) { + return bidResponse.pbLg; + } else if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.MEDIUM) { + return bidResponse.pbMg; + } else if (_granularity === CONSTANTS.GRANULARITY_OPTIONS.HIGH) { + return bidResponse.pbHg; + } + } + }, { + key: 'hb_size', + val: function (bidResponse) { + return bidResponse.size; + + } + } + ] + }; + } + + setKeys(keyValues, bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD], custBidObj); + } + + //2) set keys from specific bidder setting override if they exist + if (bidderCode && custBidObj && bidder_settings && bidder_settings[bidderCode] && bidder_settings[bidderCode][CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]) { + setKeys(keyValues, bidder_settings[bidderCode], custBidObj); + custBidObj.alwaysUseBid = bidder_settings[bidderCode].alwaysUseBid; + } + + //2) set keys from standard setting. NOTE: this API doesn't seem to be in use by any Adapter + else if (defaultBidderSettingsMap[bidderCode]) { + setKeys(keyValues, defaultBidderSettingsMap[bidderCode], custBidObj); + custBidObj.alwaysUseBid = defaultBidderSettingsMap[bidderCode].alwaysUseBid; + } + + return keyValues; +} + +exports.getKeyValueTargetingPairs = function() { + return getKeyValueTargetingPairs(...arguments); +}; + +function setKeys(keyValues, bidderSettings, custBidObj) { + var targeting = bidderSettings[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]; + custBidObj.size = custBidObj.getSize(); + + utils._each(targeting, function (kvPair) { + var key = kvPair.key; + var value = kvPair.val; + + if (keyValues[key]) { + utils.logWarn('The key: ' + key + ' is getting ovewritten'); + } + + if (utils.isFn(value)) { + try { + keyValues[key] = value(custBidObj); + } catch (e) { + utils.logError('bidmanager', 'ERROR', e); + } + } else { + keyValues[key] = value; + } + }); + + return keyValues; +} + +exports.setPriceGranularity = function setPriceGranularity(granularity) { + var granularityOptions = CONSTANTS.GRANULARITY_OPTIONS; + if (Object.keys(granularityOptions).filter(option => granularity === granularityOptions[option])) { + _granularity = granularity; + } else { + utils.logWarn('Prebid Warning: setPriceGranularity was called with invalid setting, using' + + ' `medium` as default.'); + _granularity = CONSTANTS.GRANULARITY_OPTIONS.MEDIUM; + } +}; + +exports.registerDefaultBidderSetting = function (bidderCode, defaultSetting) { + defaultBidderSettingsMap[bidderCode] = defaultSetting; +}; + +exports.executeCallback = function () { + if (externalCallbackArr.called !== true) { + processCallbacks(externalCallbackArr); + externalCallbackArr.called = true; + } + + //execute one time callback + if (externalOneTimeCallback) { + processCallbacks([externalOneTimeCallback]); + externalOneTimeCallback = null; + } + + pbjs.clearAuction(); +}; + +function triggerAdUnitCallbacks(adUnitCode) { + //todo : get bid responses and send in args + var params = [adUnitCode]; + processCallbacks(externalCallbackByAdUnitArr, params); +} + +function processCallbacks(callbackQueue) { + var i; + if (utils.isArray(callbackQueue)) { + for (i = 0; i < callbackQueue.length; i++) { + var func = callbackQueue[i]; + func.call(pbjs, pbjs._bidsReceived.reduce(groupByPlacement, {})); + } + } +} + +/** + * groupByPlacement is a reduce function that converts an array of Bid objects + * to an object with placement codes as keys, with each key representing an object + * with an array of `Bid` objects for that placement + * @param prev previous value as accumulator object + * @param item current array item + * @param idx current index + * @param arr the array being reduced + * @returns {*} as { [adUnitCode]: { bids: [Bid, Bid, Bid] } } + */ +function groupByPlacement(prev, item, idx, arr) { + // this uses a standard "array to map" operation that could be abstracted further + if (item.adUnitCode in Object.keys(prev)) { + // if the adUnitCode key is present in the accumulator object, continue + return prev; + } else { + // otherwise add the adUnitCode key to the accumulator object and set to an object with an + // array of Bids for that adUnitCode + prev[item.adUnitCode] = { + bids: arr.filter(bid => bid.adUnitCode === item.adUnitCode) + }; + return prev; + } +} + +/** + * Add a one time callback, that is discarded after it is called + * @param {Function} callback [description] + */ +exports.addOneTimeCallback = function (callback) { + externalOneTimeCallback = callback; +}; + +exports.addCallback = function (id, callback, cbEvent) { + callback.id = id; + if (CONSTANTS.CB.TYPE.ALL_BIDS_BACK === cbEvent) { + externalCallbackArr.push(callback); + } else if (CONSTANTS.CB.TYPE.AD_UNIT_BIDS_BACK === cbEvent) { + externalCallbackByAdUnitArr.push(callback); + } +}; + +//register event for bid adjustment +events.on(CONSTANTS.EVENTS.BID_ADJUSTMENT, function (bid) { + adjustBids(bid); +}); + +function adjustBids(bid) { + var code = bid.bidderCode; + var bidPriceAdjusted = bid.cpm; + if (code && pbjs.bidderSettings && pbjs.bidderSettings[code]) { + if (typeof pbjs.bidderSettings[code].bidCpmAdjustment === objectType_function) { + try { + bidPriceAdjusted = pbjs.bidderSettings[code].bidCpmAdjustment.call(null, bid.cpm); + } + catch (e) { + utils.logError('Error during bid adjustment', 'bidmanager.js', e); + } + } + } + + if (bidPriceAdjusted !== 0) { + bid.cpm = bidPriceAdjusted; + } +} + +function getPriceBucketString(cpm) { + var cpmFloat = 0; + var returnObj = { + low: '', + med: '', + high: '', + auto: '', + dense: '' + }; + try { + cpmFloat = parseFloat(cpm); + if (cpmFloat) { + //round to closest .5 + if (cpmFloat > _lgPriceCap) { + returnObj.low = _lgPriceCap.toFixed(2); + } else { + returnObj.low = (Math.floor(cpm * 2) / 2).toFixed(2); + } + + //round to closest .1 + if (cpmFloat > _mgPriceCap) { + returnObj.med = _mgPriceCap.toFixed(2); + } else { + returnObj.med = (Math.floor(cpm * 10) / 10).toFixed(2); + } + + //round to closest .01 + if (cpmFloat > _hgPriceCap) { + returnObj.high = _hgPriceCap.toFixed(2); + } else { + returnObj.high = (Math.floor(cpm * 100) / 100).toFixed(2); + } + + // round auto default sliding scale + if (cpmFloat <= 5) { + // round to closest .05 + returnObj.auto = (Math.floor(cpm * 20) / 20).toFixed(2); + } else if (cpmFloat <= 10) { + // round to closest .10 + returnObj.auto = (Math.floor(cpm * 10) / 10).toFixed(2); + } else if (cpmFloat <= 20) { + // round to closest .50 + returnObj.auto = (Math.floor(cpm * 2) / 2).toFixed(2); + } else { + // cap at 20.00 + returnObj.auto = '20.00'; + } + + // dense mode + if (cpmFloat <= 3) { + // round to closest .01 + returnObj.dense = (Math.floor(cpm * 100) / 100).toFixed(2); + } else if (cpmFloat <= 8) { + // round to closest .05 + returnObj.dense = (Math.floor(cpm * 20) / 20).toFixed(2); + } else if (cpmFloat <= 20) { + // round to closest .50 + returnObj.dense = (Math.floor(cpm * 2) / 2).toFixed(2); + } else { + // cap at 20.00 + returnObj.dense = '20.00'; + } + } + } catch (e) { + this.logError('Exception parsing CPM :' + e.message); + } + + return returnObj; +} diff --git a/src/prebid.js b/src/prebid.js index 3be970493aa..35235bcf660 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -1,705 +1,723 @@ -/** @module pbjs */ - -import { flatten, uniques, getKeys, isGptPubadsDefined, getHighestCpm } from './utils'; -import 'polyfill'; - -// if pbjs already exists in global document scope, use it, if not, create the object -window.pbjs = (window.pbjs || {}); -window.pbjs.que = window.pbjs.que || []; -var pbjs = window.pbjs; -var CONSTANTS = require('./constants.json'); -var utils = require('./utils.js'); -var bidmanager = require('./bidmanager.js'); -var adaptermanager = require('./adaptermanager'); -var bidfactory = require('./bidfactory'); -var adloader = require('./adloader'); -var ga = require('./ga'); -var events = require('./events'); - -/* private variables */ - -var objectType_function = 'function'; -var objectType_undefined = 'undefined'; -var objectType_object = 'object'; -var BID_WON = CONSTANTS.EVENTS.BID_WON; -var BID_TIMEOUT = CONSTANTS.EVENTS.BID_TIMEOUT; - -var pb_bidsTimedOut = false; -var auctionRunning = false; -var presetTargeting = []; - -var eventValidators = { - bidWon: checkDefinedPlacement -}; - -/* Public vars */ - -pbjs._bidsRequested = []; -pbjs._bidsReceived = []; -pbjs._adsReceived = []; -pbjs._sendAllBids = false; - -//default timeout for all bids -pbjs.bidderTimeout = pbjs.bidderTimeout || 2000; -pbjs.logging = pbjs.logging || false; - -//let the world know we are loaded -pbjs.libLoaded = true; - -//version auto generated from build -utils.logInfo('Prebid.js v$prebid.version$ loaded'); - -//create adUnit array -pbjs.adUnits = pbjs.adUnits || []; - -/** - * Command queue that functions will execute once prebid.js is loaded - * @param {function} cmd Annoymous function to execute - * @alias module:pbjs.que.push - */ -pbjs.que.push = function (cmd) { - if (typeof cmd === objectType_function) { - try { - cmd.call(); - } catch (e) { - utils.logError('Error processing command :' + e.message); - } - } else { - utils.logError('Commands written into pbjs.que.push must wrapped in a function'); - } -}; - -function processQue() { - for (var i = 0; i < pbjs.que.length; i++) { - if (typeof pbjs.que[i].called === objectType_undefined) { - try { - pbjs.que[i].call(); - pbjs.que[i].called = true; - } - catch (e) { - utils.logError('Error processing command :', 'prebid.js', e); - } - } - } -} - -function timeOutBidders() { - if (!pb_bidsTimedOut) { - pb_bidsTimedOut = true; - var timedOutBidders = bidmanager.getTimedOutBidders(); - events.emit(BID_TIMEOUT, timedOutBidders); - } -} - -function checkDefinedPlacement(id) { - var placementCodes = pbjs._bidsRequested.map(bidSet => bidSet.bids.map(bid => bid.placementCode)) - .reduce(flatten) - .filter(uniques); - - if (!utils.contains(placementCodes, id)) { - utils.logError('The "' + id + '" placement is not defined.'); - return; - } - - return true; -} - -function resetPresetTargeting() { - if (isGptPubadsDefined()) { - window.googletag.pubads().getSlots().forEach(slot => { - slot.clearTargeting(); - }); - - setTargeting(presetTargeting); - } -} - -function setTargeting(targetingConfig) { - window.googletag.pubads().getSlots().forEach(slot => { - targetingConfig.filter(targeting => Object.keys(targeting)[0] === slot.getAdUnitPath() || - Object.keys(targeting)[0] === slot.getSlotElementId()) - .forEach(targeting => targeting[Object.keys(targeting)[0]] - .forEach(key => { - key[Object.keys(key)[0]] - .map((value) => { - utils.logMessage(`Attempting to set key value for slot: ${slot.getSlotElementId()} key: ${Object.keys(key)[0]} value: ${value}`); - return value; - }) - .forEach(value => slot.setTargeting(Object.keys(key)[0], value)); - })); - }); -} - -function getWinningBidTargeting() { - if (isGptPubadsDefined()) { - presetTargeting = (function getPresetTargeting() { - return window.googletag.pubads().getSlots().map(slot => { - return { - [slot.getAdUnitPath()]: slot.getTargetingKeys().map(key => { - return { [key]: slot.getTargeting(key) }; - }) - }; - }); - })(); - } - - let winners = pbjs._bidsReceived.map(bid => bid.adUnitCode) - .filter(uniques) - .map(adUnitCode => pbjs._bidsReceived - .filter(bid => bid.adUnitCode === adUnitCode ? bid : null) - .reduce(getHighestCpm, - { - adUnitCode: adUnitCode, - cpm: 0, - adserverTargeting: {}, - timeToRespond: 0 - })); - - // winning bids with deals need an hb_deal targeting key - winners - .filter(bid => bid.dealId) - .map(bid => bid.adserverTargeting.hb_deal = bid.dealId); - - winners = winners.map(winner => { - return { - [winner.adUnitCode]: Object.keys(winner.adserverTargeting, key => key) - .map(key => { - return { [key.substring(0, 20)]: [winner.adserverTargeting[key]] }; - }) - }; - }); - - if (presetTargeting) { - winners.concat(presetTargeting); - } - - return winners; -} - -function getDealTargeting() { - return pbjs._bidsReceived.filter(bid => bid.dealId).map(bid => { - const dealKey = `hb_deal_${bid.bidderCode}`; - return { - [bid.adUnitCode]: CONSTANTS.TARGETING_KEYS.map(key => { - return { - [`${key}_${bid.bidderCode}`.substring(0, 20)]: [bid.adserverTargeting[key]] - }; - }) - .concat({ [dealKey]: [bid.adserverTargeting[dealKey]] }) - }; - }); -} - -/** - * Get custom targeting keys for bids that have `alwaysUseBid=true`. - */ -function getAlwaysUseBidTargeting() { - return pbjs._bidsReceived.map(bid => { - if (bid.alwaysUseBid) { - const standardKeys = CONSTANTS.TARGETING_KEYS; - return { - [bid.adUnitCode]: Object.keys(bid.adserverTargeting, key => key).map(key => { - // Get only the non-standard keys of the losing bids, since we - // don't want to override the standard keys of the winning bid. - if (standardKeys.indexOf(key) > -1) { - return; - } - - return { [key.substring(0, 20)]: [bid.adserverTargeting[key]] }; - - }).filter(key => key) // remove empty elements - }; - } - }).filter(bid => bid); // removes empty elements in array; -} - -function getBidLandscapeTargeting() { - const standardKeys = CONSTANTS.TARGETING_KEYS; - - return pbjs._bidsReceived.map(bid => { - if (bid.adserverTargeting) { - return { - [bid.adUnitCode]: standardKeys.map(key => { - return { - [`${key}_${bid.bidderCode}`.substring(0, 20)]: [bid.adserverTargeting[key]] - }; - }) - }; - } - }).filter(bid => bid); // removes empty elements in array -} - -function getAllTargeting() { - // Get targeting for the winning bid. Add targeting for any bids that have - // `alwaysUseBid=true`. If sending all bids is enabled, add targeting for losing bids. - return getDealTargeting() - .concat(getWinningBidTargeting()) - .concat(getAlwaysUseBidTargeting()) - .concat(pbjs._sendAllBids ? getBidLandscapeTargeting() : []); -} - -////////////////////////////////// -// // -// Start Public APIs // -// // -////////////////////////////////// - -/** - * This function returns the query string targeting parameters available at this moment for a given ad unit. Note that some bidder's response may not have been received if you call this function too quickly after the requests are sent. - * @param {string} [adunitCode] adUnitCode to get the bid responses for - * @alias module:pbjs.getAdserverTargetingForAdUnitCodeStr - * @return {array} returnObj return bids array - */ -pbjs.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { - utils.logInfo('Invoking pbjs.getAdserverTargetingForAdUnitCodeStr', arguments); - - // call to retrieve bids array - if (adunitCode) { - var res = pbjs.getAdserverTargetingForAdUnitCode(adunitCode); - return utils.transformAdServerTargetingObj(res); - } else { - utils.logMessage('Need to call getAdserverTargetingForAdUnitCodeStr with adunitCode'); - } -}; - -/** -* This function returns the query string targeting parameters available at this moment for a given ad unit. Note that some bidder's response may not have been received if you call this function too quickly after the requests are sent. - * @param adUnitCode {string} adUnitCode to get the bid responses for - * @returns {object} returnObj return bids - */ -pbjs.getAdserverTargetingForAdUnitCode = function (adUnitCode) { - utils.logInfo('Invoking pbjs.getAdserverTargetingForAdUnitCode', arguments); - - return getAllTargeting().filter(targeting => getKeys(targeting)[0] === adUnitCode) - .map(targeting => { - return { - [Object.keys(targeting)[0]]: targeting[Object.keys(targeting)[0]] - .map(target => { - return { - [Object.keys(target)[0]]: target[Object.keys(target)[0]].join(', ') - }; - }).reduce((p, c) => Object.assign(c, p), {}) - }; - }) - .reduce(function (accumulator, targeting) { - var key = Object.keys(targeting)[0]; - accumulator[key] = Object.assign({}, accumulator[key], targeting[key]); - return accumulator; - }, {})[adUnitCode]; -}; - -/** - * returns all ad server targeting for all ad units - * @return {object} Map of adUnitCodes and targeting values [] - * @alias module:pbjs.getAdserverTargeting - */ - -pbjs.getAdserverTargeting = function () { - utils.logInfo('Invoking pbjs.getAdserverTargeting', arguments); - return getAllTargeting() - .map(targeting => { - return { - [Object.keys(targeting)[0]]: targeting[Object.keys(targeting)[0]] - .map(target => { - return { - [Object.keys(target)[0]]: target[Object.keys(target)[0]].join(', ') - }; - }).reduce((p, c) => Object.assign(c, p), {}) - }; - }) - .reduce(function (accumulator, targeting) { - var key = Object.keys(targeting)[0]; - accumulator[key] = Object.assign({}, accumulator[key], targeting[key]); - return accumulator; - }, {}); -}; - -/** - * This function returns the bid responses at the given moment. - * @alias module:pbjs.getBidResponses - * @return {object} map | object that contains the bidResponses - */ - -pbjs.getBidResponses = function () { - utils.logInfo('Invoking pbjs.getBidResponses', arguments); - - return pbjs._bidsReceived.map(bid => bid.adUnitCode) - .filter(uniques).map(adUnitCode => pbjs._bidsReceived - .filter(bid => bid.adUnitCode === adUnitCode)) - .map(bids => { - return { - [bids[0].adUnitCode]: { bids: bids } - }; - }) - .reduce((a, b) => Object.assign(a, b), {}); -}; - -/** - * Returns bidResponses for the specified adUnitCode - * @param {String} adUnitCode adUnitCode - * @alias module:pbjs.getBidResponsesForAdUnitCode - * @return {Object} bidResponse object - */ - -pbjs.getBidResponsesForAdUnitCode = function (adUnitCode) { - const bids = pbjs._bidsReceived.filter(bid => bid.adUnitCode === adUnitCode); - return { - bids: bids - }; -}; - -/** - * Set query string targeting on all GPT ad units. - * @alias module:pbjs.setTargetingForGPTAsync - */ -pbjs.setTargetingForGPTAsync = function () { - utils.logInfo('Invoking pbjs.setTargetingForGPTAsync', arguments); - if (!isGptPubadsDefined()) { - utils.logError('window.googletag is not defined on the page'); - return; - } - - setTargeting(getAllTargeting()); -}; - -/** - * Returns a bool if all the bids have returned or timed out - * @alias module:pbjs.allBidsAvailable - * @return {bool} all bids available - */ -pbjs.allBidsAvailable = function () { - utils.logInfo('Invoking pbjs.allBidsAvailable', arguments); - return bidmanager.bidsBackAll(); -}; - -/** - * This function will render the ad (based on params) in the given iframe document passed through. Note that doc SHOULD NOT be the parent document page as we can't doc.write() asynchrounsly - * @param {object} doc document - * @param {string} id bid id to locate the ad - * @alias module:pbjs.renderAd - */ -pbjs.renderAd = function (doc, id) { - utils.logInfo('Invoking pbjs.renderAd', arguments); - utils.logMessage('Calling renderAd with adId :' + id); - if (doc && id) { - try { - //lookup ad by ad Id - var adObject = pbjs._bidsReceived.find(bid => bid.adId === id); - if (adObject) { - //emit 'bid won' event here - events.emit(BID_WON, adObject); - var height = adObject.height; - var width = adObject.width; - var url = adObject.adUrl; - var ad = adObject.ad; - - if (ad) { - doc.write(ad); - doc.close(); - if (doc.defaultView && doc.defaultView.frameElement) { - doc.defaultView.frameElement.width = width; - doc.defaultView.frameElement.height = height; - } - } - - //doc.body.style.width = width; - //doc.body.style.height = height; - else if (url) { - doc.write(''); - doc.close(); - - if (doc.defaultView && doc.defaultView.frameElement) { - doc.defaultView.frameElement.width = width; - doc.defaultView.frameElement.height = height; - } - - } else { - utils.logError('Error trying to write ad. No ad for bid response id: ' + id); - } - - } else { - utils.logError('Error trying to write ad. Cannot find ad by given id : ' + id); - } - - } catch (e) { - utils.logError('Error trying to write ad Id :' + id + ' to the page:' + e.message); - } - } else { - utils.logError('Error trying to write ad Id :' + id + ' to the page. Missing document or adId'); - } - -}; - -/** - * Remove adUnit from the pbjs configuration - * @param {String} adUnitCode the adUnitCode to remove - * @alias module:pbjs.removeAdUnit - */ -pbjs.removeAdUnit = function (adUnitCode) { - utils.logInfo('Invoking pbjs.removeAdUnit', arguments); - if (adUnitCode) { - for (var i = 0; i < pbjs.adUnits.length; i++) { - if (pbjs.adUnits[i].code === adUnitCode) { - pbjs.adUnits.splice(i, 1); - } - } - } -}; - -pbjs.clearAuction = function() { - auctionRunning = false; - utils.logMessage('Prebid auction cleared'); -}; - -/** - * - * @param bidsBackHandler - * @param timeout - * @param adUnits - * @param adUnitCodes - */ -pbjs.requestBids = function ({ bidsBackHandler, timeout, adUnits, adUnitCodes }) { - if (auctionRunning) { - utils.logError('Prebid Error: `pbjs.requestBids` was called while a previous auction was' + - ' still running. Resubmit this request.'); - return; - } else { - auctionRunning = true; - pbjs._bidsRequested = []; - pbjs._bidsReceived = []; - resetPresetTargeting(); - } - - const cbTimeout = timeout || pbjs.bidderTimeout; - - // use adUnits provided or from pbjs global - adUnits = adUnits || pbjs.adUnits; - - // if specific adUnitCodes filter adUnits for those codes - if (adUnitCodes && adUnitCodes.length) { - adUnits = adUnits.filter(adUnit => adUnitCodes.includes(adUnit.code)); - } - - if (typeof bidsBackHandler === objectType_function) { - bidmanager.addOneTimeCallback(bidsBackHandler); - } - - utils.logInfo('Invoking pbjs.requestBids', arguments); - - if (!adUnits || adUnits.length === 0) { - utils.logMessage('No adUnits configured. No bids requested.'); - return; - } - - //set timeout for all bids - setTimeout(bidmanager.executeCallback, cbTimeout); - - adaptermanager.callBids({ adUnits, adUnitCodes }); -}; - -/** - * - * Add adunit(s) - * @param {Array|String} adUnitArr Array of adUnits or single adUnit Object. - * @alias module:pbjs.addAdUnits - */ -pbjs.addAdUnits = function (adUnitArr) { - utils.logInfo('Invoking pbjs.addAdUnits', arguments); - if (utils.isArray(adUnitArr)) { - //append array to existing - pbjs.adUnits.push.apply(pbjs.adUnits, adUnitArr); - } else if (typeof adUnitArr === objectType_object) { - pbjs.adUnits.push(adUnitArr); - } -}; - -/** - * @param {String} event the name of the event - * @param {Function} handler a callback to set on event - * @param {String} id an identifier in the context of the event - * - * This API call allows you to register a callback to handle a Prebid.js event. - * An optional `id` parameter provides more finely-grained event callback registration. - * This makes it possible to register callback events for a specific item in the - * event context. For example, `bidWon` events will accept an `id` for ad unit code. - * `bidWon` callbacks registered with an ad unit code id will be called when a bid - * for that ad unit code wins the auction. Without an `id` this method registers the - * callback for every `bidWon` event. - * - * Currently `bidWon` is the only event that accepts an `id` parameter. - */ -pbjs.onEvent = function (event, handler, id) { - utils.logInfo('Invoking pbjs.onEvent', arguments); - if (!utils.isFn(handler)) { - utils.logError('The event handler provided is not a function and was not set on event "' + event + '".'); - return; - } - - if (id && !eventValidators[event].call(null, id)) { - utils.logError('The id provided is not valid for event "' + event + '" and no handler was set.'); - return; - } - - events.on(event, handler, id); -}; - -/** - * @param {String} event the name of the event - * @param {Function} handler a callback to remove from the event - * @param {String} id an identifier in the context of the event (see `pbjs.onEvent`) - */ -pbjs.offEvent = function (event, handler, id) { - utils.logInfo('Invoking pbjs.offEvent', arguments); - if (id && !eventValidators[event].call(null, id)) { - return; - } - - events.off(event, handler, id); -}; - -/** - * Add a callback event - * @param {String} eventStr event to attach callback to Options: "allRequestedBidsBack" | "adUnitBidsBack" - * @param {Function} func function to execute. Paramaters passed into the function: (bidResObj), [adUnitCode]); - * @alias module:pbjs.addCallback - * @returns {String} id for callback - */ -pbjs.addCallback = function (eventStr, func) { - utils.logInfo('Invoking pbjs.addCallback', arguments); - var id = null; - if (!eventStr || !func || typeof func !== objectType_function) { - utils.logError('error registering callback. Check method signature'); - return id; - } - - id = utils.getUniqueIdentifierStr; - bidmanager.addCallback(id, func, eventStr); - return id; -}; - -/** - * Remove a callback event - * //@param {string} cbId id of the callback to remove - * @alias module:pbjs.removeCallback - * @returns {String} id for callback - */ -pbjs.removeCallback = function (/* cbId */) { - //todo - return null; -}; - -/** - * Wrapper to register bidderAdapter externally (adaptermanager.registerBidAdapter()) - * @param {[type]} bidderAdaptor [description] - * @param {[type]} bidderCode [description] - * @return {[type]} [description] - */ -pbjs.registerBidAdapter = function (bidderAdaptor, bidderCode) { - utils.logInfo('Invoking pbjs.registerBidAdapter', arguments); - try { - adaptermanager.registerBidAdapter(bidderAdaptor(), bidderCode); - } - catch (e) { - utils.logError('Error registering bidder adapter : ' + e.message); - } -}; - -pbjs.bidsAvailableForAdapter = function (bidderCode) { - utils.logInfo('Invoking pbjs.bidsAvailableForAdapter', arguments); - - pbjs._bidsRequested.find(bidderRequest => bidderRequest.bidderCode === bidderCode).bids - .map(bid => { - return Object.assign(bid, bidfactory.createBid(1), { - bidderCode, - adUnitCode: bid.placementCode - }); - }) - .map(bid => pbjs._bidsReceived.push(bid)); -}; - -/** - * Wrapper to bidfactory.createBid() - * @param {[type]} statusCode [description] - * @return {[type]} [description] - */ -pbjs.createBid = function (statusCode) { - utils.logInfo('Invoking pbjs.createBid', arguments); - return bidfactory.createBid(statusCode); -}; - -/** - * Wrapper to bidmanager.addBidResponse - * @param {[type]} adUnitCode [description] - * @param {[type]} bid [description] - */ -pbjs.addBidResponse = function (adUnitCode, bid) { - utils.logInfo('Invoking pbjs.addBidResponse', arguments); - bidmanager.addBidResponse(adUnitCode, bid); -}; - -/** - * Wrapper to adloader.loadScript - * @param {[type]} tagSrc [description] - * @param {Function} callback [description] - * @return {[type]} [description] - */ -pbjs.loadScript = function (tagSrc, callback, useCache) { - utils.logInfo('Invoking pbjs.loadScript', arguments); - adloader.loadScript(tagSrc, callback, useCache); -}; - -/** - * Will enable sendinga prebid.js to data provider specified - * @param {object} options object {provider : 'string', options : {}} - */ -pbjs.enableAnalytics = function (options) { - utils.logInfo('Invoking pbjs.enableAnalytics', arguments); - if (!options) { - utils.logError('pbjs.enableAnalytics should be called with option {}', 'prebid.js'); - return; - } - - if (options.provider === 'ga') { - try { - ga.enableAnalytics(typeof options.options === 'undefined' ? {} : options.options); - } - catch (e) { - utils.logError('Error calling GA: ', 'prebid.js', e); - } - } else if (options.provider === 'other_provider') { - //todo - return null; - } -}; - -/** - * This will tell analytics that all bids received after are "timed out" - */ -pbjs.sendTimeoutEvent = function () { - utils.logInfo('Invoking pbjs.sendTimeoutEvent', arguments); - timeOutBidders(); -}; - -pbjs.aliasBidder = function (bidderCode, alias) { - utils.logInfo('Invoking pbjs.aliasBidder', arguments); - if (bidderCode && alias) { - adaptermanager.aliasBidAdapter(bidderCode, alias); - } else { - utils.logError('bidderCode and alias must be passed as arguments', 'pbjs.aliasBidder'); - } -}; - -pbjs.setPriceGranularity = function (granularity) { - utils.logInfo('Invoking pbjs.setPriceGranularity', arguments); - if (!granularity) { - utils.logError('Prebid Error: no value passed to `setPriceGranularity()`'); - } else { - bidmanager.setPriceGranularity(granularity); - } -}; - -pbjs.enableSendAllBids = function () { - pbjs._sendAllBids = true; -}; - -processQue(); +/** @module pbjs */ + +import { flatten, uniques, getKeys, isGptPubadsDefined, getHighestCpm } from './utils'; +import 'polyfill'; + +// if pbjs already exists in global document scope, use it, if not, create the object +window.pbjs = (window.pbjs || {}); +window.pbjs.que = window.pbjs.que || []; +var pbjs = window.pbjs; +var CONSTANTS = require('./constants.json'); +var utils = require('./utils.js'); +var bidmanager = require('./bidmanager.js'); +var adaptermanager = require('./adaptermanager'); +var bidfactory = require('./bidfactory'); +var adloader = require('./adloader'); +var ga = require('./ga'); +var events = require('./events'); + +/* private variables */ + +var objectType_function = 'function'; +var objectType_undefined = 'undefined'; +var objectType_object = 'object'; +var BID_WON = CONSTANTS.EVENTS.BID_WON; +var BID_TIMEOUT = CONSTANTS.EVENTS.BID_TIMEOUT; + +var pb_bidsTimedOut = false; +var auctionRunning = false; +var presetTargeting = []; +var pbTargetingKeys = []; + +var eventValidators = { + bidWon: checkDefinedPlacement +}; + +/* Public vars */ + +pbjs._bidsRequested = []; +pbjs._bidsReceived = []; +pbjs._adsReceived = []; +pbjs._sendAllBids = false; + +//default timeout for all bids +pbjs.bidderTimeout = pbjs.bidderTimeout || 2000; +pbjs.logging = pbjs.logging || false; + +//let the world know we are loaded +pbjs.libLoaded = true; + +//version auto generated from build +utils.logInfo('Prebid.js v$prebid.version$ loaded'); + +//create adUnit array +pbjs.adUnits = pbjs.adUnits || []; + +/** + * Command queue that functions will execute once prebid.js is loaded + * @param {function} cmd Annoymous function to execute + * @alias module:pbjs.que.push + */ +pbjs.que.push = function (cmd) { + if (typeof cmd === objectType_function) { + try { + cmd.call(); + } catch (e) { + utils.logError('Error processing command :' + e.message); + } + } else { + utils.logError('Commands written into pbjs.que.push must wrapped in a function'); + } +}; + +function processQue() { + for (var i = 0; i < pbjs.que.length; i++) { + if (typeof pbjs.que[i].called === objectType_undefined) { + try { + pbjs.que[i].call(); + pbjs.que[i].called = true; + } + catch (e) { + utils.logError('Error processing command :', 'prebid.js', e); + } + } + } +} + +function timeOutBidders() { + if (!pb_bidsTimedOut) { + pb_bidsTimedOut = true; + var timedOutBidders = bidmanager.getTimedOutBidders(); + events.emit(BID_TIMEOUT, timedOutBidders); + } +} + +function checkDefinedPlacement(id) { + var placementCodes = pbjs._bidsRequested.map(bidSet => bidSet.bids.map(bid => bid.placementCode)) + .reduce(flatten) + .filter(uniques); + + if (!utils.contains(placementCodes, id)) { + utils.logError('The "' + id + '" placement is not defined.'); + return; + } + + return true; +} + +function resetPresetTargeting() { + if (isGptPubadsDefined()) { + window.googletag.pubads().getSlots().forEach(slot => { + slot.clearTargeting(); + }); + + setTargeting(presetTargeting); + } +} + +function setTargeting(targetingConfig) { + window.googletag.pubads().getSlots().forEach(slot => { + targetingConfig.filter(targeting => Object.keys(targeting)[0] === slot.getAdUnitPath() || + Object.keys(targeting)[0] === slot.getSlotElementId()) + .forEach(targeting => targeting[Object.keys(targeting)[0]] + .forEach(key => { + key[Object.keys(key)[0]] + .map((value) => { + utils.logMessage(`Attempting to set key value for slot: ${slot.getSlotElementId()} key: ${Object.keys(key)[0]} value: ${value}`); + return value; + }) + .forEach(value => { + slot.setTargeting(Object.keys(key)[0], value); + }); + })); + }); +} + +function isNotSetByPb(key) { + return pbTargetingKeys.indexOf(key) === -1; +} + +function getPresetTargeting() { + if (isGptPubadsDefined()) { + presetTargeting = (function getPresetTargeting() { + return window.googletag.pubads().getSlots().map(slot => { + return { + [slot.getAdUnitPath()]: slot.getTargetingKeys().filter(isNotSetByPb).map(key => { + return { [key]: slot.getTargeting(key) }; + }) + }; + }); + })(); + } +} + +function getWinningBidTargeting() { + let winners = pbjs._bidsReceived.map(bid => bid.adUnitCode) + .filter(uniques) + .map(adUnitCode => pbjs._bidsReceived + .filter(bid => bid.adUnitCode === adUnitCode ? bid : null) + .reduce(getHighestCpm, + { + adUnitCode: adUnitCode, + cpm: 0, + adserverTargeting: {}, + timeToRespond: 0 + })); + + // winning bids with deals need an hb_deal targeting key + winners + .filter(bid => bid.dealId) + .map(bid => bid.adserverTargeting.hb_deal = bid.dealId); + + winners = winners.map(winner => { + return { + [winner.adUnitCode]: Object.keys(winner.adserverTargeting, key => key) + .map(key => { + return { [key.substring(0, 20)]: [winner.adserverTargeting[key]] }; + }) + }; + }); + + return winners; +} + +function getDealTargeting() { + return pbjs._bidsReceived.filter(bid => bid.dealId).map(bid => { + const dealKey = `hb_deal_${bid.bidderCode}`; + return { + [bid.adUnitCode]: CONSTANTS.TARGETING_KEYS.map(key => { + return { + [`${key}_${bid.bidderCode}`.substring(0, 20)]: [bid.adserverTargeting[key]] + }; + }) + .concat({ [dealKey]: [bid.adserverTargeting[dealKey]] }) + }; + }); +} + +/** + * Get custom targeting keys for bids that have `alwaysUseBid=true`. + */ +function getAlwaysUseBidTargeting() { + return pbjs._bidsReceived.map(bid => { + if (bid.alwaysUseBid) { + const standardKeys = CONSTANTS.TARGETING_KEYS; + return { + [bid.adUnitCode]: Object.keys(bid.adserverTargeting, key => key).map(key => { + // Get only the non-standard keys of the losing bids, since we + // don't want to override the standard keys of the winning bid. + if (standardKeys.indexOf(key) > -1) { + return; + } + + return { [key.substring(0, 20)]: [bid.adserverTargeting[key]] }; + + }).filter(key => key) // remove empty elements + }; + } + }).filter(bid => bid); // removes empty elements in array; +} + +function getBidLandscapeTargeting() { + const standardKeys = CONSTANTS.TARGETING_KEYS; + + return pbjs._bidsReceived.map(bid => { + if (bid.adserverTargeting) { + return { + [bid.adUnitCode]: standardKeys.map(key => { + return { + [`${key}_${bid.bidderCode}`.substring(0, 20)]: [bid.adserverTargeting[key]] + }; + }) + }; + } + }).filter(bid => bid); // removes empty elements in array +} + +function getAllTargeting() { + // Get targeting for the winning bid. Add targeting for any bids that have + // `alwaysUseBid=true`. If sending all bids is enabled, add targeting for losing bids. + var targeting = getDealTargeting() + .concat(getWinningBidTargeting()) + .concat(getAlwaysUseBidTargeting()) + .concat(pbjs._sendAllBids ? getBidLandscapeTargeting() : []); + + //store a reference of the targeting keys + targeting.map(adUnitCode => { + Object.keys(adUnitCode).map(key => { + adUnitCode[key].map(targetKey => { + pbTargetingKeys = Object.keys(targetKey).concat(pbTargetingKeys); + }); + }); + }); + return targeting; +} + +////////////////////////////////// +// // +// Start Public APIs // +// // +////////////////////////////////// + +/** + * This function returns the query string targeting parameters available at this moment for a given ad unit. Note that some bidder's response may not have been received if you call this function too quickly after the requests are sent. + * @param {string} [adunitCode] adUnitCode to get the bid responses for + * @alias module:pbjs.getAdserverTargetingForAdUnitCodeStr + * @return {array} returnObj return bids array + */ +pbjs.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { + utils.logInfo('Invoking pbjs.getAdserverTargetingForAdUnitCodeStr', arguments); + + // call to retrieve bids array + if (adunitCode) { + var res = pbjs.getAdserverTargetingForAdUnitCode(adunitCode); + return utils.transformAdServerTargetingObj(res); + } else { + utils.logMessage('Need to call getAdserverTargetingForAdUnitCodeStr with adunitCode'); + } +}; + +/** +* This function returns the query string targeting parameters available at this moment for a given ad unit. Note that some bidder's response may not have been received if you call this function too quickly after the requests are sent. + * @param adUnitCode {string} adUnitCode to get the bid responses for + * @returns {object} returnObj return bids + */ +pbjs.getAdserverTargetingForAdUnitCode = function (adUnitCode) { + utils.logInfo('Invoking pbjs.getAdserverTargetingForAdUnitCode', arguments); + + return getAllTargeting().filter(targeting => getKeys(targeting)[0] === adUnitCode) + .map(targeting => { + return { + [Object.keys(targeting)[0]]: targeting[Object.keys(targeting)[0]] + .map(target => { + return { + [Object.keys(target)[0]]: target[Object.keys(target)[0]].join(', ') + }; + }).reduce((p, c) => Object.assign(c, p), {}) + }; + }) + .reduce(function (accumulator, targeting) { + var key = Object.keys(targeting)[0]; + accumulator[key] = Object.assign({}, accumulator[key], targeting[key]); + return accumulator; + }, {})[adUnitCode]; +}; + +/** + * returns all ad server targeting for all ad units + * @return {object} Map of adUnitCodes and targeting values [] + * @alias module:pbjs.getAdserverTargeting + */ + +pbjs.getAdserverTargeting = function () { + utils.logInfo('Invoking pbjs.getAdserverTargeting', arguments); + return getAllTargeting() + .map(targeting => { + return { + [Object.keys(targeting)[0]]: targeting[Object.keys(targeting)[0]] + .map(target => { + return { + [Object.keys(target)[0]]: target[Object.keys(target)[0]].join(', ') + }; + }).reduce((p, c) => Object.assign(c, p), {}) + }; + }) + .reduce(function (accumulator, targeting) { + var key = Object.keys(targeting)[0]; + accumulator[key] = Object.assign({}, accumulator[key], targeting[key]); + return accumulator; + }, {}); +}; + +/** + * This function returns the bid responses at the given moment. + * @alias module:pbjs.getBidResponses + * @return {object} map | object that contains the bidResponses + */ + +pbjs.getBidResponses = function () { + utils.logInfo('Invoking pbjs.getBidResponses', arguments); + + return pbjs._bidsReceived.map(bid => bid.adUnitCode) + .filter(uniques).map(adUnitCode => pbjs._bidsReceived + .filter(bid => bid.adUnitCode === adUnitCode)) + .map(bids => { + return { + [bids[0].adUnitCode]: { bids: bids } + }; + }) + .reduce((a, b) => Object.assign(a, b), {}); +}; + +/** + * Returns bidResponses for the specified adUnitCode + * @param {String} adUnitCode adUnitCode + * @alias module:pbjs.getBidResponsesForAdUnitCode + * @return {Object} bidResponse object + */ + +pbjs.getBidResponsesForAdUnitCode = function (adUnitCode) { + const bids = pbjs._bidsReceived.filter(bid => bid.adUnitCode === adUnitCode); + return { + bids: bids + }; +}; + +/** + * Set query string targeting on all GPT ad units. + * @alias module:pbjs.setTargetingForGPTAsync + */ +pbjs.setTargetingForGPTAsync = function () { + utils.logInfo('Invoking pbjs.setTargetingForGPTAsync', arguments); + if (!isGptPubadsDefined()) { + utils.logError('window.googletag is not defined on the page'); + return; + } + + //first reset any old targeting + getPresetTargeting(); + resetPresetTargeting(); + //now set new targeting keys + setTargeting(getAllTargeting()); +}; + +/** + * Returns a bool if all the bids have returned or timed out + * @alias module:pbjs.allBidsAvailable + * @return {bool} all bids available + */ +pbjs.allBidsAvailable = function () { + utils.logInfo('Invoking pbjs.allBidsAvailable', arguments); + return bidmanager.bidsBackAll(); +}; + +/** + * This function will render the ad (based on params) in the given iframe document passed through. Note that doc SHOULD NOT be the parent document page as we can't doc.write() asynchrounsly + * @param {object} doc document + * @param {string} id bid id to locate the ad + * @alias module:pbjs.renderAd + */ +pbjs.renderAd = function (doc, id) { + utils.logInfo('Invoking pbjs.renderAd', arguments); + utils.logMessage('Calling renderAd with adId :' + id); + if (doc && id) { + try { + //lookup ad by ad Id + var adObject = pbjs._bidsReceived.find(bid => bid.adId === id); + if (adObject) { + //emit 'bid won' event here + events.emit(BID_WON, adObject); + var height = adObject.height; + var width = adObject.width; + var url = adObject.adUrl; + var ad = adObject.ad; + + if (ad) { + doc.write(ad); + doc.close(); + if (doc.defaultView && doc.defaultView.frameElement) { + doc.defaultView.frameElement.width = width; + doc.defaultView.frameElement.height = height; + } + } + + //doc.body.style.width = width; + //doc.body.style.height = height; + else if (url) { + doc.write(''); + doc.close(); + + if (doc.defaultView && doc.defaultView.frameElement) { + doc.defaultView.frameElement.width = width; + doc.defaultView.frameElement.height = height; + } + + } else { + utils.logError('Error trying to write ad. No ad for bid response id: ' + id); + } + + } else { + utils.logError('Error trying to write ad. Cannot find ad by given id : ' + id); + } + + } catch (e) { + utils.logError('Error trying to write ad Id :' + id + ' to the page:' + e.message); + } + } else { + utils.logError('Error trying to write ad Id :' + id + ' to the page. Missing document or adId'); + } + +}; + +/** + * Remove adUnit from the pbjs configuration + * @param {String} adUnitCode the adUnitCode to remove + * @alias module:pbjs.removeAdUnit + */ +pbjs.removeAdUnit = function (adUnitCode) { + utils.logInfo('Invoking pbjs.removeAdUnit', arguments); + if (adUnitCode) { + for (var i = 0; i < pbjs.adUnits.length; i++) { + if (pbjs.adUnits[i].code === adUnitCode) { + pbjs.adUnits.splice(i, 1); + } + } + } +}; + +pbjs.clearAuction = function() { + auctionRunning = false; + utils.logMessage('Prebid auction cleared'); +}; + +/** + * + * @param bidsBackHandler + * @param timeout + * @param adUnits + * @param adUnitCodes + */ +pbjs.requestBids = function ({ bidsBackHandler, timeout, adUnits, adUnitCodes }) { + if (auctionRunning) { + utils.logError('Prebid Error: `pbjs.requestBids` was called while a previous auction was' + + ' still running. Resubmit this request.'); + return; + } else { + auctionRunning = true; + pbjs._bidsRequested = []; + pbjs._bidsReceived = []; + } + + const cbTimeout = timeout || pbjs.bidderTimeout; + + // use adUnits provided or from pbjs global + adUnits = adUnits || pbjs.adUnits; + + // if specific adUnitCodes filter adUnits for those codes + if (adUnitCodes && adUnitCodes.length) { + adUnits = adUnits.filter(adUnit => adUnitCodes.includes(adUnit.code)); + } + + if (typeof bidsBackHandler === objectType_function) { + bidmanager.addOneTimeCallback(bidsBackHandler); + } + + utils.logInfo('Invoking pbjs.requestBids', arguments); + + if (!adUnits || adUnits.length === 0) { + utils.logMessage('No adUnits configured. No bids requested.'); + return; + } + + //set timeout for all bids + setTimeout(bidmanager.executeCallback, cbTimeout); + + adaptermanager.callBids({ adUnits, adUnitCodes }); +}; + +/** + * + * Add adunit(s) + * @param {Array|String} adUnitArr Array of adUnits or single adUnit Object. + * @alias module:pbjs.addAdUnits + */ +pbjs.addAdUnits = function (adUnitArr) { + utils.logInfo('Invoking pbjs.addAdUnits', arguments); + if (utils.isArray(adUnitArr)) { + //append array to existing + pbjs.adUnits.push.apply(pbjs.adUnits, adUnitArr); + } else if (typeof adUnitArr === objectType_object) { + pbjs.adUnits.push(adUnitArr); + } +}; + +/** + * @param {String} event the name of the event + * @param {Function} handler a callback to set on event + * @param {String} id an identifier in the context of the event + * + * This API call allows you to register a callback to handle a Prebid.js event. + * An optional `id` parameter provides more finely-grained event callback registration. + * This makes it possible to register callback events for a specific item in the + * event context. For example, `bidWon` events will accept an `id` for ad unit code. + * `bidWon` callbacks registered with an ad unit code id will be called when a bid + * for that ad unit code wins the auction. Without an `id` this method registers the + * callback for every `bidWon` event. + * + * Currently `bidWon` is the only event that accepts an `id` parameter. + */ +pbjs.onEvent = function (event, handler, id) { + utils.logInfo('Invoking pbjs.onEvent', arguments); + if (!utils.isFn(handler)) { + utils.logError('The event handler provided is not a function and was not set on event "' + event + '".'); + return; + } + + if (id && !eventValidators[event].call(null, id)) { + utils.logError('The id provided is not valid for event "' + event + '" and no handler was set.'); + return; + } + + events.on(event, handler, id); +}; + +/** + * @param {String} event the name of the event + * @param {Function} handler a callback to remove from the event + * @param {String} id an identifier in the context of the event (see `pbjs.onEvent`) + */ +pbjs.offEvent = function (event, handler, id) { + utils.logInfo('Invoking pbjs.offEvent', arguments); + if (id && !eventValidators[event].call(null, id)) { + return; + } + + events.off(event, handler, id); +}; + +/** + * Add a callback event + * @param {String} eventStr event to attach callback to Options: "allRequestedBidsBack" | "adUnitBidsBack" + * @param {Function} func function to execute. Paramaters passed into the function: (bidResObj), [adUnitCode]); + * @alias module:pbjs.addCallback + * @returns {String} id for callback + */ +pbjs.addCallback = function (eventStr, func) { + utils.logInfo('Invoking pbjs.addCallback', arguments); + var id = null; + if (!eventStr || !func || typeof func !== objectType_function) { + utils.logError('error registering callback. Check method signature'); + return id; + } + + id = utils.getUniqueIdentifierStr; + bidmanager.addCallback(id, func, eventStr); + return id; +}; + +/** + * Remove a callback event + * //@param {string} cbId id of the callback to remove + * @alias module:pbjs.removeCallback + * @returns {String} id for callback + */ +pbjs.removeCallback = function (/* cbId */) { + //todo + return null; +}; + +/** + * Wrapper to register bidderAdapter externally (adaptermanager.registerBidAdapter()) + * @param {[type]} bidderAdaptor [description] + * @param {[type]} bidderCode [description] + * @return {[type]} [description] + */ +pbjs.registerBidAdapter = function (bidderAdaptor, bidderCode) { + utils.logInfo('Invoking pbjs.registerBidAdapter', arguments); + try { + adaptermanager.registerBidAdapter(bidderAdaptor(), bidderCode); + } + catch (e) { + utils.logError('Error registering bidder adapter : ' + e.message); + } +}; + +pbjs.bidsAvailableForAdapter = function (bidderCode) { + utils.logInfo('Invoking pbjs.bidsAvailableForAdapter', arguments); + + pbjs._bidsRequested.find(bidderRequest => bidderRequest.bidderCode === bidderCode).bids + .map(bid => { + return Object.assign(bid, bidfactory.createBid(1), { + bidderCode, + adUnitCode: bid.placementCode + }); + }) + .map(bid => pbjs._bidsReceived.push(bid)); +}; + +/** + * Wrapper to bidfactory.createBid() + * @param {[type]} statusCode [description] + * @return {[type]} [description] + */ +pbjs.createBid = function (statusCode) { + utils.logInfo('Invoking pbjs.createBid', arguments); + return bidfactory.createBid(statusCode); +}; + +/** + * Wrapper to bidmanager.addBidResponse + * @param {[type]} adUnitCode [description] + * @param {[type]} bid [description] + */ +pbjs.addBidResponse = function (adUnitCode, bid) { + utils.logInfo('Invoking pbjs.addBidResponse', arguments); + bidmanager.addBidResponse(adUnitCode, bid); +}; + +/** + * Wrapper to adloader.loadScript + * @param {[type]} tagSrc [description] + * @param {Function} callback [description] + * @return {[type]} [description] + */ +pbjs.loadScript = function (tagSrc, callback, useCache) { + utils.logInfo('Invoking pbjs.loadScript', arguments); + adloader.loadScript(tagSrc, callback, useCache); +}; + +/** + * Will enable sendinga prebid.js to data provider specified + * @param {object} options object {provider : 'string', options : {}} + */ +pbjs.enableAnalytics = function (options) { + utils.logInfo('Invoking pbjs.enableAnalytics', arguments); + if (!options) { + utils.logError('pbjs.enableAnalytics should be called with option {}', 'prebid.js'); + return; + } + + if (options.provider === 'ga') { + try { + ga.enableAnalytics(typeof options.options === 'undefined' ? {} : options.options); + } + catch (e) { + utils.logError('Error calling GA: ', 'prebid.js', e); + } + } else if (options.provider === 'other_provider') { + //todo + return null; + } +}; + +/** + * This will tell analytics that all bids received after are "timed out" + */ +pbjs.sendTimeoutEvent = function () { + utils.logInfo('Invoking pbjs.sendTimeoutEvent', arguments); + timeOutBidders(); +}; + +pbjs.aliasBidder = function (bidderCode, alias) { + utils.logInfo('Invoking pbjs.aliasBidder', arguments); + if (bidderCode && alias) { + adaptermanager.aliasBidAdapter(bidderCode, alias); + } else { + utils.logError('bidderCode and alias must be passed as arguments', 'pbjs.aliasBidder'); + } +}; + +pbjs.setPriceGranularity = function (granularity) { + utils.logInfo('Invoking pbjs.setPriceGranularity', arguments); + if (!granularity) { + utils.logError('Prebid Error: no value passed to `setPriceGranularity()`'); + } else { + bidmanager.setPriceGranularity(granularity); + } +}; + +pbjs.enableSendAllBids = function () { + pbjs._sendAllBids = true; +}; + +processQue(); diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 9ffa5f97aeb..2f0c89f4f1c 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1,728 +1,728 @@ -import { - getAdServerTargeting, - getBidRequests, - getBidResponses, - getTargetingKeys, - getTargetingKeysBidLandscape, -} from 'test/fixtures/fixtures'; - -var assert = require('chai').assert; - -var prebid = require('src/prebid'); -var utils = require('src/utils'); -var bidmanager = require('src/bidmanager'); -var adloader = require('src/adloader'); -var adaptermanager = require('src/adaptermanager'); -var events = require('src/events'); -var ga = require('src/ga'); -var CONSTANTS = require('src/constants.json'); - -var bidResponses = require('test/fixtures/bid-responses.json'); -var targetingMap = require('test/fixtures/targeting-map.json'); -var config = require('test/fixtures/config.json'); - -pbjs = pbjs || {}; -pbjs._bidsRequested = getBidRequests(); -pbjs._bidsReceived = getBidResponses(); - -function resetAuction() { - pbjs._sendAllBids = false; - pbjs.clearAuction(); - pbjs._bidsRequested = getBidRequests(); - pbjs._bidsReceived = getBidResponses(); -} - -var Slot = function Slot(elementId, pathId) { - var slot = { - getSlotElementId: function getSlotElementId() { - return elementId; - }, - - getAdUnitPath: function getAdUnitPath() { - return pathId; - }, - - setTargeting: function setTargeting(key, value) { - }, - - getTargeting: function getTargeting() { - return [{ testKey: ['a test targeting value'] }]; - }, - - getTargetingKeys: function getTargetingKeys() { - return ['testKey']; - }, - - clearTargeting: function clearTargeting() { - return googletag.pubads().getSlots(); - } - }; - slot.spySetTargeting = sinon.spy(slot, 'setTargeting'); - return slot; -}; - -var createSlotArray = function createSlotArray() { - return [ - new Slot(config.adUnitElementIDs[0], config.adUnitCodes[0]), - new Slot(config.adUnitElementIDs[1], config.adUnitCodes[1]), - new Slot(config.adUnitElementIDs[2], config.adUnitCodes[2]) - ]; -}; - -window.googletag = { - _slots: [], - pubads: function () { - var self = this; - return { - getSlots: function () { - return self._slots; - }, - - setSlots: function (slots) { - self._slots = slots; - } - }; - } -}; - -describe('Unit: Prebid Module', function () { - - describe('getAdserverTargetingForAdUnitCodeStr', function () { - it('should return targeting info as a string', function () { - const adUnitCode = config.adUnitCodes[0]; - pbjs.enableSendAllBids(); - var expected = 'foobar=300x250&hb_size=300x250&hb_pb=10.00&hb_adid=233bcbee889d46d&hb_bidder=appnexus&hb_size_triplelift=0x0&hb_pb_triplelift=10.00&hb_adid_triplelift=222bb26f9e8bd&hb_bidder_triplelift=triplelift&hb_size_appnexus=300x250&hb_pb_appnexus=10.00&hb_adid_appnexus=233bcbee889d46d&hb_bidder_appnexus=appnexus&hb_size_pagescience=300x250&hb_pb_pagescience=10.00&hb_adid_pagescience=25bedd4813632d7&hb_bidder_pagescienc=pagescience&hb_size_brightcom=300x250&hb_pb_brightcom=10.00&hb_adid_brightcom=26e0795ab963896&hb_bidder_brightcom=brightcom&hb_size_brealtime=300x250&hb_pb_brealtime=10.00&hb_adid_brealtime=275bd666f5a5a5d&hb_bidder_brealtime=brealtime&hb_size_pubmatic=300x250&hb_pb_pubmatic=10.00&hb_adid_pubmatic=28f4039c636b6a7&hb_bidder_pubmatic=pubmatic&hb_size_rubicon=300x600&hb_pb_rubicon=10.00&hb_adid_rubicon=29019e2ab586a5a&hb_bidder_rubicon=rubicon'; - var result = pbjs.getAdserverTargetingForAdUnitCodeStr(adUnitCode); - assert.equal(expected, result, 'returns expected string of ad targeting info'); - }); - - it('should log message if adunitCode param is falsey', function () { - var spyLogMessage = sinon.spy(utils, 'logMessage'); - var result = pbjs.getAdserverTargetingForAdUnitCodeStr(); - assert.ok(spyLogMessage.calledWith('Need to call getAdserverTargetingForAdUnitCodeStr with adunitCode'), 'expected message was logged'); - assert.equal(result, undefined, 'result is undefined'); - utils.logMessage.restore(); - }); - }); - - describe('getAdserverTargetingForAdUnitCode', function () { - it('should return targeting info as an object', function () { - const adUnitCode = config.adUnitCodes[0]; - pbjs.enableSendAllBids(); - var result = pbjs.getAdserverTargetingForAdUnitCode(adUnitCode); - const expected = getAdServerTargeting()[adUnitCode]; - assert.deepEqual(result, expected, 'returns expected' + - ' targeting info object'); - }); - }); - - describe('getAdServerTargeting', function () { - - beforeEach(() => { - resetAuction(); - }); - - afterEach(() => { - resetAuction(); - }); - - it('should return current targeting data for slots', function () { - pbjs.enableSendAllBids(); - const targeting = pbjs.getAdserverTargeting(); - const expected = getAdServerTargeting(); - assert.deepEqual(targeting, expected, 'targeting ok'); - }); - - it('should return correct targeting with default settings', () => { - var targeting = pbjs.getAdserverTargeting(); - var expected = { - "/19968336/header-bid-tag-0": { - "foobar": "300x250", - "hb_size": "300x250", - "hb_pb": "10.00", - "hb_adid": "233bcbee889d46d", - "hb_bidder": "appnexus" - }, - "/19968336/header-bid-tag1": { - "foobar": "728x90", - "hb_size": "728x90", - "hb_pb": "10.00", - "hb_adid": "24bd938435ec3fc", - "hb_bidder": "appnexus" - } - }; - assert.deepEqual(targeting, expected); - }); - - it('should return correct targeting with bid landscape targeting on', () => { - pbjs.enableSendAllBids(); - var targeting = pbjs.getAdserverTargeting(); - var expected = getAdServerTargeting(); - assert.deepEqual(targeting, expected); - }); - - it("should include a losing bid's custom ad targeting key when the bid has `alwaysUseBid` set to `true`", () => { - - // Let's make sure we're getting the expected losing bid. - assert.equal(pbjs._bidsReceived[0]['bidderCode'], 'triplelift'); - assert.equal(pbjs._bidsReceived[0]['cpm'], 0.112256); - - // Modify the losing bid to have `alwaysUseBid=true` and a custom `adserverTargeting` key. - pbjs._bidsReceived[0]['alwaysUseBid'] = true; - pbjs._bidsReceived[0]['adserverTargeting'] = { - 'always_use_me': 'abc', - }; - - var targeting = pbjs.getAdserverTargeting(); - - // Ensure targeting for both ad placements includes the custom key. - assert.equal( - targeting['/19968336/header-bid-tag-0'].hasOwnProperty('always_use_me'), - true - ); - - var expected = { - "/19968336/header-bid-tag-0": { - "foobar": "300x250", - "hb_size": "300x250", - "hb_pb": "10.00", - "hb_adid": "233bcbee889d46d", - "hb_bidder": "appnexus", - "always_use_me": "abc" - }, - "/19968336/header-bid-tag1": { - "foobar": "728x90", - "hb_size": "728x90", - "hb_pb": "10.00", - "hb_adid": "24bd938435ec3fc", - "hb_bidder": "appnexus" - } - }; - - assert.deepEqual(targeting, expected); - }); - }); - - describe('getBidResponses', function () { - it('should return expected bid responses when not passed an adunitCode', function () { - var result = pbjs.getBidResponses(); - var compare = getBidResponses().map(bid => bid.adUnitCode) - .filter((v, i, a) => a.indexOf(v) === i).map(adUnitCode => pbjs._bidsReceived - .filter(bid => bid.adUnitCode === adUnitCode)) - .map(bids => { - return { - [bids[0].adUnitCode]: { bids: bids } - }; - }) - .reduce((a, b) => Object.assign(a, b), {}); - - assert.deepEqual(result, compare, 'expected bid responses are returned'); - }); - }); - - describe('getBidResponsesForAdUnitCode', function () { - it('should return bid responses as expected', function () { - const adUnitCode = '/19968336/header-bid-tag-0'; - const result = pbjs.getBidResponsesForAdUnitCode(adUnitCode); - const bids = getBidResponses().filter(bid => bid.adUnitCode === adUnitCode); - const compare = { bids: bids}; - assert.deepEqual(result, compare, 'expected id responses for ad unit code are returned'); - }); - }); - - describe('setTargetingForGPTAsync', function () { - let logErrorSpy; - - beforeEach(() => { - logErrorSpy = sinon.spy(utils, 'logError'); - resetAuction(); - }); - - afterEach(() => { - utils.logError.restore(); - resetAuction(); - }); - - it('should set targeting when passed an array of ad unit codes', function () { - var slots = createSlotArray(); - window.googletag.pubads().setSlots(slots); - - pbjs.setTargetingForGPTAsync(config.adUnitCodes); - assert.deepEqual(slots[0].spySetTargeting.args[0], ['hb_bidder', 'appnexus'], 'slot.setTargeting was called with expected key/values'); - }); - - it('should set targeting from googletag data', function () { - var slots = createSlotArray(); - window.googletag.pubads().setSlots(slots); - - pbjs.setTargetingForGPTAsync(); - - var expected = getTargetingKeys(); - assert.deepEqual(slots[0].spySetTargeting.args, expected); - }); - - it('Calling enableSendAllBids should set targeting to include standard keys with bidder' + - ' append to key name', function () { - var slots = createSlotArray(); - window.googletag.pubads().setSlots(slots); - - pbjs.enableSendAllBids(); - pbjs.setTargetingForGPTAsync(); - - var expected = getTargetingKeysBidLandscape(); - assert.deepEqual(slots[0].spySetTargeting.args, expected); - }); - - it('should set targeting for bids with `alwaysUseBid=true`', function () { - - // Make sure we're getting the expected losing bid. - assert.equal(pbjs._bidsReceived[0]['bidderCode'], 'triplelift'); - assert.equal(pbjs._bidsReceived[0]['cpm'], 0.112256); - - // Modify the losing bid to have `alwaysUseBid=true` and a custom `adserverTargeting` key. - pbjs._bidsReceived[0]['alwaysUseBid'] = true; - pbjs._bidsReceived[0]['adserverTargeting'] = { - 'always_use_me': 'abc', - }; - - var slots = createSlotArray(); - window.googletag.pubads().setSlots(slots); - - pbjs.setTargetingForGPTAsync(config.adUnitCodes); - - var expected = [ - [ - "hb_bidder", - "appnexus" - ], - [ - "hb_adid", - "233bcbee889d46d" - ], - [ - "hb_pb", - "10.00" - ], - [ - "hb_size", - "300x250" - ], - [ - "foobar", - "300x250" - ], - [ - "always_use_me", - "abc" - ], - [ - "foobar", - "300x250" - ] - ]; - - assert.deepEqual(slots[0].spySetTargeting.args, expected); - }); - - it('should log error when googletag is not defined on page', function () { - const error = 'window.googletag is not defined on the page'; - const windowGoogletagBackup = window.googletag; - window.googletag = {}; - - pbjs.setTargetingForGPTAsync(); - assert.ok(logErrorSpy.calledWith(error), 'expected error was logged'); - window.googletag = windowGoogletagBackup; - }); - }); - - describe('allBidsAvailable', function () { - it('should call bidmanager.allBidsBack', function () { - var spyAllBidsBack = sinon.spy(bidmanager, 'bidsBackAll'); - - pbjs.allBidsAvailable(); - assert.ok(spyAllBidsBack.called, 'called bidmanager.allBidsBack'); - bidmanager.bidsBackAll.restore(); - }); - }); - - describe('renderAd', function () { - var bidId = 1; - var doc = {}; - var adResponse = {}; - var spyLogError = null; - var spyLogMessage = null; - - beforeEach(function () { - doc = { - write: sinon.spy(), - close: sinon.spy(), - defaultView: { - frameElement: { - width: 0, - height: 0 - } - } - }; - - adResponse = { - "adId": bidId, - "width": 300, - "height": 250, - }; - pbjs._bidsReceived.push(adResponse); - - spyLogError = sinon.spy(utils, 'logError'); - spyLogMessage = sinon.spy(utils, 'logMessage'); - }); - - afterEach(function () { - pbjs._bidsReceived.splice(pbjs._bidsReceived.indexOf(adResponse), 1); - utils.logError.restore(); - utils.logMessage.restore(); - }); - - it('should require doc and id params', function () { - pbjs.renderAd(); - var error = 'Error trying to write ad Id :undefined to the page. Missing document or adId'; - assert.ok(spyLogError.calledWith(error), 'expected param error was logged'); - }); - - it('should log message with bid id', function () { - pbjs.renderAd(doc, bidId); - var message = 'Calling renderAd with adId :' + bidId; - assert.ok(spyLogMessage.calledWith(message), 'expected message was logged'); - }); - - it('should write the ad to the doc', function () { - adResponse.ad = ""; - pbjs.renderAd(doc, bidId); - assert.ok(doc.write.calledWith(adResponse.ad), 'ad was written to doc'); - assert.ok(doc.close.called, 'close method called'); - }); - - it('should place the url inside an iframe on the doc', function () { - adResponse.adUrl = "http://server.example.com/ad/ad.js"; - pbjs.renderAd(doc, bidId); - var iframe = '' - assert.ok(doc.write.calledWith(iframe), 'url was written to iframe in doc'); - }); - - it('should log an error when no ad or url', function () { - pbjs.renderAd(doc, bidId); - var error = 'Error trying to write ad. No ad for bid response id: ' + bidId; - assert.ok(spyLogError.calledWith(error), 'expected error was logged'); - }); - - it('should catch errors thrown when trying to write ads to the page', function () { - adResponse.ad = ""; - - var error = { message: 'doc write error' }; - doc.write = sinon.stub().throws(error); - pbjs.renderAd(doc, bidId); - - var errorMessage = 'Error trying to write ad Id :' + bidId + ' to the page:' + error.message; - assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged'); - }); - - it('should log an error when ad not found', function () { - var fakeId = 99; - pbjs.renderAd(doc, fakeId); - var error = 'Error trying to write ad. Cannot find ad by given id : ' + fakeId; - assert.ok(spyLogError.calledWith(error), 'expected error was logged'); - }); - }); - - describe('requestBids', () => { - it('should add bidsBackHandler callback to bidmanager', () => { - var spyAddOneTimeCallBack = sinon.spy(bidmanager, 'addOneTimeCallback'); - var requestObj = { - bidsBackHandler: function bidsBackHandlerCallback() {} - }; - pbjs.requestBids(requestObj); - assert.ok(spyAddOneTimeCallBack.calledWith(requestObj.bidsBackHandler), - 'called bidmanager.addOneTimeCallback'); - bidmanager.addOneTimeCallback.restore(); - resetAuction(); - }); - - it('should log message when adUnits not configured', () => { - const logMessageSpy = sinon.spy(utils, 'logMessage'); - const adUnitsBackup = pbjs.adUnits; - - pbjs.adUnits = []; - pbjs.requestBids({}); - - assert.ok(logMessageSpy.calledWith('No adUnits configured. No bids requested.'), 'expected message was logged'); - utils.logMessage.restore(); - pbjs.adUnits = adUnitsBackup; - resetAuction(); - }); - - it('should execute callback after timeout', () => { - var spyExecuteCallback = sinon.spy(bidmanager, 'executeCallback'); - var clock = sinon.useFakeTimers(); - var requestObj = { - bidsBackHandler: function bidsBackHandlerCallback() {}, - timeout: 2000 - }; - - pbjs.requestBids(requestObj); - - clock.tick(requestObj.timeout - 1); - assert.ok(spyExecuteCallback.notCalled, 'bidmanager.executeCallback not called'); - - clock.tick(1); - assert.ok(spyExecuteCallback.called, 'called bidmanager.executeCallback'); - - bidmanager.executeCallback.restore(); - clock.restore(); - resetAuction(); - }); - - it('should call callBids function on adaptermanager', () => { - var spyCallBids = sinon.spy(adaptermanager, 'callBids'); - pbjs.requestBids({}); - assert.ok(spyCallBids.called, 'called adaptermanager.callBids'); - adaptermanager.callBids.restore(); - resetAuction(); - }); - }); - - describe('onEvent', () => { - it('should log an error when handler is not a function', () => { - var spyLogError = sinon.spy(utils, 'logError'); - var event = 'testEvent'; - pbjs.onEvent(event); - assert.ok(spyLogError.calledWith('The event handler provided is not a function and was not set on event "' + event + '".'), - 'expected error was logged'); - utils.logError.restore(); - }); - - it('should log an error when id provided is not valid for event', () => { - var spyLogError = sinon.spy(utils, 'logError'); - var event = 'bidWon'; - pbjs.onEvent(event, Function, 'testId'); - assert.ok(spyLogError.calledWith('The id provided is not valid for event "' + event + '" and no handler was set.'), - 'expected error was logged'); - utils.logError.restore(); - }); - - it('should call events.on with valid parameters', () => { - var spyEventsOn = sinon.spy(events, 'on'); - pbjs.onEvent('bidWon', Function); - assert.ok(spyEventsOn.calledWith('bidWon', Function)); - events.on.restore(); - }); - }); - - describe('offEvent', () => { - it('should return when id provided is not valid for event', () => { - var spyEventsOff = sinon.spy(events, 'off'); - pbjs.offEvent('bidWon', Function, 'testId'); - assert.ok(spyEventsOff.notCalled); - events.off.restore(); - }); - - it('should call events.off with valid parameters', () => { - var spyEventsOff = sinon.spy(events, 'off'); - pbjs.offEvent('bidWon', Function); - assert.ok(spyEventsOff.calledWith('bidWon', Function)); - events.off.restore(); - }); - }); - - describe('addCallback', () => { - it('should log error and return null id when error registering callback', () => { - var spyLogError = sinon.spy(utils, 'logError'); - var id = pbjs.addCallback('event', 'fakeFunction'); - assert.equal(id, null, 'id returned was null'); - assert.ok(spyLogError.calledWith('error registering callback. Check method signature'), - 'expected error was logged'); - utils.logError.restore(); - }); - - it('should add callback to bidmanager', () => { - var spyAddCallback = sinon.spy(bidmanager, 'addCallback'); - var id = pbjs.addCallback('event', Function); - assert.ok(spyAddCallback.calledWith(id, Function, 'event'), 'called bidmanager.addCallback'); - bidmanager.addCallback.restore(); - }); - }); - - describe('removeCallback', () => { - it('should return null', () => { - const id = pbjs.removeCallback(); - assert.equal(id, null); - }); - }); - - describe('registerBidAdapter', () => { - it('should register bidAdaptor with adaptermanager', () => { - var registerBidAdapterSpy = sinon.spy(adaptermanager, 'registerBidAdapter'); - pbjs.registerBidAdapter(Function, 'biddercode'); - assert.ok(registerBidAdapterSpy.called, 'called adaptermanager.registerBidAdapter'); - adaptermanager.registerBidAdapter.restore(); - }); - - it('should catch thrown errors', () => { - var spyLogError = sinon.spy(utils, 'logError'); - var errorObject = {message: 'bidderAdaptor error'}; - var bidderAdaptor = sinon.stub().throws(errorObject); - - pbjs.registerBidAdapter(bidderAdaptor, 'biddercode'); - - var errorMessage = 'Error registering bidder adapter : ' + errorObject.message; - assert.ok(spyLogError.calledWith(errorMessage), 'expected error was caught'); - utils.logError.restore(); - }); - }); - - describe('bidsAvailableForAdapter', () => { - it('should update requested bid with status set to available', () => { - const bidderCode = 'appnexus'; - pbjs.bidsAvailableForAdapter(bidderCode); - - const requestedBids = pbjs._bidsRequested.find(bid => bid.bidderCode === bidderCode); - requestedBids.bids.forEach(bid => { - assert.equal(bid.bidderCode, bidderCode, 'bidderCode was set'); - assert.equal(bid.statusMessage, 'Bid available', 'bid set as available'); - }); - }); - }); - - describe('createBid', () => { - it('should return a bid object', () => { - const statusCode = 1; - const bid = pbjs.createBid(statusCode); - assert.isObject(bid, 'bid is an object'); - assert.equal(bid.getStatusCode(), statusCode, 'bid has correct status'); - - const defaultStatusBid = pbjs.createBid(); - assert.isObject(defaultStatusBid, 'bid is an object'); - assert.equal(defaultStatusBid.getStatusCode(), 0, 'bid has correct status'); - }); - }); - - describe('addBidResponse', () => { - it('should call bidmanager.addBidResponse', () => { - const addBidResponseStub = sinon.stub(bidmanager, 'addBidResponse'); - const adUnitCode = 'testcode'; - const bid = pbjs.createBid(0); - - pbjs.addBidResponse(adUnitCode, bid); - assert.ok(addBidResponseStub.calledWith(adUnitCode, bid), 'called bidmanager.addBidResponse'); - bidmanager.addBidResponse.restore(); - }); - }); - - describe('loadScript', () => { - it('should call adloader.loadScript', () => { - const loadScriptSpy = sinon.spy(adloader, 'loadScript'); - const tagSrc = 'testsrc'; - const callback = Function; - const useCache = false; - - pbjs.loadScript(tagSrc, callback, useCache); - assert.ok(loadScriptSpy.calledWith(tagSrc, callback, useCache), 'called adloader.loadScript'); - adloader.loadScript.restore(); - }); - }); - - describe('enableAnalytics', () => { - let logErrorSpy; - - beforeEach(() => { - logErrorSpy = sinon.spy(utils, 'logError'); - }); - - afterEach(() => { - utils.logError.restore(); - }); - - it('should log error when not passed options', () => { - const error = 'pbjs.enableAnalytics should be called with option {}'; - pbjs.enableAnalytics(); - assert.ok(logErrorSpy.calledWith(error), 'expected error was logged'); - }); - - it('should call ga.enableAnalytics with options', () => { - const enableAnalyticsSpy = sinon.spy(ga, 'enableAnalytics'); - - let options = {'provider': 'ga'}; - pbjs.enableAnalytics(options); - assert.ok(enableAnalyticsSpy.calledWith({}), 'ga.enableAnalytics called with empty options object'); - - options['options'] = 'testoptions'; - pbjs.enableAnalytics(options); - assert.ok(enableAnalyticsSpy.calledWith(options.options), 'ga.enableAnalytics called with provided options'); - - ga.enableAnalytics.restore(); - }); - - it('should catch errors thrown from ga.enableAnalytics', () => { - const error = {message: 'Error calling GA: '}; - const enableAnalyticsStub = sinon.stub(ga, 'enableAnalytics').throws(error); - const options = {'provider': 'ga'}; - - pbjs.enableAnalytics(options); - assert.ok(logErrorSpy.calledWith(error.message), 'expected error was caught'); - ga.enableAnalytics.restore(); - }); - - it('should return null for other providers', () => { - const options = {'provider': 'other_provider'}; - const returnValue = pbjs.enableAnalytics(options); - assert.equal(returnValue, null, 'expected return value'); - }); - }); - - describe('sendTimeoutEvent', () => { - it('should emit BID_TIMEOUT for timed out bids', () => { - const eventsEmitSpy = sinon.spy(events, 'emit'); - pbjs.sendTimeoutEvent(); - assert.ok(eventsEmitSpy.calledWith(CONSTANTS.EVENTS.BID_TIMEOUT), 'emitted events BID_TIMEOUT'); - events.emit.restore(); - }); - }); - - describe('aliasBidder', () => { - it('should call adaptermanager.aliasBidder', () => { - const aliasBidAdapterSpy = sinon.spy(adaptermanager, 'aliasBidAdapter'); - const bidderCode = 'testcode'; - const alias = 'testalias'; - - pbjs.aliasBidder(bidderCode, alias); - assert.ok(aliasBidAdapterSpy.calledWith(bidderCode, alias), 'called adaptermanager.aliasBidAdapterSpy'); - adaptermanager.aliasBidAdapter.restore(); - }); - - it('should log error when not passed correct arguments', () => { - const logErrorSpy = sinon.spy(utils, 'logError'); - const error = 'bidderCode and alias must be passed as arguments'; - - pbjs.aliasBidder(); - assert.ok(logErrorSpy.calledWith(error), 'expected error was logged'); - utils.logError.restore(); - }); - }); - - describe('setPriceGranularity', () => { - it('should log error when not passed granularity', () => { - const logErrorSpy = sinon.spy(utils, 'logError'); - const error = 'Prebid Error: no value passed to `setPriceGranularity()`'; - - pbjs.setPriceGranularity(); - assert.ok(logErrorSpy.calledWith(error), 'expected error was logged'); - utils.logError.restore(); - }); - - it('should call bidmanager.setPriceGranularity with granularity', () => { - const setPriceGranularitySpy = sinon.spy(bidmanager, 'setPriceGranularity'); - const granularity = 'low'; - - pbjs.setPriceGranularity(granularity); - assert.ok(setPriceGranularitySpy.called, 'called bidmanager.setPriceGranularity'); - bidmanager.setPriceGranularity.restore(); - }); - }); -}); +import { + getAdServerTargeting, + getBidRequests, + getBidResponses, + getTargetingKeys, + getTargetingKeysBidLandscape, +} from 'test/fixtures/fixtures'; + +var assert = require('chai').assert; + +var prebid = require('src/prebid'); +var utils = require('src/utils'); +var bidmanager = require('src/bidmanager'); +var adloader = require('src/adloader'); +var adaptermanager = require('src/adaptermanager'); +var events = require('src/events'); +var ga = require('src/ga'); +var CONSTANTS = require('src/constants.json'); + +var bidResponses = require('test/fixtures/bid-responses.json'); +var targetingMap = require('test/fixtures/targeting-map.json'); +var config = require('test/fixtures/config.json'); + +pbjs = pbjs || {}; +pbjs._bidsRequested = getBidRequests(); +pbjs._bidsReceived = getBidResponses(); + +function resetAuction() { + pbjs._sendAllBids = false; + pbjs.clearAuction(); + pbjs._bidsRequested = getBidRequests(); + pbjs._bidsReceived = getBidResponses(); +} + +var Slot = function Slot(elementId, pathId) { + var slot = { + getSlotElementId: function getSlotElementId() { + return elementId; + }, + + getAdUnitPath: function getAdUnitPath() { + return pathId; + }, + + setTargeting: function setTargeting(key, value) { + }, + + getTargeting: function getTargeting(key) { + return []; + }, + + getTargetingKeys: function getTargetingKeys() { + return []; + }, + + clearTargeting: function clearTargeting() { + return googletag.pubads().getSlots(); + } + }; + slot.spySetTargeting = sinon.spy(slot, 'setTargeting'); + return slot; +}; + +var createSlotArray = function createSlotArray() { + return [ + new Slot(config.adUnitElementIDs[0], config.adUnitCodes[0]), + new Slot(config.adUnitElementIDs[1], config.adUnitCodes[1]), + new Slot(config.adUnitElementIDs[2], config.adUnitCodes[2]) + ]; +}; + +window.googletag = { + _slots: [], + pubads: function () { + var self = this; + return { + getSlots: function () { + return self._slots; + }, + + setSlots: function (slots) { + self._slots = slots; + } + }; + } +}; + +describe('Unit: Prebid Module', function () { + + describe('getAdserverTargetingForAdUnitCodeStr', function () { + it('should return targeting info as a string', function () { + const adUnitCode = config.adUnitCodes[0]; + pbjs.enableSendAllBids(); + var expected = 'foobar=300x250&hb_size=300x250&hb_pb=10.00&hb_adid=233bcbee889d46d&hb_bidder=appnexus&hb_size_triplelift=0x0&hb_pb_triplelift=10.00&hb_adid_triplelift=222bb26f9e8bd&hb_bidder_triplelift=triplelift&hb_size_appnexus=300x250&hb_pb_appnexus=10.00&hb_adid_appnexus=233bcbee889d46d&hb_bidder_appnexus=appnexus&hb_size_pagescience=300x250&hb_pb_pagescience=10.00&hb_adid_pagescience=25bedd4813632d7&hb_bidder_pagescienc=pagescience&hb_size_brightcom=300x250&hb_pb_brightcom=10.00&hb_adid_brightcom=26e0795ab963896&hb_bidder_brightcom=brightcom&hb_size_brealtime=300x250&hb_pb_brealtime=10.00&hb_adid_brealtime=275bd666f5a5a5d&hb_bidder_brealtime=brealtime&hb_size_pubmatic=300x250&hb_pb_pubmatic=10.00&hb_adid_pubmatic=28f4039c636b6a7&hb_bidder_pubmatic=pubmatic&hb_size_rubicon=300x600&hb_pb_rubicon=10.00&hb_adid_rubicon=29019e2ab586a5a&hb_bidder_rubicon=rubicon'; + var result = pbjs.getAdserverTargetingForAdUnitCodeStr(adUnitCode); + assert.equal(expected, result, 'returns expected string of ad targeting info'); + }); + + it('should log message if adunitCode param is falsey', function () { + var spyLogMessage = sinon.spy(utils, 'logMessage'); + var result = pbjs.getAdserverTargetingForAdUnitCodeStr(); + assert.ok(spyLogMessage.calledWith('Need to call getAdserverTargetingForAdUnitCodeStr with adunitCode'), 'expected message was logged'); + assert.equal(result, undefined, 'result is undefined'); + utils.logMessage.restore(); + }); + }); + + describe('getAdserverTargetingForAdUnitCode', function () { + it('should return targeting info as an object', function () { + const adUnitCode = config.adUnitCodes[0]; + pbjs.enableSendAllBids(); + var result = pbjs.getAdserverTargetingForAdUnitCode(adUnitCode); + const expected = getAdServerTargeting()[adUnitCode]; + assert.deepEqual(result, expected, 'returns expected' + + ' targeting info object'); + }); + }); + + describe('getAdServerTargeting', function () { + + beforeEach(() => { + resetAuction(); + }); + + afterEach(() => { + resetAuction(); + }); + + it('should return current targeting data for slots', function () { + pbjs.enableSendAllBids(); + const targeting = pbjs.getAdserverTargeting(); + const expected = getAdServerTargeting(); + assert.deepEqual(targeting, expected, 'targeting ok'); + }); + + it('should return correct targeting with default settings', () => { + var targeting = pbjs.getAdserverTargeting(); + var expected = { + "/19968336/header-bid-tag-0": { + "foobar": "300x250", + "hb_size": "300x250", + "hb_pb": "10.00", + "hb_adid": "233bcbee889d46d", + "hb_bidder": "appnexus" + }, + "/19968336/header-bid-tag1": { + "foobar": "728x90", + "hb_size": "728x90", + "hb_pb": "10.00", + "hb_adid": "24bd938435ec3fc", + "hb_bidder": "appnexus" + } + }; + assert.deepEqual(targeting, expected); + }); + + it('should return correct targeting with bid landscape targeting on', () => { + pbjs.enableSendAllBids(); + var targeting = pbjs.getAdserverTargeting(); + var expected = getAdServerTargeting(); + assert.deepEqual(targeting, expected); + }); + + it("should include a losing bid's custom ad targeting key when the bid has `alwaysUseBid` set to `true`", () => { + + // Let's make sure we're getting the expected losing bid. + assert.equal(pbjs._bidsReceived[0]['bidderCode'], 'triplelift'); + assert.equal(pbjs._bidsReceived[0]['cpm'], 0.112256); + + // Modify the losing bid to have `alwaysUseBid=true` and a custom `adserverTargeting` key. + pbjs._bidsReceived[0]['alwaysUseBid'] = true; + pbjs._bidsReceived[0]['adserverTargeting'] = { + 'always_use_me': 'abc', + }; + + var targeting = pbjs.getAdserverTargeting(); + + // Ensure targeting for both ad placements includes the custom key. + assert.equal( + targeting['/19968336/header-bid-tag-0'].hasOwnProperty('always_use_me'), + true + ); + + var expected = { + "/19968336/header-bid-tag-0": { + "foobar": "300x250", + "hb_size": "300x250", + "hb_pb": "10.00", + "hb_adid": "233bcbee889d46d", + "hb_bidder": "appnexus", + "always_use_me": "abc" + }, + "/19968336/header-bid-tag1": { + "foobar": "728x90", + "hb_size": "728x90", + "hb_pb": "10.00", + "hb_adid": "24bd938435ec3fc", + "hb_bidder": "appnexus" + } + }; + + assert.deepEqual(targeting, expected); + }); + }); + + describe('getBidResponses', function () { + it('should return expected bid responses when not passed an adunitCode', function () { + var result = pbjs.getBidResponses(); + var compare = getBidResponses().map(bid => bid.adUnitCode) + .filter((v, i, a) => a.indexOf(v) === i).map(adUnitCode => pbjs._bidsReceived + .filter(bid => bid.adUnitCode === adUnitCode)) + .map(bids => { + return { + [bids[0].adUnitCode]: { bids: bids } + }; + }) + .reduce((a, b) => Object.assign(a, b), {}); + + assert.deepEqual(result, compare, 'expected bid responses are returned'); + }); + }); + + describe('getBidResponsesForAdUnitCode', function () { + it('should return bid responses as expected', function () { + const adUnitCode = '/19968336/header-bid-tag-0'; + const result = pbjs.getBidResponsesForAdUnitCode(adUnitCode); + const bids = getBidResponses().filter(bid => bid.adUnitCode === adUnitCode); + const compare = { bids: bids}; + assert.deepEqual(result, compare, 'expected id responses for ad unit code are returned'); + }); + }); + + describe('setTargetingForGPTAsync', function () { + let logErrorSpy; + + beforeEach(() => { + logErrorSpy = sinon.spy(utils, 'logError'); + resetAuction(); + }); + + afterEach(() => { + utils.logError.restore(); + resetAuction(); + }); + + it('should set targeting when passed an array of ad unit codes', function () { + var slots = createSlotArray(); + window.googletag.pubads().setSlots(slots); + + pbjs.setTargetingForGPTAsync(config.adUnitCodes); + assert.deepEqual(slots[0].spySetTargeting.args[0], ['hb_bidder', 'appnexus'], 'slot.setTargeting was called with expected key/values'); + }); + + it('should set targeting from googletag data', function () { + var slots = createSlotArray(); + window.googletag.pubads().setSlots(slots); + + pbjs.setTargetingForGPTAsync(); + + var expected = getTargetingKeys(); + assert.deepEqual(slots[0].spySetTargeting.args, expected); + }); + + it('Calling enableSendAllBids should set targeting to include standard keys with bidder' + + ' append to key name', function () { + var slots = createSlotArray(); + window.googletag.pubads().setSlots(slots); + + pbjs.enableSendAllBids(); + pbjs.setTargetingForGPTAsync(); + + var expected = getTargetingKeysBidLandscape(); + assert.deepEqual(slots[0].spySetTargeting.args, expected); + }); + + it('should set targeting for bids with `alwaysUseBid=true`', function () { + + // Make sure we're getting the expected losing bid. + assert.equal(pbjs._bidsReceived[0]['bidderCode'], 'triplelift'); + assert.equal(pbjs._bidsReceived[0]['cpm'], 0.112256); + + // Modify the losing bid to have `alwaysUseBid=true` and a custom `adserverTargeting` key. + pbjs._bidsReceived[0]['alwaysUseBid'] = true; + pbjs._bidsReceived[0]['adserverTargeting'] = { + 'always_use_me': 'abc', + }; + + var slots = createSlotArray(); + window.googletag.pubads().setSlots(slots); + + pbjs.setTargetingForGPTAsync(config.adUnitCodes); + + var expected = [ + [ + "hb_bidder", + "appnexus" + ], + [ + "hb_adid", + "233bcbee889d46d" + ], + [ + "hb_pb", + "10.00" + ], + [ + "hb_size", + "300x250" + ], + [ + "foobar", + "300x250" + ], + [ + "always_use_me", + "abc" + ], + [ + "foobar", + "300x250" + ] + ]; + + assert.deepEqual(slots[0].spySetTargeting.args, expected); + }); + + it('should log error when googletag is not defined on page', function () { + const error = 'window.googletag is not defined on the page'; + const windowGoogletagBackup = window.googletag; + window.googletag = {}; + + pbjs.setTargetingForGPTAsync(); + assert.ok(logErrorSpy.calledWith(error), 'expected error was logged'); + window.googletag = windowGoogletagBackup; + }); + }); + + describe('allBidsAvailable', function () { + it('should call bidmanager.allBidsBack', function () { + var spyAllBidsBack = sinon.spy(bidmanager, 'bidsBackAll'); + + pbjs.allBidsAvailable(); + assert.ok(spyAllBidsBack.called, 'called bidmanager.allBidsBack'); + bidmanager.bidsBackAll.restore(); + }); + }); + + describe('renderAd', function () { + var bidId = 1; + var doc = {}; + var adResponse = {}; + var spyLogError = null; + var spyLogMessage = null; + + beforeEach(function () { + doc = { + write: sinon.spy(), + close: sinon.spy(), + defaultView: { + frameElement: { + width: 0, + height: 0 + } + } + }; + + adResponse = { + "adId": bidId, + "width": 300, + "height": 250, + }; + pbjs._bidsReceived.push(adResponse); + + spyLogError = sinon.spy(utils, 'logError'); + spyLogMessage = sinon.spy(utils, 'logMessage'); + }); + + afterEach(function () { + pbjs._bidsReceived.splice(pbjs._bidsReceived.indexOf(adResponse), 1); + utils.logError.restore(); + utils.logMessage.restore(); + }); + + it('should require doc and id params', function () { + pbjs.renderAd(); + var error = 'Error trying to write ad Id :undefined to the page. Missing document or adId'; + assert.ok(spyLogError.calledWith(error), 'expected param error was logged'); + }); + + it('should log message with bid id', function () { + pbjs.renderAd(doc, bidId); + var message = 'Calling renderAd with adId :' + bidId; + assert.ok(spyLogMessage.calledWith(message), 'expected message was logged'); + }); + + it('should write the ad to the doc', function () { + adResponse.ad = ""; + pbjs.renderAd(doc, bidId); + assert.ok(doc.write.calledWith(adResponse.ad), 'ad was written to doc'); + assert.ok(doc.close.called, 'close method called'); + }); + + it('should place the url inside an iframe on the doc', function () { + adResponse.adUrl = "http://server.example.com/ad/ad.js"; + pbjs.renderAd(doc, bidId); + var iframe = '' + assert.ok(doc.write.calledWith(iframe), 'url was written to iframe in doc'); + }); + + it('should log an error when no ad or url', function () { + pbjs.renderAd(doc, bidId); + var error = 'Error trying to write ad. No ad for bid response id: ' + bidId; + assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + }); + + it('should catch errors thrown when trying to write ads to the page', function () { + adResponse.ad = ""; + + var error = { message: 'doc write error' }; + doc.write = sinon.stub().throws(error); + pbjs.renderAd(doc, bidId); + + var errorMessage = 'Error trying to write ad Id :' + bidId + ' to the page:' + error.message; + assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged'); + }); + + it('should log an error when ad not found', function () { + var fakeId = 99; + pbjs.renderAd(doc, fakeId); + var error = 'Error trying to write ad. Cannot find ad by given id : ' + fakeId; + assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + }); + }); + + describe('requestBids', () => { + it('should add bidsBackHandler callback to bidmanager', () => { + var spyAddOneTimeCallBack = sinon.spy(bidmanager, 'addOneTimeCallback'); + var requestObj = { + bidsBackHandler: function bidsBackHandlerCallback() {} + }; + pbjs.requestBids(requestObj); + assert.ok(spyAddOneTimeCallBack.calledWith(requestObj.bidsBackHandler), + 'called bidmanager.addOneTimeCallback'); + bidmanager.addOneTimeCallback.restore(); + resetAuction(); + }); + + it('should log message when adUnits not configured', () => { + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const adUnitsBackup = pbjs.adUnits; + + pbjs.adUnits = []; + pbjs.requestBids({}); + + assert.ok(logMessageSpy.calledWith('No adUnits configured. No bids requested.'), 'expected message was logged'); + utils.logMessage.restore(); + pbjs.adUnits = adUnitsBackup; + resetAuction(); + }); + + it('should execute callback after timeout', () => { + var spyExecuteCallback = sinon.spy(bidmanager, 'executeCallback'); + var clock = sinon.useFakeTimers(); + var requestObj = { + bidsBackHandler: function bidsBackHandlerCallback() {}, + timeout: 2000 + }; + + pbjs.requestBids(requestObj); + + clock.tick(requestObj.timeout - 1); + assert.ok(spyExecuteCallback.notCalled, 'bidmanager.executeCallback not called'); + + clock.tick(1); + assert.ok(spyExecuteCallback.called, 'called bidmanager.executeCallback'); + + bidmanager.executeCallback.restore(); + clock.restore(); + resetAuction(); + }); + + it('should call callBids function on adaptermanager', () => { + var spyCallBids = sinon.spy(adaptermanager, 'callBids'); + pbjs.requestBids({}); + assert.ok(spyCallBids.called, 'called adaptermanager.callBids'); + adaptermanager.callBids.restore(); + resetAuction(); + }); + }); + + describe('onEvent', () => { + it('should log an error when handler is not a function', () => { + var spyLogError = sinon.spy(utils, 'logError'); + var event = 'testEvent'; + pbjs.onEvent(event); + assert.ok(spyLogError.calledWith('The event handler provided is not a function and was not set on event "' + event + '".'), + 'expected error was logged'); + utils.logError.restore(); + }); + + it('should log an error when id provided is not valid for event', () => { + var spyLogError = sinon.spy(utils, 'logError'); + var event = 'bidWon'; + pbjs.onEvent(event, Function, 'testId'); + assert.ok(spyLogError.calledWith('The id provided is not valid for event "' + event + '" and no handler was set.'), + 'expected error was logged'); + utils.logError.restore(); + }); + + it('should call events.on with valid parameters', () => { + var spyEventsOn = sinon.spy(events, 'on'); + pbjs.onEvent('bidWon', Function); + assert.ok(spyEventsOn.calledWith('bidWon', Function)); + events.on.restore(); + }); + }); + + describe('offEvent', () => { + it('should return when id provided is not valid for event', () => { + var spyEventsOff = sinon.spy(events, 'off'); + pbjs.offEvent('bidWon', Function, 'testId'); + assert.ok(spyEventsOff.notCalled); + events.off.restore(); + }); + + it('should call events.off with valid parameters', () => { + var spyEventsOff = sinon.spy(events, 'off'); + pbjs.offEvent('bidWon', Function); + assert.ok(spyEventsOff.calledWith('bidWon', Function)); + events.off.restore(); + }); + }); + + describe('addCallback', () => { + it('should log error and return null id when error registering callback', () => { + var spyLogError = sinon.spy(utils, 'logError'); + var id = pbjs.addCallback('event', 'fakeFunction'); + assert.equal(id, null, 'id returned was null'); + assert.ok(spyLogError.calledWith('error registering callback. Check method signature'), + 'expected error was logged'); + utils.logError.restore(); + }); + + it('should add callback to bidmanager', () => { + var spyAddCallback = sinon.spy(bidmanager, 'addCallback'); + var id = pbjs.addCallback('event', Function); + assert.ok(spyAddCallback.calledWith(id, Function, 'event'), 'called bidmanager.addCallback'); + bidmanager.addCallback.restore(); + }); + }); + + describe('removeCallback', () => { + it('should return null', () => { + const id = pbjs.removeCallback(); + assert.equal(id, null); + }); + }); + + describe('registerBidAdapter', () => { + it('should register bidAdaptor with adaptermanager', () => { + var registerBidAdapterSpy = sinon.spy(adaptermanager, 'registerBidAdapter'); + pbjs.registerBidAdapter(Function, 'biddercode'); + assert.ok(registerBidAdapterSpy.called, 'called adaptermanager.registerBidAdapter'); + adaptermanager.registerBidAdapter.restore(); + }); + + it('should catch thrown errors', () => { + var spyLogError = sinon.spy(utils, 'logError'); + var errorObject = {message: 'bidderAdaptor error'}; + var bidderAdaptor = sinon.stub().throws(errorObject); + + pbjs.registerBidAdapter(bidderAdaptor, 'biddercode'); + + var errorMessage = 'Error registering bidder adapter : ' + errorObject.message; + assert.ok(spyLogError.calledWith(errorMessage), 'expected error was caught'); + utils.logError.restore(); + }); + }); + + describe('bidsAvailableForAdapter', () => { + it('should update requested bid with status set to available', () => { + const bidderCode = 'appnexus'; + pbjs.bidsAvailableForAdapter(bidderCode); + + const requestedBids = pbjs._bidsRequested.find(bid => bid.bidderCode === bidderCode); + requestedBids.bids.forEach(bid => { + assert.equal(bid.bidderCode, bidderCode, 'bidderCode was set'); + assert.equal(bid.statusMessage, 'Bid available', 'bid set as available'); + }); + }); + }); + + describe('createBid', () => { + it('should return a bid object', () => { + const statusCode = 1; + const bid = pbjs.createBid(statusCode); + assert.isObject(bid, 'bid is an object'); + assert.equal(bid.getStatusCode(), statusCode, 'bid has correct status'); + + const defaultStatusBid = pbjs.createBid(); + assert.isObject(defaultStatusBid, 'bid is an object'); + assert.equal(defaultStatusBid.getStatusCode(), 0, 'bid has correct status'); + }); + }); + + describe('addBidResponse', () => { + it('should call bidmanager.addBidResponse', () => { + const addBidResponseStub = sinon.stub(bidmanager, 'addBidResponse'); + const adUnitCode = 'testcode'; + const bid = pbjs.createBid(0); + + pbjs.addBidResponse(adUnitCode, bid); + assert.ok(addBidResponseStub.calledWith(adUnitCode, bid), 'called bidmanager.addBidResponse'); + bidmanager.addBidResponse.restore(); + }); + }); + + describe('loadScript', () => { + it('should call adloader.loadScript', () => { + const loadScriptSpy = sinon.spy(adloader, 'loadScript'); + const tagSrc = 'testsrc'; + const callback = Function; + const useCache = false; + + pbjs.loadScript(tagSrc, callback, useCache); + assert.ok(loadScriptSpy.calledWith(tagSrc, callback, useCache), 'called adloader.loadScript'); + adloader.loadScript.restore(); + }); + }); + + describe('enableAnalytics', () => { + let logErrorSpy; + + beforeEach(() => { + logErrorSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(() => { + utils.logError.restore(); + }); + + it('should log error when not passed options', () => { + const error = 'pbjs.enableAnalytics should be called with option {}'; + pbjs.enableAnalytics(); + assert.ok(logErrorSpy.calledWith(error), 'expected error was logged'); + }); + + it('should call ga.enableAnalytics with options', () => { + const enableAnalyticsSpy = sinon.spy(ga, 'enableAnalytics'); + + let options = {'provider': 'ga'}; + pbjs.enableAnalytics(options); + assert.ok(enableAnalyticsSpy.calledWith({}), 'ga.enableAnalytics called with empty options object'); + + options['options'] = 'testoptions'; + pbjs.enableAnalytics(options); + assert.ok(enableAnalyticsSpy.calledWith(options.options), 'ga.enableAnalytics called with provided options'); + + ga.enableAnalytics.restore(); + }); + + it('should catch errors thrown from ga.enableAnalytics', () => { + const error = {message: 'Error calling GA: '}; + const enableAnalyticsStub = sinon.stub(ga, 'enableAnalytics').throws(error); + const options = {'provider': 'ga'}; + + pbjs.enableAnalytics(options); + assert.ok(logErrorSpy.calledWith(error.message), 'expected error was caught'); + ga.enableAnalytics.restore(); + }); + + it('should return null for other providers', () => { + const options = {'provider': 'other_provider'}; + const returnValue = pbjs.enableAnalytics(options); + assert.equal(returnValue, null, 'expected return value'); + }); + }); + + describe('sendTimeoutEvent', () => { + it('should emit BID_TIMEOUT for timed out bids', () => { + const eventsEmitSpy = sinon.spy(events, 'emit'); + pbjs.sendTimeoutEvent(); + assert.ok(eventsEmitSpy.calledWith(CONSTANTS.EVENTS.BID_TIMEOUT), 'emitted events BID_TIMEOUT'); + events.emit.restore(); + }); + }); + + describe('aliasBidder', () => { + it('should call adaptermanager.aliasBidder', () => { + const aliasBidAdapterSpy = sinon.spy(adaptermanager, 'aliasBidAdapter'); + const bidderCode = 'testcode'; + const alias = 'testalias'; + + pbjs.aliasBidder(bidderCode, alias); + assert.ok(aliasBidAdapterSpy.calledWith(bidderCode, alias), 'called adaptermanager.aliasBidAdapterSpy'); + adaptermanager.aliasBidAdapter.restore(); + }); + + it('should log error when not passed correct arguments', () => { + const logErrorSpy = sinon.spy(utils, 'logError'); + const error = 'bidderCode and alias must be passed as arguments'; + + pbjs.aliasBidder(); + assert.ok(logErrorSpy.calledWith(error), 'expected error was logged'); + utils.logError.restore(); + }); + }); + + describe('setPriceGranularity', () => { + it('should log error when not passed granularity', () => { + const logErrorSpy = sinon.spy(utils, 'logError'); + const error = 'Prebid Error: no value passed to `setPriceGranularity()`'; + + pbjs.setPriceGranularity(); + assert.ok(logErrorSpy.calledWith(error), 'expected error was logged'); + utils.logError.restore(); + }); + + it('should call bidmanager.setPriceGranularity with granularity', () => { + const setPriceGranularitySpy = sinon.spy(bidmanager, 'setPriceGranularity'); + const granularity = 'low'; + + pbjs.setPriceGranularity(granularity); + assert.ok(setPriceGranularitySpy.called, 'called bidmanager.setPriceGranularity'); + bidmanager.setPriceGranularity.restore(); + }); + }); +});