Skip to content

Commit

Permalink
Mobian Bid Adapter : push context data to GAM (#12389)
Browse files Browse the repository at this point in the history
* Push context data to GAM

* Update browsi to set gpt key values

* fix browsi

* Revamps module to make it configurable

* Revamps module and tests, adds config

* Adds more config and documentation

* Updates mock emotion

---------

Co-authored-by: Demetrio Girardi <[email protected]>
  • Loading branch information
arielmtk and dgirardi authored Dec 2, 2024
1 parent f7e44cc commit f7e8034
Show file tree
Hide file tree
Showing 5 changed files with 406 additions and 284 deletions.
12 changes: 12 additions & 0 deletions libraries/gptUtils/gptUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ export function isSlotMatchingAdUnitCode(adUnitCode) {
return (slot) => compareCodeAndSlot(slot, adUnitCode);
}

/**
* @summary Export a k-v pair to GAM
*/
export function setKeyValue(key, value) {
if (!key || typeof key !== 'string') return false;
window.googletag = window.googletag || {cmd: []};
window.googletag.cmd = window.googletag.cmd || [];
window.googletag.cmd.push(() => {
window.googletag.pubads().setTargeting(key, value);
});
}

/**
* @summary Uses the adUnit's code in order to find a matching gpt slot object on the page
*/
Expand Down
10 changes: 2 additions & 8 deletions modules/browsiRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {getGlobal} from '../src/prebidGlobal.js';
import * as events from '../src/events.js';
import {EVENTS} from '../src/constants.js';
import {MODULE_TYPE_RTD} from '../src/activities/modules.js';
import {setKeyValue as setGptKeyValue} from '../libraries/gptUtils/gptUtils.js';

/**
* @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule
Expand Down Expand Up @@ -67,14 +68,7 @@ export function addBrowsiTag(data) {
return script;
}

export function setKeyValue(key) {
if (!key || typeof key !== 'string') return false;
window.googletag = window.googletag || {cmd: []};
window.googletag.cmd = window.googletag.cmd || [];
window.googletag.cmd.push(() => {
window.googletag.pubads().setTargeting(key, RANDOM.toString());
});
}
export const setKeyValue = (key) => setGptKeyValue(key, RANDOM.toString());

export function sendPageviewEvent(eventType) {
if (eventType === 'PAGEVIEW') {
Expand Down
241 changes: 178 additions & 63 deletions modules/mobianRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,85 +4,200 @@
*/
import { submodule } from '../src/hook.js';
import { ajaxBuilder } from '../src/ajax.js';
import { deepSetValue, safeJSONParse } from '../src/utils.js';
import { safeJSONParse, logMessage as _logMessage } from '../src/utils.js';
import { setKeyValue } from '../libraries/gptUtils/gptUtils.js';

/**
* @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule
*/

/**
* @typedef {Object} MobianConfig
* @property {MobianConfigParams} params
*/

/**
* @typedef {Object} MobianConfigParams
* @property {string} [prefix] - Optional prefix for targeting keys (default: 'mobian')
* @property {boolean} [publisherTargeting] - Optional boolean to enable targeting for publishers (default: false)
* @property {boolean} [advertiserTargeting] - Optional boolean to enable targeting for advertisers (default: false)
*/

/**
* @typedef {Object} MobianContextData
* @property {Object} apValues
* @property {string[]} categories
* @property {string[]} emotions
* @property {string[]} genres
* @property {string} risk
* @property {string} sentiment
* @property {string[]} themes
* @property {string[]} tones
*/

export const MOBIAN_URL = 'https://prebid.outcomes.net/api/prebid/v1/assessment/async';

/** @type {RtdSubmodule} */
export const mobianBrandSafetySubmodule = {
name: 'mobianBrandSafety',
init: init,
getBidRequestData: getBidRequestData
export const CONTEXT_KEYS = [
'apValues',
'categories',
'emotions',
'genres',
'risk',
'sentiment',
'themes',
'tones'
];

const AP_KEYS = ['a0', 'a1', 'p0', 'p1'];

const logMessage = (...args) => {
_logMessage('Mobian', ...args);
};

function init() {
return true;
function makeMemoizedFetch() {
let cachedResponse = null;
return async function () {
if (cachedResponse) {
return Promise.resolve(cachedResponse);
}
try {
const response = await fetchContextData();
cachedResponse = makeDataFromResponse(response);
return cachedResponse;
} catch (error) {
logMessage('error', error);
return Promise.resolve({});
}
}
}

function getBidRequestData(bidReqConfig, callback, config) {
const { site: ortb2Site } = bidReqConfig.ortb2Fragments.global;
const pageUrl = encodeURIComponent(getPageUrl());
export const getContextData = makeMemoizedFetch();

export async function fetchContextData() {
const pageUrl = encodeURIComponent(window.location.href);
const requestUrl = `${MOBIAN_URL}?url=${pageUrl}`;
const request = ajaxBuilder();

return new Promise((resolve, reject) => {
request(requestUrl, { success: resolve, error: reject });
});
}

export function getConfig(config) {
const [advertiserTargeting, publisherTargeting] = ['advertiserTargeting', 'publisherTargeting'].map((key) => {
const value = config?.params?.[key];
if (!value) {
return [];
} else if (value === true) {
return CONTEXT_KEYS;
} else if (Array.isArray(value) && value.length) {
return value.filter((key) => CONTEXT_KEYS.includes(key));
}
return [];
});

const prefix = config?.params?.prefix || 'mobian';
return { advertiserTargeting, prefix, publisherTargeting };
}

/**
* @param {MobianConfigParams} parsedConfig
* @param {MobianContextData} contextData
* @returns {function}
*/
export function setTargeting(parsedConfig, contextData) {
const { publisherTargeting, prefix } = parsedConfig;
logMessage('context', contextData);

CONTEXT_KEYS.forEach((key) => {
if (!publisherTargeting.includes(key)) return;

const ajax = ajaxBuilder();

return new Promise((resolve) => {
ajax(requestUrl, {
success: function(responseData) {
let response = safeJSONParse(responseData);
if (!response || !response.meta.has_results) {
resolve({});
callback();
return;
}

const results = response.results;
const mobianRisk = results.mobianRisk || 'unknown';
const contentCategories = results.mobianContentCategories || [];
const sentiment = results.mobianSentiment || 'unknown';
const emotions = results.mobianEmotions || [];
const themes = results.mobianThemes || [];
const tones = results.mobianTones || [];
const genres = results.mobianGenres || [];
const apValues = results.ap || {};

const risk = {
risk: mobianRisk,
contentCategories: contentCategories,
sentiment: sentiment,
emotions: emotions,
themes: themes,
tones: tones,
genres: genres,
apValues: apValues,
};

deepSetValue(ortb2Site, 'ext.data.mobianRisk', mobianRisk);
deepSetValue(ortb2Site, 'ext.data.mobianContentCategories', contentCategories);
deepSetValue(ortb2Site, 'ext.data.mobianSentiment', sentiment);
deepSetValue(ortb2Site, 'ext.data.mobianEmotions', emotions);
deepSetValue(ortb2Site, 'ext.data.mobianThemes', themes);
deepSetValue(ortb2Site, 'ext.data.mobianTones', tones);
deepSetValue(ortb2Site, 'ext.data.mobianGenres', genres);
deepSetValue(ortb2Site, 'ext.data.apValues', apValues);

resolve(risk);
callback();
},
error: function () {
resolve({});
callback();
}
});
if (key === 'apValues') {
AP_KEYS.forEach((apKey) => {
if (!contextData[key]?.[apKey]?.length) return;
logMessage(`${prefix}_ap_${apKey}`, contextData[key][apKey]);
setKeyValue(`${prefix}_ap_${apKey}`, contextData[key][apKey]);
});
return;
}

if (contextData[key]?.length) {
logMessage(`${prefix}_${key}`, contextData[key]);
setKeyValue(`${prefix}_${key}`, contextData[key]);
}
});
}

function getPageUrl() {
return window.location.href;
export function makeDataFromResponse(contextData) {
const data = typeof contextData === 'string' ? safeJSONParse(contextData) : contextData;
const results = data.results;
if (!results) {
return {};
}
return {
apValues: results.ap || {},
categories: results.mobianContentCategories,
emotions: results.mobianEmotions,
genres: results.mobianGenres,
risk: results.mobianRisk || 'unknown',
sentiment: results.mobianSentiment || 'unknown',
themes: results.mobianThemes,
tones: results.mobianTones,
};
}

export function extendBidRequestConfig(bidReqConfig, contextData) {
logMessage('extendBidRequestConfig', bidReqConfig, contextData);
const { site: ortb2Site } = bidReqConfig.ortb2Fragments.global;

ortb2Site.ext = ortb2Site.ext || {};
ortb2Site.ext.data = {
...(ortb2Site.ext.data || {}),
...contextData
};

return bidReqConfig;
}

/**
* @param {MobianConfig} config
* @returns {boolean}
*/
function init(config) {
logMessage('init', config);

const parsedConfig = getConfig(config);

if (parsedConfig.publisherTargeting.length) {
getContextData().then((contextData) => setTargeting(parsedConfig, contextData));
}

return true;
}

function getBidRequestData(bidReqConfig, callback, config) {
logMessage('getBidRequestData', bidReqConfig);

const { advertiserTargeting } = getConfig(config);

if (!advertiserTargeting.length) {
callback();
return;
}

getContextData()
.then((contextData) => {
extendBidRequestConfig(bidReqConfig, contextData);
})
.catch(() => {})
.finally(() => callback());
}

/** @type {RtdSubmodule} */
export const mobianBrandSafetySubmodule = {
name: 'mobianBrandSafety',
init: init,
getBidRequestData: getBidRequestData
};

submodule('realTimeData', mobianBrandSafetySubmodule);
34 changes: 32 additions & 2 deletions modules/mobianRtdProvider.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
# Overview
# Mobian Rtd Provider

## Overview

Module Name: Mobian Rtd Provider
Module Type: Rtd Provider
Maintainer: [email protected]

# Description
## Description

RTD provider for themobian Brand Safety determinations. Publishers
should use this to get Mobian's GARM Risk evaluations for
a URL.

## Configuration

```js
pbjs.setConfig({
realTimeData: {
dataProviders: [{
name: 'mobianBrandSafety',
params: {
// Prefix for the targeting keys (default: 'mobian')
prefix: 'mobian',

// Enable targeting keys for advertiser data
advertiserTargeting: true,
// Or set it as an array to pick specific targeting keys:
// advertiserTargeting: ['genres', 'emotions', 'themes'],
// Available values: 'apValues', 'categories', 'emotions', 'genres', 'risk', 'sentiment', 'themes', 'tones'

// Enable targeting keys for publisher data
publisherTargeting: true,
// Or set it as an array to pick specific targeting keys:
// publisherTargeting: ['tones', 'risk'],
// Available values: 'apValues', 'categories', 'emotions', 'genres', 'risk', 'sentiment', 'themes', 'tones'
}
}]
}
});
```
Loading

0 comments on commit f7e8034

Please sign in to comment.