-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Category translation module for adpod #3513
Changes from 9 commits
e1ddfc4
53a94c4
759018b
ebe6e7e
cbd7df4
d2d8313
37391e4
2b81856
e70ba4e
230e6c1
632f4da
8bebe7c
37695d7
d9a818f
f7f65c2
e334120
e7e485e
d31d8d9
2e8fe49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/** | ||
* This module translates iab category to freewheel industry using translation mapping file | ||
* Publisher can set translation file by using setConfig method | ||
* | ||
* Example: | ||
* config.setConfig({ | ||
* 'brandCategoryTranslation': { | ||
* 'translationFile': 'http://sample.com' | ||
* } | ||
* }); | ||
* If publisher has not defined translation file than prebid will use default prebid translation file provided here <TODO add url once it is uploaded on cdn> | ||
*/ | ||
|
||
import { config } from '../src/config'; | ||
import { hooks, hook } from '../src/hook'; | ||
import { ajax } from '../src/ajax'; | ||
import { timestamp, logError, setDataInLocalStorage, getDataFromLocalStorage } from '../src/utils'; | ||
|
||
// TODO udpate url once it is uploaded on cdn | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO can removed. |
||
const DEFAULT_TRANSLATION_FILE_URL = 'https://api.myjson.com/bins/j5d0k'; | ||
const DEFAULT_IAB_TO_FW_MAPPING_KEY = 'iabToFwMappingkey'; | ||
const DEFAULT_IAB_TO_FW_MAPPING_KEY_PUB = 'iabToFwMappingkeyPub'; | ||
const refreshInDays = 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we increase default to 7? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have kept it 1 because if publisher changes the mapping file, localStorage will be updated in everyone's browser within 24 hours. So per day 1 request. |
||
|
||
let adServerInUse; | ||
export const registerAdserver = hook('async', function(adServer) { | ||
adServerInUse = adServer; | ||
}, 'registerAdserver'); | ||
|
||
export function getAdserverCategoryHook(fn, adUnitCode, bid) { | ||
if (!bid) { | ||
return fn.call(this, adUnitCode); // if no bid, call original and let it display warnings | ||
} | ||
if (!adServerInUse) { | ||
registerAdserver(); | ||
} | ||
|
||
let localStorageKey = (config.getConfig('brandCategoryTranslation.translationFile')) ? DEFAULT_IAB_TO_FW_MAPPING_KEY_PUB : DEFAULT_IAB_TO_FW_MAPPING_KEY; | ||
|
||
if (bid.meta && !bid.meta.adServerCatId) { | ||
let mapping = getDataFromLocalStorage(localStorageKey); | ||
if (mapping) { | ||
try { | ||
mapping = JSON.parse(mapping); | ||
mapping = mapping['data']; | ||
} catch (error) { | ||
logError('Failed to parse translation mapping file'); | ||
} | ||
if (bid.meta) { | ||
bid.meta.adServerCatId = (bid.meta.iabSubCatId && mapping[adServerInUse] && mapping[adServerInUse]['mapping']) ? mapping[adServerInUse]['mapping'][bid.meta.iabSubCatId] : undefined; | ||
} | ||
} else { | ||
logError('Translation mapping data not found in local storage'); | ||
} | ||
} | ||
fn.call(this, adUnitCode, bid); | ||
} | ||
|
||
export function initTranslation(...args) { | ||
hooks['addBidResponse'].before(getAdserverCategoryHook, 50); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is hook add part needs to be checked if it's already been setup. When using the |
||
let url = DEFAULT_TRANSLATION_FILE_URL; | ||
let localStorageKey = DEFAULT_IAB_TO_FW_MAPPING_KEY; | ||
if (args && args.length > 0) { | ||
// use publisher defined translation file | ||
url = args[0]; | ||
localStorageKey = DEFAULT_IAB_TO_FW_MAPPING_KEY_PUB; | ||
} | ||
|
||
let mappingData = getDataFromLocalStorage(localStorageKey); | ||
if (!mappingData || timestamp() < mappingData.lastUpdated + refreshInDays * 24 * 60 * 60 * 1000) { | ||
ajax(url, | ||
{ | ||
success: (response) => { | ||
try { | ||
response = JSON.parse(response); | ||
let mapping = { | ||
lastUpdated: timestamp(), | ||
data: response | ||
} | ||
setDataInLocalStorage(localStorageKey, JSON.stringify(mapping)); | ||
} catch (error) { | ||
logError('Failed to parse translation mapping file'); | ||
} | ||
}, | ||
error: () => { | ||
logError('Failed to load brand category translation file.') | ||
} | ||
}, | ||
); | ||
} | ||
} | ||
|
||
function setConfig(config) { | ||
if (config.translationFile) { | ||
// if publisher has defined the translation file, preload that file here | ||
initTranslation(config.translationFile); | ||
} | ||
} | ||
|
||
initTranslation(); | ||
config.getConfig('brandCategoryTranslation', config => setConfig(config.brandCategoryTranslation)); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,9 @@ import { isValidVideoBid } from '../video'; | |
import CONSTANTS from '../constants.json'; | ||
import events from '../events'; | ||
import includes from 'core-js/library/fn/array/includes'; | ||
import { logWarn, logError, parseQueryStringParameters, delayExecution, parseSizesInput, getBidderRequest } from '../utils'; | ||
import { ajax } from '../ajax'; | ||
import { logWarn, logError, parseQueryStringParameters, delayExecution, parseSizesInput, getBidderRequest, flatten, uniques, timestamp, setDataInLocalStorage, getDataFromLocalStorage, deepAccess } from '../utils'; | ||
import { ADPOD } from '../mediaTypes'; | ||
|
||
/** | ||
* This file aims to support Adapters during the Prebid 0.x -> 1.x transition. | ||
|
@@ -345,6 +347,67 @@ export function newBidder(spec) { | |
} | ||
} | ||
|
||
export function preloadBidderMappingFile(fn, adUnits) { | ||
let adPodBidders = adUnits | ||
.filter((adUnit) => deepAccess(adUnit, 'mediaTypes.video.context') === ADPOD) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also check if |
||
.map((adUnit) => adUnit.bids.map((bid) => bid.bidder)) | ||
.reduce(flatten, []) | ||
.filter(uniques); | ||
|
||
adPodBidders.forEach(bidder => { | ||
let bidderSpec = adapterManager.getBidAdapter(bidder); | ||
if (bidderSpec.getSpec().getMappingFileInfo) { | ||
let info = bidderSpec.getSpec().getMappingFileInfo(); | ||
let key = (info.localStorageKey) ? info.localStorageKey : bidderSpec.getSpec().code; | ||
let mappingData = getDataFromLocalStorage(key); | ||
if (!mappingData || timestamp() < mappingData.lastUpdated + info.refreshInDays * 24 * 60 * 60 * 1000) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we make |
||
ajax(info.url, | ||
{ | ||
success: (response) => { | ||
try { | ||
response = JSON.parse(response); | ||
let mapping = { | ||
lastUpdated: timestamp(), | ||
mapping: response.mapping | ||
} | ||
setDataInLocalStorage(key, JSON.stringify(mapping)); | ||
} catch (error) { | ||
logError(`Failed to parse ${bidder} bidder translation mapping file`); | ||
} | ||
}, | ||
error: () => { | ||
logError(`Failed to load ${bidder} bidder translation file`) | ||
} | ||
}, | ||
); | ||
} | ||
} | ||
}); | ||
fn.call(this, adUnits); | ||
} | ||
|
||
/** | ||
* Reads the data stored in localstorage and returns iab subcategory | ||
* @param {string} bidderCode bidderCode | ||
* @param {string} category bidders category | ||
*/ | ||
export function getIabSubCategory(bidderCode, category) { | ||
let bidderSpec = adapterManager.getBidAdapter(bidderCode); | ||
if (bidderSpec.getSpec().getMappingFileInfo) { | ||
let info = bidderSpec.getSpec().getMappingFileInfo(); | ||
let key = (info.localStorageKey) ? info.localStorageKey : bidderSpec.getBidderCode(); | ||
let data = getDataFromLocalStorage(key); | ||
if (data) { | ||
try { | ||
data = JSON.parse(data); | ||
} catch (error) { | ||
logError(`Failed to parse ${bidderCode} mapping data stored in local storage`); | ||
} | ||
return (data.mapping[category]) ? data.mapping[category] : null; | ||
} | ||
} | ||
} | ||
|
||
// check that the bid has a width and height set | ||
function validBidSize(adUnitCode, bid, bidRequests) { | ||
if ((bid.width || bid.width === 0) && (bid.height || bid.height === 0)) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1187,6 +1187,22 @@ export function convertTypes(types, params) { | |
return params; | ||
} | ||
|
||
export function setDataInLocalStorage(key, value) { | ||
if (hasLocalStorage()) { | ||
window.localStorage.setItem(key, value); | ||
} | ||
} | ||
|
||
export function getDataFromLocalStorage(key) { | ||
if (hasLocalStorage()) { | ||
return window.localStorage.getItem(key); | ||
} | ||
} | ||
|
||
export function hasLocalStorage() { | ||
return !!window.localStorage; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wrap in try/catch. If localstorage is disabled it will throw. |
||
} | ||
|
||
export function isArrayOfNums(val, size) { | ||
return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v))); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { getAdserverCategoryHook, initTranslation } from 'modules/categoryTranslation'; | ||
import { config } from 'src/config'; | ||
import * as utils from 'src/utils'; | ||
import { expect } from 'chai'; | ||
import { hooks } from 'src/hook'; | ||
|
||
describe('category translation', function () { | ||
let fakeTranslationServer; | ||
let getLocalStorageStub; | ||
|
||
beforeEach(function () { | ||
fakeTranslationServer = sinon.fakeServer.create(); | ||
getLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); | ||
}); | ||
|
||
afterEach(function() { | ||
getLocalStorageStub.restore(); | ||
config.resetConfig(); | ||
}); | ||
|
||
it('should translate iab category to adserver category', function () { | ||
hooks['registerAdserver'].before(notifyTranslationModule); | ||
function notifyTranslationModule(fn) { | ||
fn.call(this, 'freewheel'); | ||
} | ||
|
||
getLocalStorageStub.returns(JSON.stringify({ | ||
'data': { | ||
'freewheel': { | ||
mapping: { | ||
'iab-1': '1' | ||
} | ||
} | ||
} | ||
})); | ||
let bid = { | ||
meta: { | ||
iabSubCatId: 'iab-1' | ||
} | ||
} | ||
getAdserverCategoryHook(sinon.spy(), 'code', bid); | ||
expect(bid.meta.adServerCatId).to.equal('1'); | ||
}); | ||
|
||
it('should not make ajax call to update mapping file if data found in localstorage and is not expired', function () { | ||
let clock = sinon.useFakeTimers(utils.timestamp()); | ||
getLocalStorageStub.returns(JSON.stringify({ | ||
lastUpdated: utils.timestamp(), | ||
mapping: { | ||
'iab-1': '1' | ||
} | ||
})); | ||
initTranslation(); | ||
expect(fakeTranslationServer.requests.length).to.equal(0); | ||
clock.restore(); | ||
}); | ||
|
||
it('should use default mapping file if publisher has not defined in config', function () { | ||
getLocalStorageStub.returns(null); | ||
initTranslation(); | ||
expect(fakeTranslationServer.requests.length).to.equal(1); | ||
expect(fakeTranslationServer.requests[0].url).to.equal('https://api.myjson.com/bins/j5d0k'); | ||
}); | ||
|
||
it('should use publisher defined defined mapping file', function () { | ||
config.setConfig({ | ||
'brandCategoryTranslation': { | ||
'translationFile': 'http://sample.com' | ||
} | ||
}); | ||
getLocalStorageStub.returns(null); | ||
initTranslation(); | ||
expect(fakeTranslationServer.requests.length).to.equal(2); | ||
expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Todo here.