Skip to content

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

Merged
merged 13 commits into from
Mar 26, 2025
1 change: 1 addition & 0 deletions modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"optimeraRtdProvider",
"oxxionRtdProvider",
"permutiveRtdProvider",
"pubmaticRtdProvider",
"pubxaiRtdProvider",
"qortexRtdProvider",
"reconciliationRtdProvider",
Expand Down
33 changes: 33 additions & 0 deletions modules/pubmaticAnalyticsAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ const MEDIATYPE = {
NATIVE: 2
}

// TODO : Remove - Once BM calculation moves to Server Side
const BROWSER_MAP = [
{ value: /(firefox)\/([\w\.]+)/i, key: 12 }, // Firefox
{ value: /\b(?:crios)\/([\w\.]+)/i, key: 1 }, // Chrome for iOS
{ value: /edg(?:e|ios|a)?\/([\w\.]+)/i, key: 2 }, // Edge
{ value: /(opera)(?:.+version\/|[\/ ]+)([\w\.]+)/i, key: 3 }, // Opera
{ value: /(?:ms|\()(ie) ([\w\.]+)/i, key: 4 }, // Internet Explorer
{ value: /fxios\/([-\w\.]+)/i, key: 5 }, // Firefox for iOS
{ value: /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w\.]+);)/i, key: 6 }, // Facebook In-App Browser
{ value: / wv\).+(chrome)\/([\w\.]+)/i, key: 7 }, // Chrome WebView
{ value: /droid.+ version\/([\w\.]+)\b.+(?:mobile safari|safari)/i, key: 8 }, // Android Browser
{ value: /(chrome|chromium|crios)\/v?([\w\.]+)/i, key: 9 }, // Chrome
{ value: /version\/([\w\.\,]+) .*mobile\/\w+ (safari)/i, key: 10 }, // Safari Mobile
{ value: /version\/([\w(\.|\,)]+) .*(mobile ?safari|safari)/i, key: 11 }, // Safari
];

/// /////////// VARIABLES //////////////
let publisherId = DEFAULT_PUBLISHER_ID; // int: mandatory
let profileId = DEFAULT_PROFILE_ID; // int: optional
Expand Down Expand Up @@ -204,6 +220,17 @@ function getDevicePlatform() {
return deviceType;
}

// TODO : Remove - Once BM calculation moves to Server Side
function getBrowserType() {
const userAgent = navigator?.userAgent;
let browserIndex = userAgent == null ? -1 : 0;

if (userAgent) {
browserIndex = BROWSER_MAP.find(({ value }) => value.test(userAgent))?.key || 0;
}
return browserIndex;
}

function getValueForKgpv(bid, adUnitId) {
if (bid.params && bid.params.regexPattern) {
return bid.params.regexPattern;
Expand Down Expand Up @@ -409,6 +436,10 @@ function executeBidsLoggerCall(e, highestCpmBids) {
let outputObj = { s: [] };
let pixelURL = END_POINT_BID_LOGGER;

const user = e.bidderRequests?.length > 0
? e.bidderRequests.find(bidder => bidder?.bidderCode === ADAPTER_CODE)?.ortb2?.user?.ext || {}
: {};

if (!auctionCache || auctionCache.sent) {
return;
}
Expand All @@ -426,6 +457,8 @@ function executeBidsLoggerCall(e, highestCpmBids) {
outputObj['tgid'] = getTgId();
outputObj['dm'] = DISPLAY_MANAGER;
outputObj['dmv'] = '$prebid.version$' || '-1';
outputObj['bm'] = getBrowserType();
outputObj['ctr'] = Object.keys(user)?.length ? user.ctr : '';

if (floorData) {
const floorRootValues = getFloorsCommonField(floorData?.floorRequestData);
Expand Down
242 changes: 242 additions & 0 deletions modules/pubmaticRtdProvider.js
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
Copy link
Collaborator

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.

Copy link
Contributor Author

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.


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.`);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

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, we can remove this failsafe condition.

Copy link
Collaborator

@dgirardi dgirardi Mar 17, 2025

Choose a reason for hiding this comment

The 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 continueAuction. After this change, the floors module does its thing and calls continueAuction; plus you do your thing and continue the auction a second time. I'd expect this to give you an extra auction, but really the behavior is undefined.

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:

  • defines a few additional fields,
  • set a floors data source that can use them, and
  • could be done with a normal setConfig snippet - it doesn't actually need or want to modify the bid stream?

If so, my recommendation is to make it not an RTD module; just ask for your publisher/profile ID and call setConfig a way a publisher would - they would do that instead of configuring floors, it would run at the same time it'd make sense to configure floors (before any auction), and you'd let the floors module fetch the data and work normally.

If you do have a reason to run this on every auction, you can make sure you can setConfig before the floors module sees it by attaching a requestBid hook with higher priority.

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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It was brought to my attention that this pattern has a precedent:

/**
* This RTD module has a dependency on the priceFloors module.
* We utilize the createFloorsDataForAuction function from the priceFloors module to incorporate price floors data into the current auction.
*/
import { createFloorsDataForAuction } from './priceFloors.js'; // eslint-disable-line prebid/validate-imports

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();
72 changes: 72 additions & 0 deletions modules/pubmaticRtdProvider.md
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.
Loading