Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rubicon Analytics Adapter: pass along billing events #8182

Merged
merged 5 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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