diff --git a/modules/pubwiseAnalyticsAdapter.js b/modules/pubwiseAnalyticsAdapter.js index d3e596d91eb..179115d7712 100644 --- a/modules/pubwiseAnalyticsAdapter.js +++ b/modules/pubwiseAnalyticsAdapter.js @@ -18,24 +18,41 @@ const utils = require('../src/utils'); endpoint: 'https://api.pubwise.io/api/v4/event/add/', } }); - */ +*/ const analyticsType = 'endpoint'; -const analyticsName = 'PubWise Analytics: '; -let defaultUrl = 'https://api.pubwise.io/api/v4/event/default/'; -let pubwiseVersion = '3.0'; -let pubwiseSchema = 'AVOCET'; -let configOptions = {site: '', endpoint: 'https://api.pubwise.io/api/v4/event/default/', debug: ''}; +const analyticsName = 'PubWise:'; +let defaultUrl = 'https://api.pubwise.io/api/v5/event/default/'; +let pubwiseVersion = '3.0.90'; +let configOptions = {site: '', endpoint: defaultUrl, debug: ''}; let pwAnalyticsEnabled = false; let utmKeys = {utm_source: '', utm_medium: '', utm_campaign: '', utm_term: '', utm_content: ''}; +let sessionData = {sessionId: ''}; +let pwNamespace = 'pubwise'; +let pwEvents = []; +let metaData = {}; +let auctionEnded = false; +let sessTimeout = 60 * 30 * 1000; // 30 minutes, G Analytics default session length +let sessName = 'sess_id'; +let sessTimeoutName = 'sess_timeout'; -function markEnabled() { - utils.logInfo(`${analyticsName}Enabled`, configOptions); - pwAnalyticsEnabled = true; +function enrichWithSessionInfo(dataBag) { + try { + dataBag['session_id'] = sessionData.sessId; + } catch (e) { + dataBag['error_sess'] = 1; + } + + return dataBag; } function enrichWithMetrics(dataBag) { try { + if (typeof PREBID_TIMEOUT !== 'undefined') { + dataBag['target_timeout'] = PREBID_TIMEOUT; + } else { + dataBag['target_timeout'] = 'NA'; + } dataBag['pw_version'] = pubwiseVersion; dataBag['pbjs_version'] = $$PREBID_GLOBAL$$.version; dataBag['debug'] = configOptions.debug; @@ -51,7 +68,7 @@ function enrichWithUTM(dataBag) { try { for (let prop in utmKeys) { utmKeys[prop] = utils.getParameterByName(prop); - if (utmKeys[prop] != '') { + if (utmKeys[prop]) { newUtm = true; dataBag[prop] = utmKeys[prop]; } @@ -59,64 +76,260 @@ function enrichWithUTM(dataBag) { if (newUtm === false) { for (let prop in utmKeys) { - let itemValue = localStorage.getItem(`pw-${prop}`); - if (itemValue.length !== 0) { + let itemValue = localStorage.getItem(setNamespace(prop)); + if (itemValue !== null && typeof itemValue !== 'undefined' && itemValue.length !== 0) { dataBag[prop] = itemValue; } } } else { for (let prop in utmKeys) { - localStorage.setItem(`pw-${prop}`, utmKeys[prop]); + localStorage.setItem(setNamespace(prop), utmKeys[prop]); } } } catch (e) { - utils.logInfo(`${analyticsName}Error`, e); + pwInfo(`Error`, e); dataBag['error_utm'] = 1; } return dataBag; } -function sendEvent(eventType, data) { - utils.logInfo(`${analyticsName}Event ${eventType} ${pwAnalyticsEnabled}`, data); +function expireUtmData() { + pwInfo(`Session Expiring UTM Data`); + for (let prop in utmKeys) { + localStorage.removeItem(setNamespace(prop)); + } +} - // put the typical items in the data bag - let dataBag = { - eventType: eventType, - args: data, - target_site: configOptions.site, - pubwiseSchema: pubwiseSchema, - debug: configOptions.debug ? 1 : 0, - }; +function enrichWithCustomSegments(dataBag) { + // c_site: '', c_script_type: '', c_slot1: '', c_slot2: '', c_slot3: '', c_slot4: '' + if (configOptions.custom) { + if (configOptions.custom.c_script_type) { + dataBag['c_script_type'] = configOptions.custom.c_script_type; + } - dataBag = enrichWithMetrics(dataBag); - // for certain events, track additional info - if (eventType == CONSTANTS.EVENTS.AUCTION_INIT) { - dataBag = enrichWithUTM(dataBag); + if (configOptions.custom.c_host) { + dataBag['c_host'] = configOptions.custom.c_host; + } + + if (configOptions.custom.c_slot1) { + dataBag['c_slot1'] = configOptions.custom.c_slot1; + } + + if (configOptions.custom.c_slot2) { + dataBag['c_slot2'] = configOptions.custom.c_slot2; + } + + if (configOptions.custom.c_slot3) { + dataBag['c_slot3'] = configOptions.custom.c_slot3; + } + + if (configOptions.custom.c_slot4) { + dataBag['c_slot4'] = configOptions.custom.c_slot4; + } } - ajax(configOptions.endpoint, (result) => utils.logInfo(`${analyticsName}Result`, result), JSON.stringify(dataBag)); + return dataBag; +} + +function setNamespace(itemText) { + return pwNamespace.concat('_' + itemText); +} + +function localStorageSessTimeoutName() { + return setNamespace(sessTimeoutName); +} + +function localStorageSessName() { + return setNamespace(sessName); +} + +function extendUserSessionTimeout() { + localStorage.setItem(localStorageSessTimeoutName(), Date.now().toString()); +} + +function userSessionID() { + return localStorage.getItem(localStorageSessName()) ? localStorage.getItem(localStorageSessName()) : ''; +} + +function sessionExpired() { + let sessLastTime = localStorage.getItem(localStorageSessTimeoutName()); + return (Date.now() - parseInt(sessLastTime)) > sessTimeout; +} + +function flushEvents() { + if (pwEvents.length > 0) { + let dataBag = {metaData: metaData, eventList: pwEvents.splice(0)}; // put all the events together with the metadata and send + ajax(configOptions.endpoint, (result) => pwInfo(`Result`, result), JSON.stringify(dataBag)); + } +} + +function isIngestedEvent(eventType) { + const ingested = [CONSTANTS.EVENTS.AUCTION_INIT, CONSTANTS.EVENTS.BID_REQUESTED, CONSTANTS.EVENTS.BID_RESPONSE, CONSTANTS.EVENTS.BID_WON, CONSTANTS.EVENTS.BID_TIMEOUT]; + return ingested.includes(eventType); +} + +function markEnabled() { + pwInfo(`Enabled`, configOptions); + pwAnalyticsEnabled = true; + setInterval(flushEvents, 400); +} + +function pwInfo(info, context) { + utils.logInfo(`${analyticsName} ` + info, context); } -let pubwiseAnalytics = Object.assign(adapter( - { - defaultUrl, - analyticsType - }), -{ +/* + // unused currently + function filterNoBid(data) { + let newNoBidData = {}; + + newNoBidData.auctionId = data.auctionId; + newNoBidData.bidId = data.bidId; + newNoBidData.bidderRequestId = data.bidderRequestId; + newNoBidData.transactionId = data.transactionId; + + return newNoBidData; + } +*/ + +function filterBidResponse(data) { + let modified = Object.assign({}, data); + // clean up some properties we don't track in public version + if (typeof modified.ad !== 'undefined') { + modified.ad = ''; + } + if (typeof modified.adUrl !== 'undefined') { + modified.adUrl = ''; + } + if (typeof modified.adserverTargeting !== 'undefined') { + modified.adserverTargeting = ''; + } + if (typeof modified.ts !== 'undefined') { + modified.ts = ''; + } + // clean up a property to make simpler + if (typeof modified.statusMessage !== 'undefined' && modified.statusMessage === 'Bid returned empty or error response') { + modified.statusMessage = 'eoe'; + } + modified.auctionEnded = auctionEnded; + return modified; +} + +function filterAuctionInit(data) { + let modified = Object.assign({}, data); + + modified.refererInfo = {}; + // handle clean referrer, we only need one + if (typeof modified.bidderRequests !== 'undefined') { + if (typeof modified.bidderRequests[0] !== 'undefined') { + if (typeof modified.bidderRequests[0].refererInfo !== 'undefined') { + modified.refererInfo = modified.bidderRequests[0].refererInfo; + } + } + } + + if (typeof modified.adUnitCodes !== 'undefined') { + delete modified.adUnitCodes; + } + if (typeof modified.adUnits !== 'undefined') { + delete modified.adUnits; + } + if (typeof modified.bidderRequests !== 'undefined') { + delete modified.bidderRequests; + } + if (typeof modified.bidsReceived !== 'undefined') { + delete modified.bidsReceived; + } + if (typeof modified.config !== 'undefined') { + delete modified.config; + } + if (typeof modified.noBids !== 'undefined') { + delete modified.noBids; + } + if (typeof modified.winningBids !== 'undefined') { + delete modified.winningBids; + } + + return modified; +} + +let pubwiseAnalytics = Object.assign(adapter({defaultUrl, analyticsType}), { // Override AnalyticsAdapter functions by supplying custom methods track({eventType, args}) { - sendEvent(eventType, args); + this.handleEvent(eventType, args); } }); +pubwiseAnalytics.handleEvent = function(eventType, data) { + // we log most events, but some are information + if (isIngestedEvent(eventType)) { + pwInfo(`Emitting Event ${eventType} ${pwAnalyticsEnabled}`, data); + + // record metadata + metaData = { + target_site: configOptions.site, + debug: configOptions.debug ? 1 : 0, + }; + metaData = enrichWithSessionInfo(metaData); + metaData = enrichWithMetrics(metaData); + metaData = enrichWithUTM(metaData); + metaData = enrichWithCustomSegments(metaData); + + // add data on init to the metadata container + if (eventType === CONSTANTS.EVENTS.AUCTION_INIT) { + data = filterAuctionInit(data); + } else if (eventType === CONSTANTS.EVENTS.BID_RESPONSE) { + data = filterBidResponse(data); + } + + // add all ingested events + pwEvents.push({ + eventType: eventType, + args: data + }); + } else { + pwInfo(`Skipping Event ${eventType} ${pwAnalyticsEnabled}`, data); + } + + // if (eventType == CONSTANTS.EVENTS.AUCTION_END) { + // auctionEnded = true; + // } + + // if (eventType == CONSTANTS.EVENTS.AUCTION_END || eventType == CONSTANTS.EVENTS.BID_WON || auctionEnded === true) { + // // we consider auction_end to to be the end of the auction + // flushEvents(); + // } + + // once the auction ends, or the event is a bid won send events + if (eventType === CONSTANTS.EVENTS.AUCTION_END || eventType === CONSTANTS.EVENTS.BID_WON) { + flushEvents(); + } +} + +pubwiseAnalytics.storeSessionID = function (userSessID) { + localStorage.setItem(localStorageSessName(), userSessID); + pwInfo(`New Session Generated`, userSessID); +}; + +// ensure a session exists, if not make one, always store it +pubwiseAnalytics.ensureSession = function () { + if (sessionExpired() === true || userSessionID() === null || userSessionID() === '') { + expireUtmData(); + this.storeSessionID(utils.generateUUID()); + } + extendUserSessionTimeout(); + sessionData.sessId = userSessionID(); +}; + pubwiseAnalytics.adapterEnableAnalytics = pubwiseAnalytics.enableAnalytics; pubwiseAnalytics.enableAnalytics = function (config) { - if (config.options.debug === undefined) { - config.options.debug = utils.debugTurnedOn(); - } configOptions = config.options; + if (configOptions.debug === undefined) { + configOptions.debug = utils.debugTurnedOn(); + } markEnabled(); + this.ensureSession(); pubwiseAnalytics.adapterEnableAnalytics(config); }; diff --git a/package-lock.json b/package-lock.json index 1d224cebc76..448c3234c66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,7 @@ { "name": "prebid.js", - "version": "2.21.0-pre", + "version": "2.24.0", "lockfileVersion": 1, - "requires": true, "dependencies": { "@babel/code-frame": { "version": "7.0.0", @@ -856,16 +855,6 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -5436,6 +5425,10 @@ } } }, + "eslint-plugin-prebid": { + "version": "file:plugins/eslint", + "dev": true + }, "eslint-plugin-promise": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-3.8.0.tgz", @@ -7697,15 +7690,7 @@ }, "gulp-footer": { "version": "github:prebid/gulp-footer#ff2b46e6376c7f04900357ff9f7b30f219fe5f8a", - "from": "github:prebid/gulp-footer#master", "dev": true, - "requires": { - "event-stream": "3.3.4", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.6.2" - }, "dependencies": { "lodash._reinterpolate": { "version": "3.0.0", @@ -9367,6 +9352,16 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -13739,6 +13734,15 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "dev": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-template": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", @@ -13783,15 +13787,6 @@ "function-bind": "^1.0.2" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "stringify-entities": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz",