Skip to content

Commit

Permalink
Merge pull request #26 from themaven-net/ADS-4131-send-ad-auction-bid…
Browse files Browse the repository at this point in the history
…s-to-liftigniter

ADS-4131 update reporting format and make adapter detect failure to load LiftIgniter
  • Loading branch information
yonran committed Jun 29, 2020
2 parents 67b19d9 + 90c3e50 commit efd6ddb
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 29 deletions.
120 changes: 98 additions & 22 deletions modules/mavenDistributionAnalyticsAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import CONSTANTS from '../src/constants.json';
import adaptermanager from '../src/adapterManager.js';
import adapter from '../src/AnalyticsAdapter.js';
import { logError } from '../src/utils.js';
import { logError, logInfo, logWarn } from '../src/utils.js';

// Standard Analytics Adapter code
const AUCTION_END = CONSTANTS.EVENTS.AUCTION_END
Expand All @@ -14,6 +14,7 @@ const BATCH_MESSAGE_FREQUENCY = 1000; // Send results batched on a 1s delay

const PROVIDER_CODE = 'mavenDistributionAnalyticsAdapter'
const MAVEN_DISTRIBUTION_GLOBAL = '$p'
const MAX_BATCH_SIZE = 32

/**
* We may add more fields in the future
Expand Down Expand Up @@ -45,10 +46,10 @@ const MAVEN_DISTRIBUTION_GLOBAL = '$p'
*/

/**
* // cpms, zoneIndexes, and zoneNames all have the same length
* // cpmms, zoneIndexes, and zoneNames all have the same length
* @typedef {{
* auc: string
* cpms: number[]
* cpmms: number[]
* zoneIndexes: number[]
* zoneNames: string[]
* }} AuctionEndSummary
Expand All @@ -61,7 +62,7 @@ const MAVEN_DISTRIBUTION_GLOBAL = '$p'
*/
export function summarizeAuctionEnd(args, adapterConfig) {
/** @type {{[code: string]: number}} */
const cpmsMap = {}
const cpmmsMap = {}
const zoneNames = []
const zoneIndexes = []
const adUnitCodes = []
Expand All @@ -70,7 +71,7 @@ export function summarizeAuctionEnd(args, adapterConfig) {
let someZoneNameNonNull = false
let allZoneNamesNonNull = true
args.adUnits.forEach(adUnit => {
cpmsMap[adUnit.code] = 0
cpmmsMap[adUnit.code] = 0
adUnitCodes.push(adUnit.code)

const zoneConfig = zoneMap[adUnit.code] || {}
Expand All @@ -85,14 +86,14 @@ export function summarizeAuctionEnd(args, adapterConfig) {
allZoneNamesNonNull = allZoneNamesNonNull && zoneNameNonNull
})
args.bidsReceived.forEach(bid => {
cpmsMap[bid.adUnitCode] = Math.max(cpmsMap[bid.adUnitCode], bid.cpm || 0)
cpmmsMap[bid.adUnitCode] = Math.max(cpmmsMap[bid.adUnitCode], Math.round(bid.cpm * 1000 || 0))
})
const cpms = args.adUnits.map(adUnit => cpmsMap[adUnit.code])
const cpmms = args.adUnits.map(adUnit => cpmmsMap[adUnit.code])

/** @type {AuctionEndSummary} */
const eventToSend = {
auc: args.auctionId,
cpms: cpms,
cpmms: cpmms,
}
if (!allZoneNamesNonNull) eventToSend.codes = adUnitCodes
if (someZoneNameNonNull) eventToSend.zoneNames = zoneNames
Expand All @@ -101,24 +102,87 @@ export function summarizeAuctionEnd(args, adapterConfig) {
}

/**
* Price is in microdollars
* @param {AuctionEndSummary[]} batch
* @return {{batch: string}}
* @return {{batch: string, price: number}}
*/
export function createSendOptionsFromBatch(batch) {
const batchJson = JSON.stringify(batch)
return { batch: batchJson }
let price = 0
batch.forEach(auctionEndSummary => {
auctionEndSummary.cpmms.forEach(cpmm => price += cpmm)
})
return { batch: batchJson, price: price }
}

const STATE_DOM_CONTENT_LOADING = 'wait-for-$p-to-be-defined'
const STATE_LIFTIGNITER_LOADING = 'waiting-$p(\'onload\')'
const STATE_LIFTIGNITER_LOADED = 'loaded'
/**
* Wrapper around $p that detects when $p is defined
* and then detects when the script is loaded
* so that the caller can discard events if the script never loads
*/
function LiftIgniterWrapper() {
this._state = null
this._onReadyStateChangeBound = this._onReadyStateChange.bind(this)
this._init()
}
LiftIgniterWrapper.prototype = {
_expectState(state) {
if (this._state != state) {
logError(`wrong state ${this._state}; expected ${state}`)
return false
} else {
return true
}
},
_init() {
this._state = STATE_DOM_CONTENT_LOADING
document.addEventListener('DOMContentLoaded', this._onReadyStateChangeBound)
this._onReadyStateChange()
},
_onReadyStateChange() {
if (!this._expectState(STATE_DOM_CONTENT_LOADING)) return
const found = window[MAVEN_DISTRIBUTION_GLOBAL] != null
logInfo(`wrap$p: onReadyStateChange; $p found = ${found}`)
if (found) {
document.removeEventListener('DOMContentLoaded', this._onReadyStateChangeBound)
this._state = STATE_LIFTIGNITER_LOADING
// note: $p('onload', cb) may synchronously call the callback
window[MAVEN_DISTRIBUTION_GLOBAL]('onload', this._onLiftIgniterLoad.bind(this))
}
},
_onLiftIgniterLoad() {
if (!this._expectState(STATE_LIFTIGNITER_LOADING)) return
this._state = STATE_LIFTIGNITER_LOADED
logInfo(`wrap$p: onLiftIgniterLoad`)
},
checkIsLoaded() {
if (this._state === STATE_DOM_CONTENT_LOADING) {
this._onReadyStateChange()
}
return this._state === STATE_LIFTIGNITER_LOADED
},
sendPrebid(obj) {
if (!this._expectState(STATE_LIFTIGNITER_LOADED)) return
window[MAVEN_DISTRIBUTION_GLOBAL]('send', 'prebid', obj)
logInfo(`wrap$p: $p('send')`)
},
}

/**
* @param {MavenDistributionAdapterConfig} adapterConfig
* @property {object[] | null} batch
* @property {number | null} batchTimeout
* @property {MavenDistributionAdapterConfig} adapterConfig
* @property {LiftIgniterWrapper} liftIgniterWrapper
*/
function MavenDistributionAnalyticsAdapterInner(adapterConfig) {
this.batch = null
function MavenDistributionAnalyticsAdapterInner(adapterConfig, liftIgniterWrapper) {
this.batch = []
this.batchTimeout = null
this.adapterConfig = adapterConfig
this.liftIgniterWrapper = liftIgniterWrapper
}
MavenDistributionAnalyticsAdapterInner.prototype = {
/**
Expand All @@ -128,22 +192,28 @@ MavenDistributionAnalyticsAdapterInner.prototype = {
const {eventType, args} = typeAndArgs
if (eventType === AUCTION_END) {
const eventToSend = summarizeAuctionEnd(args, this.adapterConfig)
if (!this.batch) {
this.batch = []
if (this.timeout == null) {
this.timeout = setTimeout(this._sendBatch.bind(this), BATCH_MESSAGE_FREQUENCY)
logInfo(`$p: added auctionEnd to new batch`)
} else {
logInfo(`$p: added auctionEnd to existing batch`)
}
this.batch.push(eventToSend)
}
},

_sendBatch() {
const sendOptions = createSendOptionsFromBatch(this.batch)
if (window[MAVEN_DISTRIBUTION_GLOBAL]) {
window[MAVEN_DISTRIBUTION_GLOBAL]('send', 'prebid', sendOptions)
this.timeout = null
this.batch = null
} else {
this.timeout = setTimeout(this._sendBatch.bind(this), BATCH_MESSAGE_FREQUENCY)
this.timeout = null
const countToDiscard = this.batch.length - MAX_BATCH_SIZE
if (countToDiscard > 0) {
logWarn(`$p: Discarding ${countToDiscard} old items`)
this.batch.splice(0, countToDiscard)
}
if (this.liftIgniterWrapper.checkIsLoaded()) {
logInfo(`$p: Sending ${this.batch.length} items`)
const sendOptions = createSendOptionsFromBatch(this.batch)
this.liftIgniterWrapper.sendPrebid(sendOptions)
this.batch.length = 0
}
},
}
Expand All @@ -169,7 +239,8 @@ MavenDistributionAnalyticsAdapter.prototype = {
if (adapterConfig.options == null) {
return logError(`Adapter ${PROVIDER_CODE}: options null; disabling`)
}
const inner = new MavenDistributionAnalyticsAdapterInner(adapterConfig)
const liftIgniterWrapper = new LiftIgniterWrapper()
const inner = new MavenDistributionAnalyticsAdapterInner(adapterConfig, liftIgniterWrapper)
const base = adapter({global: MAVEN_DISTRIBUTION_GLOBAL})
base.track = inner.track.bind(inner)
base.enableAnalytics(adapterConfig)
Expand All @@ -191,4 +262,9 @@ adaptermanager.registerAnalyticsAdapter({
code: PROVIDER_CODE,
});

// export for unit test
export const testables = {
LiftIgniterWrapper,
}

export default mavenDistributionAnalyticsAdapter;
14 changes: 14 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,23 @@ export function logError() {
}
}

let getTimestamp
if (window.performance && window.performance.now) {
getTimestamp = function getTimestamp() {
// truncate any partial millisecond
return window.performance.now() | 0
}
} else {
const initTime = +new Date()
getTimestamp = function getTimestamp() {
return new Date() - initTime
}
}

function decorateLog(args, prefix) {
args = [].slice.call(args);
prefix && args.unshift(prefix);
args.unshift(getTimestamp())
args.unshift('display: inline-block; color: #fff; background: #3b88c3; padding: 1px 4px; border-radius: 3px;');
args.unshift('%cPrebid');
return args;
Expand Down
101 changes: 94 additions & 7 deletions test/spec/modules/mavenDistributionAnalyticsAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,92 @@
import { createSendOptionsFromBatch, summarizeAuctionEnd } from '../../../modules/mavenDistributionAnalyticsAdapter.js';
import { createSendOptionsFromBatch, summarizeAuctionEnd, testables } from '../../../modules/mavenDistributionAnalyticsAdapter.js';

var assert = require('assert');

describe('MavenDistributionAnalyticsAdapter', function () {
describe('LiftIgniterWrapper', () => {
let addEventListener, removeEventListener
const domContentLoadListeners = []
const que = []
beforeEach(() => {
window.$p = null
addEventListener = sinon.stub(document, 'addEventListener').callsFake((type, fn) => {
if (type !== 'DOMContentLoaded') {
throw Error(`Unsupported type for stub; expected DOMContentLoaded; got ${type}`)
}
domContentLoadListeners.push(fn)
});
removeEventListener = sinon.stub(document, 'removeEventListener').callsFake((type, fn) => {
if (type !== 'DOMContentLoaded') {
throw Error(`Unsupported type for stub; expected DOMContentLoaded; got ${type}`)
}
for (let i = domContentLoadListeners.length; i-- > 0;) {
if (domContentLoadListeners[i] === fn) {
domContentLoadListeners.splice(i, 1)
}
}
});
})
afterEach(() => {
addEventListener.restore()
removeEventListener.restore()
domContentLoadListeners.length = 0
window.$p = null
que.length = 0
})
function defineDollarPStub() {
window.$p = (...args) => que.push(args)
}
function defineDollarPReal() {
window.$p = (name, arg) => {
if (name !== 'onload') {
throw new Error(`Only onload is supported`)
}
arg()
}
for (let i = 0; i < que.length; i++) {
window.$p.apply(window, que[i])
}
que.length = 0
}
it('should check for existence of $p and $p(onload) each time checkIsLoaded is called', () => {
const liftIgniterWrapper = new testables.LiftIgniterWrapper()
assert.equal(liftIgniterWrapper.checkIsLoaded(), false)
assert.equal(liftIgniterWrapper._state, 'wait-for-$p-to-be-defined')
defineDollarPStub()
assert.equal(liftIgniterWrapper.checkIsLoaded(), false)
assert.equal(liftIgniterWrapper._state, 'waiting-$p(\'onload\')')
assert.equal(que.length, 1)
const [firstCommand, cb] = que[0]
assert.equal(firstCommand, 'onload')
cb()
assert.equal(liftIgniterWrapper._state, 'loaded')
assert.equal(liftIgniterWrapper.checkIsLoaded(), true)
})
it('should check $p and $p(onload) when there is a DOMContentLoad event', () => {
assert.equal(window.$p, null)
const liftIgniterWrapper = new testables.LiftIgniterWrapper()
const listener = domContentLoadListeners[0]
assert.notEqual(listener, null)
assert.equal(liftIgniterWrapper._state, 'wait-for-$p-to-be-defined')
defineDollarPStub()

listener()
assert.equal(liftIgniterWrapper._state, 'waiting-$p(\'onload\')')
assert.equal(que.length, 1)
assert.equal(domContentLoadListeners.length, 0, 'should removeListener after $p is defined')
const [firstCommand, cb] = que[0]
assert.equal(firstCommand, 'onload')
cb()
assert.equal(liftIgniterWrapper._state, 'loaded')
assert.equal(liftIgniterWrapper.checkIsLoaded(), true)
})
it('should work with LiftIgniter already loaded (synchronous $p(onload))', () => {
defineDollarPReal()
const liftIgniterWrapper = new testables.LiftIgniterWrapper()
assert.equal(liftIgniterWrapper._state, 'loaded')
assert.equal(liftIgniterWrapper.checkIsLoaded(), true)
})
})
describe('summarizeAuctionEnd', function () {
it('should summarize', () => {
const args = {
Expand Down Expand Up @@ -795,7 +879,7 @@ describe('MavenDistributionAnalyticsAdapter', function () {
const actualSummary = summarizeAuctionEnd(args, adapterConfig)
const expectedSummary = {
'auc': 'e0a2febe-dc05-4999-87ed-4c40022b6796',
cpms: [0],
cpmms: [0],
zoneIndexes: [0],
zoneNames: ['fixed_bottom'],
}
Expand Down Expand Up @@ -2175,21 +2259,24 @@ describe('MavenDistributionAnalyticsAdapter', function () {
const actual = summarizeAuctionEnd(mavenArgs, adapterConfig)
const expected = {
auc: 'd01409e4-580d-4107-8d92-3c5dec19b41a',
cpms: [ 2.604162 ],
cpmms: [ 2604 ],
codes: [ 'gpt-slot-channel-banner-top' ],
}
assert.deepEqual(actual, expected)
})
});
describe('createSendOptionsFromBatch', () => {
it('should create batch json', () => {
const actual = createSendOptionsFromBatch({
const actual = createSendOptionsFromBatch([{
auc: 'aaa',
cpms: [0.04],
cpmms: [40],
zoneIndexes: [3],
zoneNames: ['sidebar']
})
const expected = {batch: '{"auc":"aaa","cpms":[0.04],"zoneIndexes":[3],"zoneNames":["sidebar"]}'}
}])
const expected = {
batch: '[{"auc":"aaa","cpmms":[40],"zoneIndexes":[3],"zoneNames":["sidebar"]}]',
price: 40,
}
assert.deepEqual(actual, expected)
})
})
Expand Down

0 comments on commit efd6ddb

Please sign in to comment.