-
Notifications
You must be signed in to change notification settings - Fork 2.2k
PubMatic RTD Provider - Initial Release #12732
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
Changes from all commits
8c924ab
79ddfe3
612157f
cf19a20
5b3318f
a5323f6
1a67e3b
71289a3
d8d2b9d
134463a
d50e134
44d8e0c
b910511
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,242 @@ | ||||||||||||
import { submodule } from '../src/hook.js'; | ||||||||||||
import { logError, isStr, logMessage, isPlainObject, isEmpty, isFn, mergeDeep } from '../src/utils.js'; | ||||||||||||
import { config as conf } from '../src/config.js'; | ||||||||||||
import { getDeviceType as fetchDeviceType, getOS } from '../libraries/userAgentUtils/index.js'; | ||||||||||||
import { getLowEntropySUA } from '../src/fpd/sua.js'; | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule | ||||||||||||
*/ | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* This RTD module has a dependency on the priceFloors module. | ||||||||||||
* We utilize the continueAuction function from the priceFloors module to incorporate price floors data into the current auction. | ||||||||||||
*/ | ||||||||||||
import { continueAuction } from './priceFloors.js'; // eslint-disable-line prebid/validate-imports | ||||||||||||
|
||||||||||||
patmmccann marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
const CONSTANTS = Object.freeze({ | ||||||||||||
SUBMODULE_NAME: 'pubmatic', | ||||||||||||
REAL_TIME_MODULE: 'realTimeData', | ||||||||||||
LOG_PRE_FIX: 'PubMatic-Rtd-Provider: ', | ||||||||||||
UTM: 'utm_', | ||||||||||||
UTM_VALUES: { | ||||||||||||
TRUE: '1', | ||||||||||||
FALSE: '0' | ||||||||||||
}, | ||||||||||||
TIME_OF_DAY_VALUES: { | ||||||||||||
MORNING: 'morning', | ||||||||||||
AFTERNOON: 'afternoon', | ||||||||||||
EVENING: 'evening', | ||||||||||||
NIGHT: 'night', | ||||||||||||
}, | ||||||||||||
ENDPOINTS: { | ||||||||||||
FLOORS_BASEURL: `https://ads.pubmatic.com/AdServer/js/pwt/floors/`, | ||||||||||||
FLOORS_ENDPOINT: `/floors.json`, | ||||||||||||
} | ||||||||||||
}); | ||||||||||||
|
||||||||||||
const BROWSER_REGEX_MAP = [ | ||||||||||||
{ regex: /\b(?:crios)\/([\w\.]+)/i, id: 1 }, // Chrome for iOS | ||||||||||||
{ regex: /(edg|edge)(?:e|ios|a)?(?:\/([\w\.]+))?/i, id: 2 }, // Edge | ||||||||||||
{ regex: /(opera)(?:.+version\/|[\/ ]+)([\w\.]+)/i, id: 3 }, // Opera | ||||||||||||
{ regex: /(?:ms|\()(ie) ([\w\.]+)/i, id: 4 }, // Internet Explorer | ||||||||||||
{ regex: /fxios\/([-\w\.]+)/i, id: 5 }, // Firefox for iOS | ||||||||||||
{ regex: /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w\.]+);)/i, id: 6 }, // Facebook In-App Browser | ||||||||||||
{ regex: / wv\).+(chrome)\/([\w\.]+)/i, id: 7 }, // Chrome WebView | ||||||||||||
{ regex: /droid.+ version\/([\w\.]+)\b.+(?:mobile safari|safari)/i, id: 8 }, // Android Browser | ||||||||||||
{ regex: /(chrome|crios)(?:\/v?([\w\.]+))?\b/i, id: 9 }, // Chrome | ||||||||||||
{ regex: /version\/([\w\.\,]+) .*mobile\/\w+ (safari)/i, id: 10 }, // Safari Mobile | ||||||||||||
{ regex: /version\/([\w(\.|\,)]+) .*(mobile ?safari|safari)/i, id: 11 }, // Safari | ||||||||||||
{ regex: /(firefox)\/([\w\.]+)/i, id: 12 } // Firefox | ||||||||||||
]; | ||||||||||||
|
||||||||||||
let _pubmaticFloorRulesPromise = null; | ||||||||||||
export let _country; | ||||||||||||
|
||||||||||||
// Utility Functions | ||||||||||||
export const getCurrentTimeOfDay = () => { | ||||||||||||
const currentHour = new Date().getHours(); | ||||||||||||
|
||||||||||||
return currentHour < 5 ? CONSTANTS.TIME_OF_DAY_VALUES.NIGHT | ||||||||||||
: currentHour < 12 ? CONSTANTS.TIME_OF_DAY_VALUES.MORNING | ||||||||||||
: currentHour < 17 ? CONSTANTS.TIME_OF_DAY_VALUES.AFTERNOON | ||||||||||||
: currentHour < 19 ? CONSTANTS.TIME_OF_DAY_VALUES.EVENING | ||||||||||||
: CONSTANTS.TIME_OF_DAY_VALUES.NIGHT; | ||||||||||||
} | ||||||||||||
|
||||||||||||
export const getBrowserType = () => { | ||||||||||||
const brandName = getLowEntropySUA()?.browsers | ||||||||||||
?.map(b => b.brand.toLowerCase()) | ||||||||||||
.join(' ') || ''; | ||||||||||||
const browserMatch = brandName ? BROWSER_REGEX_MAP.find(({ regex }) => regex.test(brandName)) : -1; | ||||||||||||
|
||||||||||||
if (browserMatch?.id) return browserMatch.id.toString(); | ||||||||||||
|
||||||||||||
const userAgent = navigator?.userAgent; | ||||||||||||
let browserIndex = userAgent == null ? -1 : 0; | ||||||||||||
|
||||||||||||
if (userAgent) { | ||||||||||||
browserIndex = BROWSER_REGEX_MAP.find(({ regex }) => regex.test(userAgent))?.id || 0; | ||||||||||||
} | ||||||||||||
return browserIndex.toString(); | ||||||||||||
} | ||||||||||||
|
||||||||||||
// Getter Functions | ||||||||||||
|
||||||||||||
export const getOs = () => getOS().toString(); | ||||||||||||
|
||||||||||||
export const getDeviceType = () => fetchDeviceType().toString(); | ||||||||||||
|
||||||||||||
export const getCountry = () => _country; | ||||||||||||
|
||||||||||||
export const getUtm = () => { | ||||||||||||
const url = new URL(window.location?.href); | ||||||||||||
const urlParams = new URLSearchParams(url?.search); | ||||||||||||
return urlParams && urlParams.toString().includes(CONSTANTS.UTM) ? CONSTANTS.UTM_VALUES.TRUE : CONSTANTS.UTM_VALUES.FALSE; | ||||||||||||
} | ||||||||||||
|
||||||||||||
export const getFloorsConfig = (apiResponse) => { | ||||||||||||
let defaultFloorConfig = conf.getConfig()?.floors ?? {}; | ||||||||||||
|
||||||||||||
if (defaultFloorConfig?.endpoint) { | ||||||||||||
delete defaultFloorConfig.endpoint | ||||||||||||
} | ||||||||||||
defaultFloorConfig.data = { ...apiResponse }; | ||||||||||||
|
||||||||||||
const floorsConfig = { | ||||||||||||
floors: { | ||||||||||||
additionalSchemaFields: { | ||||||||||||
deviceType: getDeviceType, | ||||||||||||
timeOfDay: getCurrentTimeOfDay, | ||||||||||||
browser: getBrowserType, | ||||||||||||
os: getOs, | ||||||||||||
utm: getUtm, | ||||||||||||
country: getCountry, | ||||||||||||
}, | ||||||||||||
...defaultFloorConfig | ||||||||||||
}, | ||||||||||||
}; | ||||||||||||
|
||||||||||||
return floorsConfig; | ||||||||||||
}; | ||||||||||||
|
||||||||||||
export const setFloorsConfig = (data) => { | ||||||||||||
if (data && isPlainObject(data) && !isEmpty(data)) { | ||||||||||||
conf.setConfig(getFloorsConfig(data)); | ||||||||||||
} else { | ||||||||||||
logMessage(CONSTANTS.LOG_PRE_FIX + 'The fetched floors data is empty.'); | ||||||||||||
} | ||||||||||||
}; | ||||||||||||
|
||||||||||||
export const setPriceFloors = async (publisherId, profileId) => { | ||||||||||||
const apiResponse = await fetchFloorRules(publisherId, profileId); | ||||||||||||
|
||||||||||||
if (!apiResponse) { | ||||||||||||
logError(CONSTANTS.LOG_PRE_FIX + 'Error while fetching floors: Empty response'); | ||||||||||||
} else { | ||||||||||||
setFloorsConfig(apiResponse); | ||||||||||||
} | ||||||||||||
}; | ||||||||||||
|
||||||||||||
export const fetchFloorRules = async (publisherId, profileId) => { | ||||||||||||
try { | ||||||||||||
const url = `${CONSTANTS.ENDPOINTS.FLOORS_BASEURL}${publisherId}/${profileId}${CONSTANTS.ENDPOINTS.FLOORS_ENDPOINT}`; | ||||||||||||
|
||||||||||||
const response = await fetch(url); | ||||||||||||
if (!response?.ok) { | ||||||||||||
logError(CONSTANTS.LOG_PRE_FIX + 'Error while fetching floors: No response'); | ||||||||||||
} | ||||||||||||
|
||||||||||||
const cc = response.headers?.get('country_code'); | ||||||||||||
_country = cc ? cc.split(',')?.map(code => code.trim())[0] : undefined; | ||||||||||||
|
||||||||||||
const data = await response.json(); | ||||||||||||
return data; | ||||||||||||
} catch (error) { | ||||||||||||
logError(CONSTANTS.LOG_PRE_FIX + 'Error while fetching floors:', error); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* Initialize the Pubmatic RTD Module. | ||||||||||||
* @param {Object} config | ||||||||||||
* @param {Object} _userConsent | ||||||||||||
* @returns {boolean} | ||||||||||||
*/ | ||||||||||||
const init = (config, _userConsent) => { | ||||||||||||
const publisherId = config?.params?.publisherId; | ||||||||||||
const profileId = config?.params?.profileId; | ||||||||||||
|
||||||||||||
if (!publisherId || !isStr(publisherId) || !profileId || !isStr(profileId)) { | ||||||||||||
logError( | ||||||||||||
`${CONSTANTS.LOG_PRE_FIX} ${!publisherId ? 'Missing publisher Id.' | ||||||||||||
: !isStr(publisherId) ? 'Publisher Id should be a string.' | ||||||||||||
: !profileId ? 'Missing profile Id.' | ||||||||||||
: 'Profile Id should be a string.' | ||||||||||||
}` | ||||||||||||
); | ||||||||||||
return false; | ||||||||||||
} | ||||||||||||
|
||||||||||||
if (!isFn(continueAuction)) { | ||||||||||||
logError(`${CONSTANTS.LOG_PRE_FIX} continueAuction is not a function. Please ensure to add priceFloors module.`); | ||||||||||||
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 never the case. depending on the build (which we don't always control), the fail state is you get a copy of the floor module (and of this function) in your own module, with undefined amounts of its internal state either duplicated or shared. 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. Yes, we can remove this failsafe condition. 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 does not work as you want it to, for several reason. What I described above is not fixed by removing the check, you'll still get a messed up build output. That's why we have the linter rule. But beyond that, consider what this change does; normally, the floors module does its thing and calls And, even if you can make sure they can work together and stay working (this approach necessarily means marrying the two modules together), the best case scenario is that with this the floors module does not do what's advertised. Am I right that this:
If so, my recommendation is to make it not an RTD module; just ask for your publisher/profile ID and call If you do have a reason to run this on every auction, you can make sure you can And finally, if you do need to alter how the floors module works, it (the floors module) should be updated to support your use case. 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. It was brought to my attention that this pattern has a precedent: Prebid.js/modules/pubxaiRtdProvider.js Lines 7 to 11 in 20d2c93
Same issue applies there; it makes the module larger and more brittle. However it can (and hopefully does) work, as long as the floors logic doesn't change. It may be worth updating the floors module to support this use case - allow RTD providers to set floors data. In the meanwhile, my recommendation remains to remove the dependency on the internals of floors, and update this to just set config for it; but since we have a precedent I'm OK with moving this forward as it is. |
||||||||||||
return false; | ||||||||||||
} | ||||||||||||
|
||||||||||||
_pubmaticFloorRulesPromise = setPriceFloors(publisherId, profileId); | ||||||||||||
return true; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* @param {Object} reqBidsConfigObj | ||||||||||||
* @param {function} callback | ||||||||||||
* @param {Object} config | ||||||||||||
* @param {Object} userConsent | ||||||||||||
*/ | ||||||||||||
|
||||||||||||
const getBidRequestData = (reqBidsConfigObj, callback) => { | ||||||||||||
_pubmaticFloorRulesPromise.then(() => { | ||||||||||||
const hookConfig = { | ||||||||||||
reqBidsConfigObj, | ||||||||||||
context: this, | ||||||||||||
nextFn: () => true, | ||||||||||||
haveExited: false, | ||||||||||||
timer: null | ||||||||||||
}; | ||||||||||||
continueAuction(hookConfig); | ||||||||||||
if (_country) { | ||||||||||||
const ortb2 = { | ||||||||||||
user: { | ||||||||||||
ext: { | ||||||||||||
ctr: _country, | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { | ||||||||||||
[CONSTANTS.SUBMODULE_NAME]: ortb2 | ||||||||||||
}); | ||||||||||||
} | ||||||||||||
callback(); | ||||||||||||
}).catch((error) => { | ||||||||||||
logError(CONSTANTS.LOG_PRE_FIX, 'Error in updating floors :', error); | ||||||||||||
callback(); | ||||||||||||
}); | ||||||||||||
} | ||||||||||||
|
||||||||||||
/** @type {RtdSubmodule} */ | ||||||||||||
export const pubmaticSubmodule = { | ||||||||||||
/** | ||||||||||||
* used to link submodule with realTimeData | ||||||||||||
* @type {string} | ||||||||||||
*/ | ||||||||||||
name: CONSTANTS.SUBMODULE_NAME, | ||||||||||||
init, | ||||||||||||
getBidRequestData, | ||||||||||||
}; | ||||||||||||
|
||||||||||||
export const registerSubModule = () => { | ||||||||||||
submodule(CONSTANTS.REAL_TIME_MODULE, pubmaticSubmodule); | ||||||||||||
} | ||||||||||||
|
||||||||||||
registerSubModule(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
## Overview | ||
|
||
- Module Name: PubMatic RTD Provider | ||
- Module Type: RTD Adapter | ||
- Maintainer: [email protected] | ||
|
||
## Description | ||
|
||
The PubMatic RTD module fetches pricing floor data and updates the Price Floors Module based on user's context in real-time as per Price Floors Modules Floor Data Provider Interface guidelines [Dynamic Floor Data Provider](https://docs.prebid.org/dev-docs/modules/floors.html#floor-data-provider-interface). | ||
|
||
## Usage | ||
|
||
Step 1: Contact PubMatic to get a publisher ID and create your first profile. | ||
|
||
Step 2: Integrate the PubMatic Analytics Adapter (see Prebid Analytics modules) as well as the Price Floors module. | ||
|
||
Step 3: Prepare the base Prebid file. | ||
|
||
For example: | ||
|
||
To compile the Price Floors, PubMatic RTD module and PubMatic Analytics Adapter into your Prebid build: | ||
|
||
```shell | ||
gulp build --modules=priceFloors,rtdModule,pubmaticRtdProvider,pubmaticAnalyticsAdapter | ||
``` | ||
|
||
{: .alert.alert-info :} | ||
Note: The PubMatic RTD module is dependent on the global real-time data module : `rtdModule`, price floor module : `priceFloors` and PubMatic Analytics Adapter : `pubmaticAnalyticsAdapter`. | ||
|
||
Step 4: Set configuration and enable PubMatic RTD Module using pbjs.setConfig. | ||
|
||
## Configuration | ||
|
||
This module is configured as part of the `realTimeData.dataProviders`. We recommend setting `auctionDelay` to at least 250 ms and make sure `waitForIt` is set to `true` for the `pubmatic` RTD provider. | ||
|
||
```js | ||
const AUCTION_DELAY = 250; | ||
pbjs.setConfig({ | ||
// rest of the config | ||
..., | ||
realTimeData: { | ||
auctionDelay: AUCTION_DELAY, | ||
dataProviders: [ | ||
{ | ||
name: "pubmatic", | ||
waitForIt: true, | ||
params: { | ||
publisherId: `<publisher_id>`, // please contact PubMatic to get a publisherId for yourself | ||
profileId: `<profile_id>`, // please contact PubMatic to get a profileId for yourself | ||
}, | ||
}, | ||
], | ||
}, | ||
// rest of the config | ||
..., | ||
}); | ||
``` | ||
|
||
## Parameters | ||
|
||
| Name | Type | Description | Default | | ||
| :----------------- | :------ | :------------------------------------------------------------- | :------------------------- | | ||
| name | String | Name of the real-time data module | Always `pubmatic` | | ||
| waitForIt | Boolean | Should be `true` if an `auctionDelay` is defined (mandatory) | `false` | | ||
| params | Object | | | | ||
| params.publisherId | String | Publisher ID | | | ||
| params.profileId | String | Profile ID | | | ||
|
||
|
||
## What Should Change in the Bid Request? | ||
|
||
There are no direct changes in the bid request due to our RTD module, but floor configuration will be set using the price floors module. These changes will be reflected in adunit bids or bidder requests as floor data. |
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.
why not set floor data instead? even if you can somehow make it work now, why tie yourself to the internals of the floor module - one or the other is likely to stop collaborating, since they're not meant to be collaborating.
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.
By the time the floor is set in RTD, the RequestBidHook has already executed. Consequently, updating floors using setConfig() within RTD does not impact the auction. To address this, we use the continueAuction() function to update the adUnits accordingly and return the necessary floorsData for the current auction.