Skip to content

Commit

Permalink
Rubicon Analytics Adapter: pass along billing events (#8182)
Browse files Browse the repository at this point in the history
* rubicon listens to billing events

* billingId is required

* remove log
  • Loading branch information
robertrmartinez authored Mar 15, 2022
1 parent cf6176b commit 655585c
Show file tree
Hide file tree
Showing 3 changed files with 422 additions and 38 deletions.
155 changes: 121 additions & 34 deletions modules/rubiconAnalyticsAdapter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, isGptPubadsDefined, _each, deepSetValue } from '../src/utils.js';
import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, isGptPubadsDefined, _each, deepSetValue, deepClone, logInfo } from '../src/utils.js';
import adapter from '../src/AnalyticsAdapter.js';
import adapterManager from '../src/adapterManager.js';
import CONSTANTS from '../src/constants.json';
Expand All @@ -12,6 +12,7 @@ export const storage = getStorageManager({gvlid: RUBICON_GVL_ID, moduleName: 'ru
const COOKIE_NAME = 'rpaSession';
const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins
const END_EXPIRE_TIME = 21600000; // 6 hours
const MODULE_NAME = 'Rubicon Analytics';

const pbsErrorMap = {
1: 'timeout-error',
Expand All @@ -31,7 +32,8 @@ const {
BIDDER_DONE,
BID_TIMEOUT,
BID_WON,
SET_TARGETING
SET_TARGETING,
BILLABLE_EVENT
},
STATUS: {
GOOD,
Expand All @@ -55,13 +57,19 @@ const cache = {
targeting: {},
timeouts: {},
gpt: {},
billing: {}
};

const BID_REJECTED_IPF = 'rejected-ipf';

export let rubiConf = {
pvid: generateUUID().slice(0, 8),
analyticsEventDelay: 0
analyticsEventDelay: 0,
dmBilling: {
enabled: false,
vendors: [],
waitForAuction: true
}
};
// we are saving these as global to this module so that if a pub accidentally overwrites the entire
// rubicon object, then we do not lose other data
Expand All @@ -76,7 +84,7 @@ export function getHostNameFromReferer(referer) {
try {
rubiconAdapter.referrerHostname = parseUrl(referer, { noDecodeWholeURL: true }).hostname;
} catch (e) {
logError('Rubicon Analytics: Unable to parse hostname from supplied url: ', referer, e);
logError(`${MODULE_NAME}: Unable to parse hostname from supplied url: `, referer, e);
rubiconAdapter.referrerHostname = '';
}
return rubiconAdapter.referrerHostname
Expand Down Expand Up @@ -115,6 +123,55 @@ function formatSource(src) {
return src.toLowerCase();
}

function getBillingPayload(event) {
// for now we are mapping all events to type "general", later we will expand support for specific types
let billingEvent = deepClone(event);
billingEvent.type = 'general';
billingEvent.accountId = accountId;
// mark as sent
deepSetValue(cache.billing, `${event.vendor}.${event.billingId}`, true);
return billingEvent;
}

function sendBillingEvent(event) {
let message = getBasicEventDetails(undefined, 'soloBilling');
message.billableEvents = [getBillingPayload(event)];
ajax(
rubiconAdapter.getUrl(),
null,
JSON.stringify(message),
{
contentType: 'application/json'
}
);
}

function getBasicEventDetails(auctionId, trigger) {
let auctionCache = cache.auctions[auctionId];
let referrer = config.getConfig('pageUrl') || pageReferer || (auctionCache && auctionCache.referrer);
let message = {
timestamps: {
prebidLoaded: rubiconAdapter.MODULE_INITIALIZED_TIME,
auctionEnded: auctionCache ? auctionCache.endTs : undefined,
eventTime: Date.now()
},
trigger,
integration: rubiConf.int_type || DEFAULT_INTEGRATION,
version: '$prebid.version$',
referrerUri: referrer,
referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer),
channel: 'web',
};
if (rubiConf.wrapperName) {
message.wrapper = {
name: rubiConf.wrapperName,
family: rubiConf.wrapperFamily,
rule: rubiConf.rule_name
}
}
return message;
}

function sendMessage(auctionId, bidWonId, trigger) {
function formatBid(bid) {
return pick(bid, [
Expand Down Expand Up @@ -160,28 +217,8 @@ function sendMessage(auctionId, bidWonId, trigger) {
samplingFactor
});
}
let message = getBasicEventDetails(auctionId, trigger);
let auctionCache = cache.auctions[auctionId];
let referrer = config.getConfig('pageUrl') || (auctionCache && auctionCache.referrer);
let message = {
timestamps: {
prebidLoaded: rubiconAdapter.MODULE_INITIALIZED_TIME,
auctionEnded: auctionCache.endTs,
eventTime: Date.now()
},
trigger,
integration: rubiConf.int_type || DEFAULT_INTEGRATION,
version: '$prebid.version$',
referrerUri: referrer,
referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer),
channel: 'web',
};
if (rubiConf.wrapperName) {
message.wrapper = {
name: rubiConf.wrapperName,
family: rubiConf.wrapperFamily,
rule: rubiConf.rule_name
}
}
if (auctionCache && !auctionCache.sent) {
let adUnitMap = Object.keys(auctionCache.bids).reduce((adUnits, bidId) => {
let bid = auctionCache.bids[bidId];
Expand Down Expand Up @@ -321,6 +358,12 @@ function sendMessage(auctionId, bidWonId, trigger) {
];
}

// if we have not sent any billingEvents send them
const pendingBillingEvents = getPendingBillingEvents(auctionCache);
if (pendingBillingEvents && pendingBillingEvents.length) {
message.billableEvents = pendingBillingEvents;
}

ajax(
this.getUrl(),
null,
Expand All @@ -331,6 +374,17 @@ function sendMessage(auctionId, bidWonId, trigger) {
);
}

function getPendingBillingEvents(auctionCache) {
if (auctionCache && auctionCache.billing && auctionCache.billing.length) {
return auctionCache.billing.reduce((accum, billingEvent) => {
if (deepAccess(cache.billing, `${billingEvent.vendor}.${billingEvent.billingId}`) === false) {
accum.push(getBillingPayload(billingEvent));
}
return accum;
}, []);
}
}

function adUnitIsOnlyInstream(adUnit) {
return adUnit.mediaTypes && Object.keys(adUnit.mediaTypes).length === 1 && deepAccess(adUnit, 'mediaTypes.video.context') === 'instream';
}
Expand Down Expand Up @@ -359,7 +413,7 @@ function getBidPrice(bid) {
try {
return Number(prebidGlobal.convertCurrency(cpm, currency, 'USD'));
} catch (err) {
logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid);
logWarn(`${MODULE_NAME}: Could not determine the bidPriceUSD of the bid `, bid);
}
}

Expand Down Expand Up @@ -449,7 +503,7 @@ function getRpaCookie() {
try {
return JSON.parse(window.atob(encodedCookie));
} catch (e) {
logError(`Rubicon Analytics: Unable to decode ${COOKIE_NAME} value: `, e);
logError(`${MODULE_NAME}: Unable to decode ${COOKIE_NAME} value: `, e);
}
}
return {};
Expand All @@ -459,7 +513,7 @@ function setRpaCookie(decodedCookie) {
try {
storage.setDataInLocalStorage(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie)));
} catch (e) {
logError(`Rubicon Analytics: Unable to encode ${COOKIE_NAME} value: `, e);
logError(`${MODULE_NAME}: Unable to encode ${COOKIE_NAME} value: `, e);
}
}

Expand Down Expand Up @@ -524,6 +578,30 @@ function subscribeToGamSlots() {
});
}

let pageReferer;

const isBillingEventValid = event => {
// vendor is whitelisted
const isWhitelistedVendor = rubiConf.dmBilling.vendors.includes(event.vendor);
// event is not duplicated
const isNotDuplicate = typeof deepAccess(cache.billing, `${event.vendor}.${event.billingId}`) !== 'boolean';
// billingId is defined and a string
return typeof event.billingId === 'string' && isWhitelistedVendor && isNotDuplicate;
}

const sendOrAddEventToQueue = event => {
// if any auction is not sent yet, then add it to the auction queue
const pendingAuction = Object.keys(cache.auctions).find(auctionId => !cache.auctions[auctionId].sent);

if (rubiConf.dmBilling.waitForAuction && pendingAuction) {
cache.auctions[pendingAuction].billing = cache.auctions[pendingAuction].billing || [];
cache.auctions[pendingAuction].billing.push(event);
} else {
// send it
sendBillingEvent(event);
}
}

let baseAdapter = adapter({ analyticsType: 'endpoint' });
let rubiconAdapter = Object.assign({}, baseAdapter, {
MODULE_INITIALIZED_TIME: Date.now(),
Expand All @@ -539,15 +617,15 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
if (config.options.endpoint) {
this.getUrl = () => config.options.endpoint;
} else {
logError('required endpoint missing from rubicon analytics');
logError(`${MODULE_NAME}: required endpoint missing`);
error = true;
}
if (typeof config.options.sampling !== 'undefined') {
samplingFactor = 1 / parseFloat(config.options.sampling);
}
if (typeof config.options.samplingFactor !== 'undefined') {
if (typeof config.options.sampling !== 'undefined') {
logWarn('Both options.samplingFactor and options.sampling enabled in rubicon analytics, defaulting to samplingFactor');
logWarn(`${MODULE_NAME}: Both options.samplingFactor and options.sampling enabled defaulting to samplingFactor`);
}
samplingFactor = parseFloat(config.options.samplingFactor);
config.options.sampling = 1 / samplingFactor;
Expand All @@ -557,10 +635,10 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
let validSamplingFactors = [1, 10, 20, 40, 100];
if (validSamplingFactors.indexOf(samplingFactor) === -1) {
error = true;
logError('invalid samplingFactor for rubicon analytics: ' + samplingFactor + ', must be one of ' + validSamplingFactors.join(', '));
logError(`${MODULE_NAME}: invalid samplingFactor ${samplingFactor} - must be one of ${validSamplingFactors.join(', ')}`);
} else if (!accountId) {
error = true;
logError('required accountId missing for rubicon analytics');
logError(`${MODULE_NAME}: required accountId missing for rubicon analytics`);
}

if (!error) {
Expand All @@ -572,6 +650,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
accountId = undefined;
rubiConf = {};
cache.gpt.registered = false;
cache.billing = {};
baseAdapter.disableAnalytics.apply(this, arguments);
},
track({ eventType, args }) {
Expand All @@ -586,8 +665,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
cacheEntry.bids = {};
cacheEntry.bidsWon = {};
cacheEntry.gamHasRendered = {};
cacheEntry.referrer = pageReferer = deepAccess(args, 'bidderRequests.0.refererInfo.referer');
cacheEntry.bidderOrder = [];
cacheEntry.referrer = deepAccess(args, 'bidderRequests.0.refererInfo.referer');
const floorData = deepAccess(args, 'bidderRequests.0.bids.0.floorData');
if (floorData) {
cacheEntry.floorData = { ...floorData };
Expand Down Expand Up @@ -716,7 +795,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
auctionEntry.floorData.enforcements = { ...args.floorData.enforcements };
}
if (!bid) {
logError('Rubicon Anlytics Adapter Error: Could not find associated bid request for bid response with requestId: ', args.requestId);
logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId);
break;
}
bid.source = formatSource(bid.source || args.source);
Expand Down Expand Up @@ -814,6 +893,14 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
}
});
break;
case BILLABLE_EVENT:
if (rubiConf.dmBilling.enabled && isBillingEventValid(args)) {
// add to the map indicating it has not been sent yet
deepSetValue(cache.billing, `${args.vendor}.${args.billingId}`, false);
sendOrAddEventToQueue(args);
} else {
logInfo(`${MODULE_NAME}: Billing event ignored`, args);
}
}
}
});
Expand Down
Loading

0 comments on commit 655585c

Please sign in to comment.