-
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
First Party Data module: Add new module and two submodules to populate defaults and validate ortb2 #6452
Merged
Merged
First Party Data module: Add new module and two submodules to populate defaults and validate ortb2 #6452
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
b30a795
Creating fpd module
mmoschovas da3f25f
Continued work on FPD module.
mmoschovas 732cb02
Revert userId update. Committed in error
mmoschovas eaaddbb
Added first party data unit tests and fixed bug
mmoschovas 8e41471
Added an unsubscribe for tests to run properly
mmoschovas 5974f3f
Reworked logic to use bidderRequests hook to update global/bidder con…
mmoschovas 5e6b754
Merge remote-tracking branch 'upstream/master' into fpd_module
mmoschovas f8741b4
Merge master
mmoschovas 11492e9
Removing unused references. Fixing device data to point to device.h/d…
mmoschovas 9f839e2
Update to include opt out configuration for enrichments/validations
mmoschovas ef8ed4b
Modified logic to use ortb2 configuration mapping. This will allow fo…
mmoschovas d9c1eda
Removed LGTM unneeded defensive code for check on object 'cur'
mmoschovas ae47223
Remove unused conditional
mmoschovas a63015c
Fix lint error
mmoschovas 27457ad
Updates to remove currency enrichment as well as optout for user object
mmoschovas afd2c26
Added optout flag to user.yob and user.gender fields
mmoschovas 5ed6071
Added test for arbitrary values
mmoschovas 707f6a5
Broke module out into module and two submodules
mmoschovas ed37eaf
Merge remote-tracking branch 'upstream/master' into fpd_module
mmoschovas d455cd4
Updated cur to validate as an array of strings not just a string
mmoschovas 110ea74
Merge remote-tracking branch 'upstream/master' into fpd_module
mmoschovas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,269 @@ | ||
/** | ||
* This module sets default values and validates ortb2 first part data | ||
* @module modules/firstPartyData | ||
*/ | ||
|
||
import { config } from '../../src/config.js'; | ||
import * as utils from '../../src/utils.js'; | ||
import { getHook } from '../../src/hook.js'; | ||
import { getRefererInfo } from '../../src/refererDetection.js' | ||
import { getStorageManager } from '../../src/storageManager.js'; | ||
|
||
const STORAGE = getStorageManager(); | ||
let ortb2 = {}; | ||
let globalConfig = {}; | ||
let win = (window === window.top) ? window : window.top; | ||
|
||
/** | ||
* Checks for referer and if exists merges into ortb2 global data | ||
*/ | ||
function setReferer() { | ||
if (getRefererInfo().referer) utils.mergeDeep(ortb2, {site: {ref: getRefererInfo().referer}}); | ||
} | ||
|
||
/** | ||
* Checks for canonical url and if exists merges into ortb2 global data | ||
*/ | ||
function setPage() { | ||
if (getRefererInfo().canonicalUrl) utils.mergeDeep(ortb2, {site: {page: getRefererInfo().canonicalUrl}}); | ||
} | ||
|
||
/** | ||
* Checks for canonical url and if exists retrieves domain and merges into ortb2 global data | ||
*/ | ||
function setDomain() { | ||
let parseDomain = function(url) { | ||
if (!url || typeof url !== 'string' || url.length === 0) return; | ||
|
||
var match = url.match(/^(?:https?:\/\/)?(?:www\.)?(.*?(?=(\?|\#|\/|$)))/i); | ||
|
||
return match && match[1]; | ||
}; | ||
|
||
let domain = parseDomain(getRefererInfo().canonicalUrl) | ||
|
||
if (domain) utils.mergeDeep(ortb2, {site: {domain: domain}}); | ||
} | ||
|
||
/** | ||
* Checks for screen/device width and height and sets dimensions | ||
*/ | ||
function setDimensions() { | ||
let width; | ||
let height; | ||
|
||
try { | ||
width = win.innerWidth || win.document.documentElement.clientWidth || win.document.body.clientWidth; | ||
height = win.innerHeight || win.document.documentElement.clientHeight || win.document.body.clientHeight; | ||
} catch (e) { | ||
width = window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth; | ||
height = window.innerHeight || window.document.documentElement.clientHeight || window.document.body.clientHeight; | ||
} | ||
|
||
utils.mergeDeep(ortb2, {device: {w: width, h: height}}); | ||
} | ||
|
||
/** | ||
* Scans page for meta keywords, and if exists, merges into site.keywords | ||
*/ | ||
function setKeywords() { | ||
let keywords; | ||
|
||
try { | ||
keywords = win.document.querySelector("meta[name='keywords']"); | ||
} catch (e) { | ||
keywords = window.document.querySelector("meta[name='keywords']"); | ||
} | ||
|
||
if (keywords && keywords.content) utils.mergeDeep(ortb2, {site: {keywords: keywords.content.replace(/\s/g, '')}}); | ||
} | ||
|
||
/** | ||
* Checks for currency and if exists merges into ortb2 global data | ||
* Sets listener for currency if changes occur or doesnt exist when run | ||
*/ | ||
function setCurrency() { | ||
let cur = {...config.getConfig('currency')}; | ||
|
||
if (cur && cur.adServerCurrency) { | ||
utils.mergeDeep(ortb2, {cur: cur.adServerCurrency}); | ||
} | ||
} | ||
|
||
/** | ||
* Filters data based on predefined requirements | ||
* @param {Object} data object from user.data or site.content.data | ||
* @param {String} name of data parent - user/site.content | ||
* @returns {Object} filtered data | ||
*/ | ||
export function filterData(data, key) { | ||
// If data is not an array or does not exist, return null | ||
if (!Array.isArray(data) || !data.length) { | ||
utils.logWarn(`Filtered ${key}.data[]: Must be an array of objects`); | ||
return null; | ||
} | ||
|
||
let duplicate = data.filter(index => { | ||
// If index not an object, name does not exist, segment does not exist, or segment is not an array | ||
// log warning and filter data index | ||
if (typeof index !== 'object' || !index.name || !index.segment || !Array.isArray(index.segment)) { | ||
utils.logWarn(`Filtered ${key}.data[]: must be an object containing name and segment`, index); | ||
return false; | ||
} | ||
|
||
return true; | ||
}).reduce((result, value) => { | ||
// If ext exists and is not an object, log warning and filter data index | ||
if (value.ext && (typeof value.ext !== 'object' || Array.isArray(value.ext))) { | ||
utils.logWarn(`Filtered ext attribute from ${key}.data[]: must be an object`, value); | ||
delete value.ext; | ||
} | ||
|
||
value.segment = value.segment.filter(el => { | ||
// For each segment index, check that id exists and is string, otherwise filter index | ||
if (!el.id || typeof el.id !== 'string') { | ||
utils.logWarn(`Filtered ${key}.data[].segment: id is required and must be a string`, el); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
|
||
// Check that segment data had not all been filtered out, else log warning and filter data index | ||
if (value.segment.length) { | ||
result.push(value); | ||
} else { | ||
utils.logWarn(`Filtered ${key}.data: must contain segment data`); | ||
} | ||
|
||
return result; | ||
}, []); | ||
|
||
return (duplicate.length) ? duplicate : null; | ||
} | ||
|
||
/** | ||
* Validates ortb2 object and filters out invalid data | ||
* @param {Object} ortb2 object | ||
* @returns {Object} validated/filtered data | ||
*/ | ||
export function validateFpd(obj) { | ||
if (!obj) return {}; | ||
// Filter out imp property if exists | ||
let validObject = Object.assign({}, Object.keys(obj).filter(key => { | ||
if (key !== 'imp') return key; | ||
|
||
utils.logWarn('Filtered imp property in ortb2 data'); | ||
}).reduce((result, key) => { | ||
let modified = {}; | ||
|
||
// Checks for existsnece of pubcid optout cookie/storage | ||
// if exists, filters user data out | ||
let optout = (STORAGE.cookiesAreEnabled() && STORAGE.getCookie('_pubcid_optout')) || | ||
(STORAGE.hasLocalStorage() && STORAGE.getDataFromLocalStorage('_pubcid_optout')); | ||
|
||
if (key === 'user' && optout) { | ||
utils.logWarn(`Filtered ${key} data: pubcid optout found`); | ||
return result; | ||
} | ||
|
||
// Create validated object by looping through ortb2 properties | ||
modified = (typeof obj[key] === 'object' && !Array.isArray(obj[key])) | ||
? Object.keys(obj[key]).reduce((combined, keyData) => { | ||
let data; | ||
|
||
// If key is user.data, pass into filterData to remove invalid data and return | ||
// Else if key is site.content.data, pass into filterData to remove invalid data and return | ||
// Else return data unfiltered | ||
if (key === 'user' && keyData === 'data') { | ||
data = filterData(obj[key][keyData], key); | ||
|
||
if (data) combined[keyData] = data; | ||
} else if (key === 'site' && keyData === 'content' && obj[key][keyData].data) { | ||
let content = Object.keys(obj[key][keyData]).reduce((merged, contentData) => { | ||
if (contentData === 'data') { | ||
data = filterData(obj[key][keyData][contentData], key + '.content'); | ||
|
||
if (data) merged[contentData] = data; | ||
} else { | ||
merged[contentData] = obj[key][keyData][contentData]; | ||
} | ||
|
||
return merged; | ||
}, {}); | ||
|
||
if (Object.keys(content).length) combined[keyData] = content; | ||
} else { | ||
combined[keyData] = obj[key][keyData]; | ||
} | ||
|
||
return combined; | ||
}, {}) : obj[key]; | ||
|
||
// Check if modified data has data and return | ||
if (Object.keys(modified).length) result[key] = modified; | ||
|
||
return result; | ||
}, {})); | ||
|
||
// Return validated data | ||
return validObject; | ||
} | ||
|
||
/** | ||
* Resets global ortb2 data | ||
*/ | ||
export const resetOrtb2 = () => { ortb2 = {} }; | ||
|
||
function runEnrichments(shouldSkipValidate) { | ||
setReferer(); | ||
setPage(); | ||
setDomain(); | ||
setDimensions(); | ||
setKeywords(); | ||
setCurrency(); | ||
|
||
if (shouldSkipValidate) config.setConfig({ortb2: utils.mergeDeep({}, ortb2, config.getConfig('ortb2'))}); | ||
} | ||
|
||
function runValidations() { | ||
let conf = utils.mergeDeep({}, ortb2, validateFpd(config.getConfig('ortb2'))); | ||
|
||
if (!utils.deepEqual(conf, globalConfig)) { | ||
config.setConfig({ortb2: conf}); | ||
globalConfig = {...conf}; | ||
resetOrtb2(); | ||
} | ||
|
||
let bidderDuplicate = {...config.getBidderConfig()}; | ||
|
||
Object.keys(bidderDuplicate).forEach(bidder => { | ||
let modConf = Object.keys(bidderDuplicate[bidder]).reduce((res, key) => { | ||
let valid = (key !== 'ortb2') ? bidderDuplicate[bidder][key] : validateFpd(bidderDuplicate[bidder][key]); | ||
|
||
if (valid) res[key] = valid; | ||
|
||
return res; | ||
}, {}); | ||
|
||
if (Object.keys(modConf).length) config.setBidderConfig({bidders: [bidder], config: modConf}); | ||
}); | ||
} | ||
|
||
/** | ||
* Sets default values to ortb2 if exists and adds currency and ortb2 setConfig callbacks on init | ||
*/ | ||
export function init() { | ||
let conf = config.getConfig('firstPartyData'); | ||
let skipValidations = (conf && conf.skipValidations) || false; | ||
let skipEnrichments = (conf && conf.skipEnrichments) || false; | ||
|
||
if (!skipEnrichments) runEnrichments(skipValidations); | ||
bretg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!skipValidations) runValidations(); | ||
} | ||
|
||
function addBidderRequestHook(fn, bidderRequests) { | ||
init(); | ||
fn.call(this, bidderRequests); | ||
} | ||
Fawke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
getHook('addBidderRequests').before(addBidderRequestHook); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Overview | ||
|
||
``` | ||
Module Name: First Party Data Module | ||
``` | ||
|
||
# Description | ||
|
||
Module to perform the following functions to allow for consistent set of first party data | ||
|
||
- verify OpenRTB datatypes, remove/warn any that are likely to choke downstream readers | ||
- verify that certain OpenRTB attributes are not specified: just imp for now | ||
- optionally suppress user FPD based on a TBD opt-out signal (_pubcid_optout) | ||
- populate available data into object: referer, meta-keywords, cur | ||
|
||
This module will automatically run both first party data enrichments and validations. There is no configuration required. In order to load the module and opt out of either enrichements or validations, use the below opt out configuration | ||
|
||
# Opt Out Configuration | ||
|
||
``` | ||
|
||
pbjs.setConfig({ | ||
firstPartyData: { | ||
skipValidations: true, // default to false | ||
skipEnrichments: true // default to false | ||
} | ||
}); | ||
|
||
``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
We want any existing values to take precedence. Is that the behavior of mergeDeep?
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.
Correct. So right to left in terms of priority. This line is actually just setting the modules global variable which is later merged with the response of getConfig('ortb2) || {}. So if the value exists in ortb2 already it will overwrite the modules 'enrichments' setting and use what was already there.