From 033d73351859fc96da338222ee3f540249c0e524 Mon Sep 17 00:00:00 2001 From: Muhammad Usman Date: Sat, 14 Apr 2018 01:54:36 +0200 Subject: [PATCH] Widespace adapter (#2283) Update WideSpce Adapter for Prebid 1.x --- modules/widespaceBidAdapter.js | 239 ++++++++++++++++++ modules/widespaceBidAdapter.md | 40 +++ test/spec/modules/widespaceBidAdapter_spec.js | 213 ++++++++++++++++ 3 files changed, 492 insertions(+) create mode 100644 modules/widespaceBidAdapter.js create mode 100644 modules/widespaceBidAdapter.md create mode 100644 test/spec/modules/widespaceBidAdapter_spec.js diff --git a/modules/widespaceBidAdapter.js b/modules/widespaceBidAdapter.js new file mode 100644 index 00000000000..9493428cec7 --- /dev/null +++ b/modules/widespaceBidAdapter.js @@ -0,0 +1,239 @@ +import { version } from '../package.json'; +import { config } from 'src/config'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { + cookiesAreEnabled, + parseQueryStringParameters, + parseSizesInput, + getTopWindowReferrer +} from 'src/utils'; + +const BIDDER_CODE = 'widespace'; +const WS_ADAPTER_VERSION = '2.0.0'; +const LOCAL_STORAGE_AVAILABLE = window.localStorage; +const COOKIE_ENABLED = cookiesAreEnabled(); +const LS_KEYS = { + PERF_DATA: 'wsPerfData', + LC_UID: 'wsLcuid', + CUST_DATA: 'wsCustomData' +}; + +let preReqTime = 0; + +export const spec = { + code: BIDDER_CODE, + + supportedMediaTypes: ['banner'], + + isBidRequestValid: function(bid) { + if (bid.params && bid.params.sid) { + return true; + } + return false; + }, + + buildRequests: function(validBidRequests) { + let serverRequests = []; + const REQUEST_SERVER_URL = getEngineUrl(); + const DEMO_DATA_PARAMS = ['gender', 'country', 'region', 'postal', 'city', 'yob']; + const PERF_DATA = getData(LS_KEYS.PERF_DATA).map(perf_data => JSON.parse(perf_data)); + const CUST_DATA = getData(LS_KEYS.CUST_DATA, false)[0]; + const LC_UID = getLcuid(); + + let isInHostileIframe = false; + try { + window.top.location.toString(); + isInHostileIframe = false; + } catch (e) { + isInHostileIframe = true; + } + + validBidRequests.forEach((bid, i) => { + let data = { + 'screenWidthPx': screen && screen.width, + 'screenHeightPx': screen && screen.height, + 'adSpaceHttpRefUrl': getTopWindowReferrer(), + 'referer': (isInHostileIframe ? window : window.top).location.href.split('#')[0], + 'inFrame': 1, + 'sid': bid.params.sid, + 'lcuid': LC_UID, + 'vol': isInHostileIframe ? '' : visibleOnLoad(document.getElementById(bid.adUnitCode)), + 'hb': '1', + 'hb.cd': CUST_DATA ? encodedParamValue(CUST_DATA) : '', + 'hb.floor': bid.bidfloor || '', + 'hb.spb': i === 0 ? pixelSyncPossibility() : -1, + 'hb.ver': WS_ADAPTER_VERSION, + 'hb.name': `prebidjs-${version}`, + 'hb.bidId': bid.bidId, + 'hb.sizes': parseSizesInput(bid.sizes).join(','), + 'hb.currency': bid.params.cur || bid.params.currency || '' + }; + + // Include demo data + if (bid.params.demo) { + DEMO_DATA_PARAMS.forEach((key) => { + if (bid.params.demo[key]) { + data[key] = bid.params.demo[key]; + } + }); + } + + // Include performance data + if (PERF_DATA[i]) { + Object.keys(PERF_DATA[i]).forEach((perfDataKey) => { + data[perfDataKey] = PERF_DATA[i][perfDataKey]; + }); + } + + // Include connection info if available + const CONNECTION = navigator.connection || navigator.webkitConnection; + if (CONNECTION && CONNECTION.type && CONNECTION.downlinkMax) { + data['netinfo.type'] = CONNECTION.type; + data['netinfo.downlinkMax'] = CONNECTION.downlinkMax; + } + + // Include debug data when available + if (!isInHostileIframe) { + const DEBUG_AD = (window.top.location.hash.split('&').find((val) => { + return val.includes('WS_DEBUG_FORCEADID'); + }) || '').split('=')[1]; + data.forceAdId = DEBUG_AD; + } + + // Remove empty params + Object.keys(data).forEach((key) => { + if (data[key] === '' || data[key] === undefined) { + delete data[key]; + } + }); + + serverRequests.push({ + method: 'POST', + options: { + contentType: 'application/x-www-form-urlencoded' + }, + url: REQUEST_SERVER_URL, + data: parseQueryStringParameters(data) + }); + }); + preReqTime = Date.now(); + return serverRequests; + }, + + interpretResponse: function(serverResponse, request) { + const responseTime = Date.now() - preReqTime; + const successBids = serverResponse.body || []; + let bidResponses = []; + successBids.forEach((bid) => { + storeData({ + 'perf_status': 'OK', + 'perf_reqid': bid.reqId, + 'perf_ms': responseTime + }, `${LS_KEYS.PERF_DATA}${bid.reqId}`); + if (bid.status === 'ad') { + bidResponses.push({ + requestId: bid.bidId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.adId, + currency: bid.currency, + netRevenue: Boolean(bid.netRev), + ttl: bid.ttl, + referrer: getTopWindowReferrer(), + ad: bid.code + }); + } + }); + + return bidResponses + }, + + getUserSyncs: function(syncOptions, serverResponses = []) { + let userSyncs = []; + userSyncs = serverResponses.reduce((allSyncPixels, response) => { + if (response && response.body && response.body[0]) { + (response.body[0].syncPixels || []).forEach((url) => { + allSyncPixels.push({type: 'image', url}); + }); + } + return allSyncPixels; + }, []); + return userSyncs; + } +}; + +function storeData(data, name, stringify = true) { + const value = stringify ? JSON.stringify(data) : data; + if (LOCAL_STORAGE_AVAILABLE) { + localStorage.setItem(name, value); + return true; + } else if (COOKIE_ENABLED) { + const theDate = new Date(); + const expDate = new Date(theDate.setMonth(theDate.getMonth() + 12)).toGMTString(); + window.document.cookie = `${name}=${value};path=/;expires=${expDate}`; + return true; + } +} + +function getData(name, remove = true) { + let data = []; + if (LOCAL_STORAGE_AVAILABLE) { + Object.keys(localStorage).filter((key) => { + if (key.includes(name)) { + data.push(localStorage.getItem(key)); + if (remove) { + localStorage.removeItem(key); + } + } + }); + } + + if (COOKIE_ENABLED) { + document.cookie.split(';').forEach((item) => { + let value = item.split('='); + if (value[0].includes(name)) { + data.push(value[1]); + if (remove) { + document.cookie = `${value[0]}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`; + } + } + }); + } + return data; +} + +function pixelSyncPossibility() { + const userSync = config.getConfig('userSync'); + return userSync && userSync.pixelEnabled && userSync.syncEnabled ? userSync.syncsPerBidder : -1; +} + +function visibleOnLoad(element) { + if (element && element.getBoundingClientRect) { + const topPos = element.getBoundingClientRect().top; + return topPos < screen.height && topPos >= window.top.pageYOffset ? 1 : 0; + }; + return ''; +} + +function getLcuid() { + let lcuid = getData(LS_KEYS.LC_UID, false)[0]; + if (!lcuid) { + const random = ('4' + new Date().getTime() + String(Math.floor(Math.random() * 1000000000))).substring(0, 18); + storeData(random, LS_KEYS.LC_UID, false); + lcuid = getData(LS_KEYS.LC_UID, false)[0]; + } + return lcuid; +} + +function encodedParamValue(value) { + const requiredStringify = typeof JSON.parse(JSON.stringify(value)) === 'object'; + return encodeURIComponent(requiredStringify ? JSON.stringify(value) : value); +} + +function getEngineUrl() { + const ENGINE_URL = 'https://engine.widespace.com/map/engine/dynadreq'; + return window.wisp && window.wisp.ENGINE_URL ? window.wisp.ENGINE_URL : ENGINE_URL; +} + +registerBidder(spec); diff --git a/modules/widespaceBidAdapter.md b/modules/widespaceBidAdapter.md new file mode 100644 index 00000000000..1ca2b61d406 --- /dev/null +++ b/modules/widespaceBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + + +**Module Name:** Widespace Bidder Adapter. +**Module Type:** Bidder Adapter. +**Maintainer:** support@widespace.com + + +# Description + +Widespace Bidder Adapter for Prebid.js. +Banner and video formats are supported. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250], [300, 300]], + bids: [ + { + bidder: 'widespace', + params: { + sid: '7b6589bf-95c8-4656-90b9-af9737bb9ad3', // Required + currency: 'EUR', // Optional + bidfloor: '0.5', // Optional + demo: { // Optional + gender: 'M', + country: 'Sweden', + region: 'Stockholm', + postal: '15115', + city: 'Stockholm', + yob: '1984' + } + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/widespaceBidAdapter_spec.js b/test/spec/modules/widespaceBidAdapter_spec.js new file mode 100644 index 00000000000..3df5b6bfff2 --- /dev/null +++ b/test/spec/modules/widespaceBidAdapter_spec.js @@ -0,0 +1,213 @@ +import { expect } from 'chai'; +import { spec } from 'modules/widespaceBidAdapter'; + +describe('+widespaceAdatperTest', () => { + // Dummy bid request + const bidRequest = [{ + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'auctionId': 'bf1e57ee-fff2-4304-8143-91aaf423a948', + 'bidId': '4045696e2278cd', + 'bidder': 'widespace', + 'params': { + sid: '7b6589bf-95c8-4656-90b9-af9737bb9ad3', + currency: 'EUR', + demo: { + gender: 'M', + country: 'Sweden', + region: 'Stockholm', + postal: '15115', + city: 'Stockholm', + yob: '1984' + } + }, + 'bidderRequestId': '37a5f053efef34', + 'sizes': [[320, 320], [300, 250], [300, 300]], + 'transactionId': '4f68b713-04ba-4d7f-8df9-643bcdab5efb' + }, { + 'adUnitCode': 'div-gpt-ad-1460505748561-1', + 'auctionId': 'bf1e57ee-fff2-4304-8143-91aaf423a944', + 'bidId': '4045696e2278ab', + 'bidder': 'widespace', + 'params': { + sid: '7b6589bf-95c8-4656-90b9-af9737bb9ad4', + demo: { + gender: 'M', + country: 'Sweden', + region: 'Stockholm', + postal: '15115', + city: 'Stockholm', + yob: '1984' + } + }, + 'bidderRequestId': '37a5f053efef34', + 'sizes': [[300, 300]], + 'transactionId': '4f68b713-04ba-4d7f-8df9-643bcdab5efv' + }]; + + // Dummy bid response with ad code + const bidResponse = { + body: [{ + 'adId': '12345', + 'bidId': '67890', + 'code': '
', + 'cpm': 6.6, + 'currency': 'EUR', + 'height': 300, + 'netRev': true, + 'reqId': '224804081406', + 'status': 'ad', + 'ttl': 30, + 'width': 300, + 'syncPixels': ['https://url1.com/url', 'https://url2.com/url'] + }], + headers: {} + }; + + // Dummy bid response of noad + const bidResponseNoAd = { + body: [{ + 'status': 'noad', + }], + headers: {} + }; + + // Appending a div with id of adUnitCode so we can calculate vol + const div1 = document.createElement('div'); + div1.id = bidRequest[0].adUnitCode; + document.body.appendChild(div1); + const div2 = document.createElement('div'); + div2.id = bidRequest[0].adUnitCode; + document.body.appendChild(div2); + + // Adding custom data cookie se we can test cookie is readable + const theDate = new Date(); + const expDate = new Date(theDate.setMonth(theDate.getMonth() + 1)).toGMTString(); + window.document.cookie = `wsCustomData1={id: test};path=/;expires=${expDate};`; + const PERF_DATA = JSON.stringify({perf_status: 'OK', perf_reqid: '226920425154', perf_ms: '747'}); + window.document.cookie = `wsPerfData123=${PERF_DATA};path=/;expires=${expDate};`; + + // Connect dummy data test + navigator.connection.downlinkMax = 80; + navigator.connection.type = 'wifi'; + + describe('+bidRequestValidity', () => { + it('bidRequest with sid and currency params', () => { + expect(spec.isBidRequestValid({ + bidder: 'widespace', + params: { + sid: '7b6589bf-95c8-4656-90b9-af9737bb9ad3', + currency: 'EUR' + } + })).to.equal(true); + }); + + it('-bidRequest with missing sid', () => { + expect(spec.isBidRequestValid({ + bidder: 'widespace', + params: { + currency: 'EUR' + } + })).to.equal(false); + }); + + it('-bidRequest with missing currency', () => { + expect(spec.isBidRequestValid({ + bidder: 'widespace', + params: { + sid: '7b6589bf-95c8-4656-90b9-af9737bb9ad3' + } + })).to.equal(true); + }); + }); + + describe('+bidRequest', () => { + const request = spec.buildRequests(bidRequest); + const UrlRegExp = /^((ftp|http|https):)?\/\/[^ "]+$/; + + it('-bidRequest method is POST', () => { + expect(request[0].method).to.equal('POST'); + }); + + it('-bidRequest url is valid', () => { + expect(UrlRegExp.test(request[0].url)).to.equal(true); + }); + + it('-bidRequest data exist', () => { + expect(request[0].data).to.exists; + }); + + it('-bidRequest data is form data', () => { + expect(typeof request[0].data).to.equal('string'); + }); + + it('-bidRequest options have header type', () => { + expect(request[0].options.contentType).to.exists; + }); + + it('-cookie test for wsCustomData ', () => { + expect(request[0].data.includes('hb.cd')).to.equal(true); + }); + }); + + describe('+interpretResponse', () => { + it('-required params available in response', () => { + const result = spec.interpretResponse(bidResponse, bidRequest); + let requiredKeys = [ + 'requestId', + 'cpm', + 'width', + 'height', + 'creativeId', + 'currency', + 'netRevenue', + 'ttl', + 'referrer', + 'ad' + ]; + const resultKeys = Object.keys(result[0]); + requiredKeys.forEach((key) => { + expect(resultKeys.includes(key)).to.equal(true); + }); + + // Each value except referrer should not be empty|null|undefined + result.forEach((res) => { + Object.keys(res).forEach((resKey) => { + if (resKey !== 'referrer') { + expect(res[resKey]).to.not.be.null; + expect(res[resKey]).to.not.be.undefined; + expect(res[resKey]).to.not.equal(''); + } + }); + }); + }); + + it('-empty result if noad responded', () => { + const noAdResult = spec.interpretResponse(bidResponseNoAd, bidRequest); + expect(noAdResult.length).to.equal(0); + }); + + it('-empty response should not breake anything in adapter', () => { + const noResponse = spec.interpretResponse({}, bidRequest); + expect(noResponse.length).to.equal(0); + }); + }); + + describe('+getUserSyncs', () => { + it('-always return an array', () => { + const userSync_test1 = spec.getUserSyncs({}, [bidResponse]); + expect(Array.isArray(userSync_test1)).to.equal(true); + + const userSync_test2 = spec.getUserSyncs({}, [bidResponseNoAd]); + expect(Array.isArray(userSync_test2)).to.equal(true); + + const userSync_test3 = spec.getUserSyncs({}, [bidResponse, bidResponseNoAd]); + expect(Array.isArray(userSync_test3)).to.equal(true); + + const userSync_test4 = spec.getUserSyncs(); + expect(Array.isArray(userSync_test4)).to.equal(true); + + const userSync_test5 = spec.getUserSyncs({}, []); + expect(Array.isArray(userSync_test5)).to.equal(true); + }); + }); +});