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

New Analytics Adapter: Implement Appier Analytics Adapter. #3871

Merged
merged 1 commit into from
Jun 10, 2019
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
244 changes: 244 additions & 0 deletions modules/appierAnalyticsAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import {ajax} from '../src/ajax';
import adapter from '../src/AnalyticsAdapter';
import CONSTANTS from '../src/constants.json';
import adapterManager from '../src/adapterManager';
import {logError, logInfo, deepClone} from '../src/utils';

const analyticsType = 'endpoint';

export const ANALYTICS_VERSION = '1.0.0';

const DEFAULT_SERVER = 'https://prebid-analytics.c.appier.net/v1';

const {
EVENTS: {
AUCTION_END,
BID_WON,
BID_TIMEOUT
}
} = CONSTANTS;

export const BIDDER_STATUS = {
BID: 'bid',
NO_BID: 'noBid',
BID_WON: 'bidWon',
TIMEOUT: 'timeout'
};

export const getCpmInUsd = function (bid) {
if (bid.currency === 'USD') {
return bid.cpm;
} else {
return bid.getCpmInNewCurrency('USD');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm - this property getCpmInNewCurrency is only populated when the publisher has the currency module activated. Are you expecting the currency module to be active as a requirement for using your analytics adapter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, currency module is required. I have added this reminder in README.md, thank you :)

}
};

const analyticsOptions = {};

export const parseBidderCode = function (bid) {
let bidderCode = bid.bidderCode || bid.bidder;
return bidderCode.toLowerCase();
};

export const parseAdUnitCode = function (bidResponse) {
return bidResponse.adUnitCode.toLowerCase();
};

export const appierAnalyticsAdapter = Object.assign(adapter({DEFAULT_SERVER, analyticsType}), {

cachedAuctions: {},

initConfig(config) {
/**
* Required option: affiliateId
* Required option: configId
*
* Optional option: server
* Optional option: sampling
* Optional option: adSampling
* Optional option: autoPick
* Optional option: predictionId
* @type {boolean}
*/
analyticsOptions.options = deepClone(config.options);
if (typeof config.options.affiliateId !== 'string' || config.options.affiliateId.length < 1) {
logError('"options.affiliateId" is required.');
return false;
}
if (typeof config.options.configId !== 'string' || config.options.configId.length < 1) {
logError('"options.configId" is required.');
return false;
}

analyticsOptions.affiliateId = config.options.affiliateId;
analyticsOptions.configId = config.options.configId;
analyticsOptions.server = config.options.server || DEFAULT_SERVER;

analyticsOptions.sampled = true;
if (typeof config.options.sampling === 'number') {
analyticsOptions.sampled = Math.random() < parseFloat(config.options.sampling);
}
analyticsOptions.adSampled = false;
if (typeof config.options.adSampling === 'number') {
analyticsOptions.adSampled = Math.random() < parseFloat(config.options.adSampling);
}
analyticsOptions.autoPick = config.options.autoPick || null;
analyticsOptions.predictionId = config.options.predictionId || null;

return true;
},
sendEventMessage(endPoint, data) {
logInfo(`AJAX: ${endPoint}: ` + JSON.stringify(data));

ajax(`${analyticsOptions.server}/${endPoint}`, null, JSON.stringify(data), {
contentType: 'application/json',
withCredentials: true
});
},
createCommonMessage(auctionId) {
return {
version: ANALYTICS_VERSION,
auctionId: auctionId,
affiliateId: analyticsOptions.affiliateId,
configId: analyticsOptions.configId,
referrer: window.location.href,
sampling: analyticsOptions.options.sampling,
adSampling: analyticsOptions.options.adSampling,
prebid: '$prebid.version$',
autoPick: analyticsOptions.autoPick,
predictionId: analyticsOptions.predictionId,
adUnits: {},
};
},
serializeBidResponse(bid, status) {
const result = {
prebidWon: (status === BIDDER_STATUS.BID_WON),
isTimeout: (status === BIDDER_STATUS.TIMEOUT),
status: status,
};
if (status === BIDDER_STATUS.BID || status === BIDDER_STATUS.BID_WON) {
Object.assign(result, {
time: bid.timeToRespond,
cpm: bid.cpm,
currency: bid.currency,
originalCpm: bid.originalCpm || bid.cpm,
cpmUsd: getCpmInUsd(bid),
originalCurrency: bid.originalCurrency || bid.currency,
});
}
return result;
},
addBidResponseToMessage(message, bid, status) {
const adUnitCode = parseAdUnitCode(bid);
message.adUnits[adUnitCode] = message.adUnits[adUnitCode] || {};
const bidder = parseBidderCode(bid);
const bidResponse = this.serializeBidResponse(bid, status);
message.adUnits[adUnitCode][bidder] = bidResponse;
},
createBidMessage(auctionEndArgs, winningBids, timeoutBids) {
const {auctionId, timestamp, timeout, auctionEnd, adUnitCodes, bidsReceived, noBids} = auctionEndArgs;
const message = this.createCommonMessage(auctionId);

message.auctionElapsed = (auctionEnd - timestamp);
message.timeout = timeout;

adUnitCodes.forEach((adUnitCode) => {
message.adUnits[adUnitCode] = {};
});

// We handled noBids first because when currency conversion is enabled, a bid with a foreign currency
// will be set to NO_BID initially, and then set to BID after the currency rate json file is fully loaded.
// In this situation, the bid exists in both noBids and bids arrays.
noBids.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.NO_BID));

// This array may contain some timeout bids (responses come back after auction timeout)
bidsReceived.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.BID));

// We handle timeout after bids since it's possible that a bid has a response, but the response comes back
// after auction end. In this case, the bid exists in both bidsReceived and timeoutBids arrays.
timeoutBids.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.TIMEOUT));

// mark the winning bids with prebidWon = true
winningBids.forEach(bid => {
const adUnitCode = parseAdUnitCode(bid);
const bidder = parseBidderCode(bid);
message.adUnits[adUnitCode][bidder].prebidWon = true;
});
return message;
},
createImpressionMessage(bid) {
const message = this.createCommonMessage(bid.auctionId);
this.addBidResponseToMessage(message, bid, BIDDER_STATUS.BID_WON);
return message;
},
createCreativeMessage(auctionId, bids) {
const message = this.createCommonMessage(auctionId);
bids.forEach((bid) => {
const adUnitCode = parseAdUnitCode(bid);
const bidder = parseBidderCode(bid);
message.adUnits[adUnitCode] = message.adUnits[adUnitCode] || {};
message.adUnits[adUnitCode][bidder] = {ad: bid.ad};
});
return message;
},
getCachedAuction(auctionId) {
this.cachedAuctions[auctionId] = this.cachedAuctions[auctionId] || {
timeoutBids: [],
};
return this.cachedAuctions[auctionId];
},
handleAuctionEnd(auctionEndArgs) {
const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId);
const highestCpmBids = pbjs.getHighestCpmBids();
this.sendEventMessage('bid',
this.createBidMessage(auctionEndArgs, highestCpmBids, cachedAuction.timeoutBids)
);
if (analyticsOptions.adSampled) {
this.sendEventMessage('cr',
this.createCreativeMessage(auctionEndArgs.auctionId, auctionEndArgs.bidsReceived)
);
}
},
handleBidTimeout(timeoutBids) {
timeoutBids.forEach((bid) => {
const cachedAuction = this.getCachedAuction(bid.auctionId);
cachedAuction.timeoutBids.push(bid);
});
},
handleBidWon(bidWonArgs) {
this.sendEventMessage('imp', this.createImpressionMessage(bidWonArgs));
},
track({eventType, args}) {
if (analyticsOptions.sampled) {
switch (eventType) {
case BID_WON:
this.handleBidWon(args);
break;
case BID_TIMEOUT:
this.handleBidTimeout(args);
break;
case AUCTION_END:
this.handleAuctionEnd(args);
break;
}
}
},
getAnalyticsOptions() {
return analyticsOptions;
},
});

// save the base class function
appierAnalyticsAdapter.originEnableAnalytics = appierAnalyticsAdapter.enableAnalytics;

// override enableAnalytics so we can get access to the config passed in from the page
appierAnalyticsAdapter.enableAnalytics = function (config) {
if (this.initConfig(config)) {
appierAnalyticsAdapter.originEnableAnalytics(config); // call the base class function
}
};

adapterManager.registerAnalyticsAdapter({
adapter: appierAnalyticsAdapter,
code: 'appierAnalytics'
});
23 changes: 23 additions & 0 deletions modules/appierAnalyticsAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Overview

Module Name: Appier Analytics Adapter
Module Type: Analytics Adapter
Maintainer: apn-dev@appier.com

# Description

Analytics adapter for Appier

# Test Parameters

```
{
provider: 'appierAnalytics',
options: {
'configId': 'YOUR_CONFIG_ID',
'affiliateId': 'YOUR_AFFILIATE_ID',
}
}
```

PS. [Prebid currency module](http://prebid.org/dev-docs/modules/currency.html) is required, please make sure your prebid code contains currency module code.
Loading