diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index a5bb3797bbf..3e618692725 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -3,26 +3,230 @@ import CONSTANTS from '../src/constants.json'; import adaptermanager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; import {ajax} from '../src/ajax.js'; +import {getStorageManager} from '../src/storageManager.js'; + +export const storage = getStorageManager(); const analyticsType = 'endpoint'; +// dev endpoints +// const preflightUrl = 'https://analytics-check.publishersite.xyz/check/'; +// export const analyticsUrl = 'https://analyticsv2.publishersite.xyz'; + +const preflightUrl = 'https://check.analytics.rlcdn.com/check/'; +export const analyticsUrl = 'https://analytics.rlcdn.com'; let handlerRequest = []; let handlerResponse = []; -let host = ''; + +let atsAnalyticsAdapterVersion = 1; + +let browsersList = [ + /* Googlebot */ + { + test: /googlebot/i, + name: 'Googlebot' + }, + + /* Opera < 13.0 */ + { + test: /opera/i, + name: 'Opera', + }, + + /* Opera > 13.0 */ + { + test: /opr\/|opios/i, + name: 'Opera' + }, + { + test: /SamsungBrowser/i, + name: 'Samsung Internet for Android', + }, + { + test: /Whale/i, + name: 'NAVER Whale Browser', + }, + { + test: /MZBrowser/i, + name: 'MZ Browser' + }, + { + test: /focus/i, + name: 'Focus', + }, + { + test: /swing/i, + name: 'Swing', + }, + { + test: /coast/i, + name: 'Opera Coast', + }, + { + test: /opt\/\d+(?:.?_?\d+)+/i, + name: 'Opera Touch', + }, + { + test: /yabrowser/i, + name: 'Yandex Browser', + }, + { + test: /ucbrowser/i, + name: 'UC Browser', + }, + { + test: /Maxthon|mxios/i, + name: 'Maxthon', + }, + { + test: /epiphany/i, + name: 'Epiphany', + }, + { + test: /puffin/i, + name: 'Puffin', + }, + { + test: /sleipnir/i, + name: 'Sleipnir', + }, + { + test: /k-meleon/i, + name: 'K-Meleon', + }, + { + test: /micromessenger/i, + name: 'WeChat', + }, + { + test: /qqbrowser/i, + name: (/qqbrowserlite/i).test(window.navigator.userAgent) ? 'QQ Browser Lite' : 'QQ Browser', + }, + { + test: /msie|trident/i, + name: 'Internet Explorer', + }, + { + test: /\sedg\//i, + name: 'Microsoft Edge', + }, + { + test: /edg([ea]|ios)/i, + name: 'Microsoft Edge', + }, + { + test: /vivaldi/i, + name: 'Vivaldi', + }, + { + test: /seamonkey/i, + name: 'SeaMonkey', + }, + { + test: /sailfish/i, + name: 'Sailfish', + }, + { + test: /silk/i, + name: 'Amazon Silk', + }, + { + test: /phantom/i, + name: 'PhantomJS', + }, + { + test: /slimerjs/i, + name: 'SlimerJS', + }, + { + test: /blackberry|\bbb\d+/i, + name: 'BlackBerry', + }, + { + test: /(web|hpw)[o0]s/i, + name: 'WebOS Browser', + }, + { + test: /bada/i, + name: 'Bada', + }, + { + test: /tizen/i, + name: 'Tizen', + }, + { + test: /qupzilla/i, + name: 'QupZilla', + }, + { + test: /firefox|iceweasel|fxios/i, + name: 'Firefox', + }, + { + test: /electron/i, + name: 'Electron', + }, + { + test: /MiuiBrowser/i, + name: 'Miui', + }, + { + test: /chromium/i, + name: 'Chromium', + }, + { + test: /chrome|crios|crmo/i, + name: 'Chrome', + }, + { + test: /GSA/i, + name: 'Google Search', + }, + + /* Android Browser */ + { + test: /android/i, + name: 'Android Browser', + }, + + /* PlayStation 4 */ + { + test: /playstation 4/i, + name: 'PlayStation 4', + }, + + /* Safari */ + { + test: /safari|applewebkit/i, + name: 'Safari', + }, +]; + +function setSamplingCookie(samplRate) { + let now = new Date(); + now.setTime(now.getTime() + 3600000); + storage.setCookie('_lr_sampling_rate', samplRate, now.toUTCString()); +} + +let listOfSupportedBrowsers = ['Safari', 'Chrome', 'Firefox', 'Microsoft Edge']; function bidRequestedHandler(args) { + let envelopeSourceCookieValue = storage.getCookie('_lr_env_src_ats'); + let envelopeSource = envelopeSourceCookieValue === 'true'; let requests; requests = args.bids.map(function(bid) { return { + envelope_source: envelopeSource, has_envelope: bid.userId ? !!bid.userId.idl_env : false, bidder: bid.bidder, bid_id: bid.bidId, auction_id: args.auctionId, - user_browser: checkUserBrowser(), + user_browser: parseBrowser(), user_platform: navigator.platform, auction_start: new Date(args.auctionStart).toJSON(), domain: window.location.hostname, pid: atsAnalyticsAdapter.context.pid, + adapter_version: atsAnalyticsAdapterVersion }; }); return requests; @@ -38,58 +242,44 @@ function bidResponseHandler(args) { }; } -export function checkUserBrowser() { - let firefox = browserIsFirefox(); - let chrome = browserIsChrome(); - let edge = browserIsEdge(); - let safari = browserIsSafari(); - if (firefox) { - return firefox; - } if (chrome) { - return chrome; - } if (edge) { - return edge; - } if (safari) { - return safari; - } else { - return 'Unknown' +export function parseBrowser() { + let ua = atsAnalyticsAdapter.getUserAgent(); + try { + let result = browsersList.filter(function(obj) { + return obj.test.test(ua); + }); + let browserName = result && result.length ? result[0].name : ''; + return (listOfSupportedBrowsers.indexOf(browserName) >= 0) ? browserName : 'Unknown'; + } catch (err) { + utils.logError('ATS Analytics - Error while checking user browser!', err); } } -export function browserIsFirefox() { - if (typeof InstallTrigger !== 'undefined') { - return 'Firefox'; - } else { - return false; +function sendDataToAnalytic () { + // send data to ats analytic endpoint + try { + let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; + let strJSON = JSON.stringify(dataToSend); + utils.logInfo('ATS Analytics - tried to send analytics data!'); + ajax(analyticsUrl, function () { + }, strJSON, {method: 'POST', contentType: 'application/json'}); + } catch (err) { + utils.logError('ATS Analytics - request encounter an error: ', err); } } -export function browserIsIE() { - return !!document.documentMode; -} - -export function browserIsEdge() { - if (!browserIsIE() && !!window.StyleMedia) { - return 'Edge'; - } else { - return false; - } -} - -export function browserIsChrome() { - if ((!!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime)) || (/Android/i.test(navigator.userAgent) && !!window.chrome)) { - return 'Chrome'; - } else { - return false; - } -} - -export function browserIsSafari() { - if (window.safari !== undefined) { - return 'Safari' - } else { - return false; - } +// preflight request, to check did publisher have permission to send data to analytics endpoint +function preflightRequest (envelopeSourceCookieValue) { + ajax(preflightUrl + atsAnalyticsAdapter.context.pid, function (data) { + let samplingRateObject = JSON.parse(data); + utils.logInfo('ATS Analytics - Sampling Rate: ', samplingRateObject); + let samplingRate = samplingRateObject['samplingRate']; + setSamplingCookie(samplingRate); + let samplingRateNumber = Number(samplingRate); + if (data && samplingRate && atsAnalyticsAdapter.shouldFireRequest(samplingRateNumber) && envelopeSourceCookieValue != null) { + sendDataToAnalytic(); + } + }, undefined, { method: 'GET', crossOrigin: true }); } function callHandler(evtype, args) { @@ -117,7 +307,6 @@ function callHandler(evtype, args) { let atsAnalyticsAdapter = Object.assign(adapter( { - host, analyticsType }), { @@ -126,16 +315,19 @@ let atsAnalyticsAdapter = Object.assign(adapter( callHandler(eventType, args); } if (eventType === CONSTANTS.EVENTS.AUCTION_END) { - if (atsAnalyticsAdapter.shouldFireRequest()) { - // send data to ats analytic endpoint - try { - let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; - let strJSON = JSON.stringify(dataToSend); - utils.logInfo('atsAnalytics tried to send analytics data!'); - ajax(atsAnalyticsAdapter.context.host, function () { - }, strJSON, {method: 'POST', contentType: 'application/json'}); - } catch (err) { + let envelopeSourceCookieValue = storage.getCookie('_lr_env_src_ats'); + try { + utils.logInfo('ATS Analytics - preflight request!'); + let samplingRateCookie = storage.getCookie('_lr_sampling_rate'); + if (!samplingRateCookie) { + preflightRequest(envelopeSourceCookieValue); + } else { + if (atsAnalyticsAdapter.shouldFireRequest(parseInt(samplingRateCookie)) && envelopeSourceCookieValue != null) { + sendDataToAnalytic(); + } } + } catch (err) { + utils.logError('ATS Analytics - preflight request encounter an error: ', err); } } } @@ -145,26 +337,23 @@ let atsAnalyticsAdapter = Object.assign(adapter( atsAnalyticsAdapter.originEnableAnalytics = atsAnalyticsAdapter.enableAnalytics; // add check to not fire request every time, but instead to send 1/10 events -atsAnalyticsAdapter.shouldFireRequest = function () { - return (Math.floor((Math.random() * 11)) === 10); -} +atsAnalyticsAdapter.shouldFireRequest = function (samplingRate) { + let shouldFireRequestValue = (Math.floor((Math.random() * samplingRate + 1)) === samplingRate); + utils.logInfo('ATS Analytics - Should Fire Request: ', shouldFireRequestValue); + return shouldFireRequestValue; +}; +atsAnalyticsAdapter.getUserAgent = function () { + return window.navigator.userAgent; +}; // override enableAnalytics so we can get access to the config passed in from the page atsAnalyticsAdapter.enableAnalytics = function (config) { if (!config.options.pid) { - utils.logError('Publisher ID (pid) option is not defined. Analytics won\'t work'); + utils.logError('ATS Analytics - Publisher ID (pid) option is not defined. Analytics won\'t work'); return; } - - if (!config.options.host) { - utils.logError('Host option is not defined. Analytics won\'t work'); - return; - } - - host = config.options.host; atsAnalyticsAdapter.context = { events: [], - host: config.options.host, pid: config.options.pid }; let initOptions = config.options; diff --git a/modules/atsAnalyticsAdapter.md b/modules/atsAnalyticsAdapter.md index 560ad237aa0..7c634f39ae2 100644 --- a/modules/atsAnalyticsAdapter.md +++ b/modules/atsAnalyticsAdapter.md @@ -17,7 +17,6 @@ Analytics adapter for Authenticated Traffic Solution(ATS), provided by LiveRamp. provider: 'atsAnalytics', options: { pid: '999', // publisher ID - host: 'https://example.com' // host is provided to publisher } } ``` diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index 33dd74380ac..716929db70a 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -43,7 +43,7 @@ export const identityLinkSubmodule = { getId(config, consentData) { const configParams = (config && config.params) || {}; if (!configParams || typeof configParams.pid !== 'string') { - utils.logError('identityLink submodule requires partner id to be defined'); + utils.logError('identityLink: requires partner id to be defined'); return; } const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; @@ -51,7 +51,7 @@ export const identityLinkSubmodule = { const tcfPolicyV2 = utils.deepAccess(consentData, 'vendorData.tcfPolicyVersion') === 2; // use protocol relative urls for http or https if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { - utils.logInfo('Consent string is required to call envelope API.'); + utils.logInfo('identityLink: Consent string is required to call envelope API.'); return; } const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? (tcfPolicyV2 ? '&ct=4&cv=' : '&ct=1&cv=') + gdprConsentString : ''}`; @@ -60,10 +60,11 @@ export const identityLinkSubmodule = { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint if (window.ats) { - utils.logInfo('ATS exists!'); + utils.logInfo('identityLink: ATS exists!'); window.ats.retrieveEnvelope(function (envelope) { if (envelope) { - utils.logInfo('An envelope can be retrieved from ATS!'); + utils.logInfo('identityLink: An envelope can be retrieved from ATS!'); + setEnvelopeSource(true); callback(JSON.parse(envelope).envelope); } else { getEnvelope(url, callback); @@ -92,14 +93,15 @@ function getEnvelope(url, callback) { callback((responseObj && responseObj.envelope) ? responseObj.envelope : ''); }, error: error => { - utils.logInfo(`identityLink: ID fetch encountered an error`, error); + utils.logInfo(`identityLink: identityLink: ID fetch encountered an error`, error); callback(); } }; if (!storage.getCookie('_lr_retry_request')) { setRetryCookie(); - utils.logInfo('A 3P retrieval is attempted!'); + utils.logInfo('identityLink: A 3P retrieval is attempted!'); + setEnvelopeSource(false); ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); } } @@ -110,4 +112,10 @@ function setRetryCookie() { storage.setCookie('_lr_retry_request', 'true', now.toUTCString()); } +function setEnvelopeSource(src) { + let now = new Date(); + now.setTime(now.getTime() + 2592000000); + storage.setCookie('_lr_env_src_ats', src, now.toUTCString()); +} + submodule('userId', identityLinkSubmodule); diff --git a/test/spec/modules/atsAnalyticsAdapter_spec.js b/test/spec/modules/atsAnalyticsAdapter_spec.js index 84206337fad..59b9105925a 100644 --- a/test/spec/modules/atsAnalyticsAdapter_spec.js +++ b/test/spec/modules/atsAnalyticsAdapter_spec.js @@ -2,27 +2,38 @@ import atsAnalyticsAdapter from '../../../modules/atsAnalyticsAdapter.js'; import { expect } from 'chai'; import adapterManager from 'src/adapterManager.js'; import {server} from '../../mocks/xhr.js'; -import {checkUserBrowser, browserIsChrome, browserIsEdge, browserIsSafari, browserIsFirefox} from '../../../modules/atsAnalyticsAdapter.js'; +import {parseBrowser} from '../../../modules/atsAnalyticsAdapter.js'; +import {getStorageManager} from '../../../src/storageManager.js'; +import {analyticsUrl} from '../../../modules/atsAnalyticsAdapter.js'; + let events = require('src/events'); let constants = require('src/constants.json'); +export const storage = getStorageManager(); + describe('ats analytics adapter', function () { beforeEach(function () { sinon.stub(events, 'getEvents').returns([]); + storage.setCookie('_lr_env_src_ats', 'true', 'Thu, 01 Jan 1970 00:00:01 GMT'); }); afterEach(function () { events.getEvents.restore(); + atsAnalyticsAdapter.getUserAgent.restore(); atsAnalyticsAdapter.disableAnalytics(); }); describe('track', function () { it('builds and sends request and response data', function () { sinon.stub(atsAnalyticsAdapter, 'shouldFireRequest').returns(true); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'); + let now = new Date(); + now.setTime(now.getTime() + 3600000); + storage.setCookie('_lr_env_src_ats', 'true', now.toUTCString()); + storage.setCookie('_lr_sampling_rate', '10', now.toUTCString()); let initOptions = { - pid: '10433394', - host: 'https://example.com/dev', + pid: '10433394' }; let auctionTimestamp = 1496510254326; @@ -74,13 +85,15 @@ describe('ats analytics adapter', function () { let expectedAfterBid = { 'Data': [{ 'has_envelope': true, + 'adapter_version': 1, 'bidder': 'appnexus', 'bid_id': '30c77d079cdf17', 'auction_id': 'a5b849e5-87d7-4205-8300-d063084fcfb7', - 'user_browser': checkUserBrowser(), + 'user_browser': parseBrowser(), 'user_platform': navigator.platform, 'auction_start': '2020-02-03T14:14:25.161Z', 'domain': window.location.hostname, + 'envelope_source': true, 'pid': '10433394', 'response_time_stamp': '2020-02-03T14:23:11.978Z', 'currency': 'USD', @@ -132,76 +145,37 @@ describe('ats analytics adapter', function () { events.emit(constants.EVENTS.AUCTION_END, {}); let requests = server.requests.filter(req => { - return req.url.indexOf(initOptions.host) > -1; + return req.url.indexOf(analyticsUrl) > -1; }); + expect(requests.length).to.equal(1); let realAfterBid = JSON.parse(requests[0].requestBody); - // Step 6: assert real data after bid and expected data expect(realAfterBid['Data']).to.deep.equal(expectedAfterBid['Data']); - // check that the host and publisher ID is configured via options - expect(atsAnalyticsAdapter.context.host).to.equal(initOptions.host); + // check that the publisher ID is configured via options expect(atsAnalyticsAdapter.context.pid).to.equal(initOptions.pid); }) - it('check browser is not safari', function () { - window.safari = undefined; - let browser = browserIsSafari(); - expect(browser).to.equal(false); - }) it('check browser is safari', function () { - window.safari = {}; - let browser = browserIsSafari(); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'); + let browser = parseBrowser(); expect(browser).to.equal('Safari'); }) - it('check browser is not chrome', function () { - window.chrome = { - app: undefined, - webstore: undefined, - runtime: undefined - }; - let browser = browserIsChrome(); - expect(browser).to.equal(false); - }) it('check browser is chrome', function () { - window.chrome = { - app: {}, - webstore: {}, - runtime: {} - }; - let browser = browserIsChrome(); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/80.0.3987.95 Mobile/15E148 Safari/604.1'); + let browser = parseBrowser(); expect(browser).to.equal('Chrome'); }) it('check browser is edge', function () { - Object.defineProperty(window, 'StyleMedia', { - value: {}, - writable: true - }); - Object.defineProperty(document, 'documentMode', { - value: undefined, - writable: true - }); - let browser = browserIsEdge(); - expect(browser).to.equal('Edge'); - }) - it('check browser is not edge', function () { - Object.defineProperty(document, 'documentMode', { - value: {}, - writable: true - }); - let browser = browserIsEdge(); - expect(browser).to.equal(false); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43'); + let browser = parseBrowser(); + expect(browser).to.equal('Microsoft Edge'); }) it('check browser is firefox', function () { - global.InstallTrigger = {}; - let browser = browserIsFirefox(); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (iPhone; CPU OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/23.0 Mobile/15E148 Safari/605.1.15'); + let browser = parseBrowser(); expect(browser).to.equal('Firefox'); }) - it('check browser is not firefox', function () { - global.InstallTrigger = undefined; - let browser = browserIsFirefox(); - expect(browser).to.equal(false); - }) }) })