From e243ff419ab282d1e5be71667c01cee35cdebbdd Mon Sep 17 00:00:00 2001 From: Bohdan V <25197509+BohdanVV@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:25:15 +0300 Subject: [PATCH 01/18] 51Degrees RTD provider --- .../gpt/51DegreesRtdProvider_example.html | 170 ++++++++++++ modules/51DegreesRtdProvider.js | 246 ++++++++++++++++++ modules/51DegreesRtdProvider.md | 114 ++++++++ .../spec/modules/51DegreesRtdProvider_spec.js | 232 +++++++++++++++++ 4 files changed, 762 insertions(+) create mode 100644 integrationExamples/gpt/51DegreesRtdProvider_example.html create mode 100644 modules/51DegreesRtdProvider.js create mode 100644 modules/51DegreesRtdProvider.md create mode 100644 test/spec/modules/51DegreesRtdProvider_spec.js diff --git a/integrationExamples/gpt/51DegreesRtdProvider_example.html b/integrationExamples/gpt/51DegreesRtdProvider_example.html new file mode 100644 index 00000000000..a5ad1bd153d --- /dev/null +++ b/integrationExamples/gpt/51DegreesRtdProvider_example.html @@ -0,0 +1,170 @@ + + + + + + + + + + + + 51Degrees RTD submodule example - Prebid.js + + +

51Degrees RTD submodule - example of usage

+ +

div-banner-native-1

+
+

No response

+ +
+ +

div-banner-native-2

+
+

No response

+ +
+ + diff --git a/modules/51DegreesRtdProvider.js b/modules/51DegreesRtdProvider.js new file mode 100644 index 00000000000..dbb852cdc50 --- /dev/null +++ b/modules/51DegreesRtdProvider.js @@ -0,0 +1,246 @@ +import {submodule} from '../src/hook.js'; +import {prefixLog, deepAccess, mergeDeep} from '../src/utils.js'; + +export const LOG_PREFIX = '[51Degrees RTD Submodule]:'; +const {logMessage, logWarn, logError} = prefixLog(LOG_PREFIX); + +// ORTB device types +const ORTB_DEVICE_TYPE = { + UNKNOWN: 0, + MOBILE_TABLET: 1, + PERSONAL_COMPUTER: 2, + CONNECTED_TV: 3, + PHONE: 4, + TABLET: 5, + CONNECTED_DEVICE: 6, + SET_TOP_BOX: 7, + OOH_DEVICE: 8 +}; + +// Map of 51Degrees device types to ORTB device types +const ORTB_DEVICE_TYPE_MAP = new Map([ + ['Phone', ORTB_DEVICE_TYPE.PHONE], + ['Console', ORTB_DEVICE_TYPE.SET_TOP_BOX], + ['Desktop', ORTB_DEVICE_TYPE.PERSONAL_COMPUTER], + ['EReader', ORTB_DEVICE_TYPE.PERSONAL_COMPUTER], + ['IoT', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['Kiosk', ORTB_DEVICE_TYPE.OOH_DEVICE], + ['MediaHub', ORTB_DEVICE_TYPE.SET_TOP_BOX], + ['Mobile', ORTB_DEVICE_TYPE.MOBILE_TABLET], + ['Router', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['SmallScreen', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['SmartPhone', ORTB_DEVICE_TYPE.MOBILE_TABLET], + ['SmartSpeaker', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['SmartWatch', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['Tablet', ORTB_DEVICE_TYPE.TABLET], + ['Tv', ORTB_DEVICE_TYPE.CONNECTED_TV], + ['Vehicle Display', ORTB_DEVICE_TYPE.PERSONAL_COMPUTER] +]); + +/** + * Extracts the parameters for 51Degrees RTD module from the config object passed at instantiation + * @param {Object} moduleConfig Configuration object of the 51Degrees RTD module + * @param {Object} reqBidsConfigObj Configuration object for the bidders, currently not used + */ +export const extractConfig = (moduleConfig, reqBidsConfigObj) => { + // Resource key + const resourceKey = deepAccess(moduleConfig, 'params.resourceKey'); + // On-premise JS URL + const onPremiseJSUrl = deepAccess(moduleConfig, 'params.onPremiseJSUrl'); + + if (!resourceKey && !onPremiseJSUrl) { + throw new Error(LOG_PREFIX + ' Missing parameter resourceKey or onPremiseJSUrl in moduleConfig'); + } else if (resourceKey && onPremiseJSUrl) { + throw new Error(LOG_PREFIX + ' Only one of resourceKey or onPremiseJSUrl should be provided in moduleConfig'); + } + + return {resourceKey, onPremiseJSUrl}; +} + +/** + * Gets 51Degrees JS URL + * @param {Object} pathData API path data + * @param {string} [pathData.resourceKey] Resource key + * @param {string} [pathData.onPremiseJSUrl] On-premise JS URL + * @returns {string} 51Degrees JS URL + */ +export const get51DegreesJSURL = (pathData) => { + if (pathData.onPremiseJSUrl) { + return pathData.onPremiseJSUrl; + } + return `https://cloud.51degrees.com/api/v4/${pathData.resourceKey}.js?fod-js-enable-cookies=false`; +} + +/** + * Injects 51Degrees script into the document head + * @param {string} url 51Degrees JS URL + * @returns {Promise} Promise that resolves when the script is loaded + */ +export const inject51DegreesScript = (url) => { + const script = document.createElement('script'); + script.src = url; + script.async = true; + document.head.appendChild(script); + + return new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = reject; + }); +} + +/** + * Check if meta[http-equiv="Delegate-CH"] tag is present in the document head and points to 51Degrees cloud + * + * @returns {boolean} True if 51Degrees meta is present + * @returns {boolean} False if 51Degrees meta is not present + */ +export const is51DegreesMetaPresent = () => { + const meta51 = document.head.querySelectorAll('meta[http-equiv="Delegate-CH"]'); + if (!meta51.length) { + return false; + } + return meta51.values().some(meta => meta.content.includes('cloud.51degrees.com')); +} + +/** + * Sets the value of a key in the ORTB2 object if the value is not empty + * + * @param {Object} obj The object to set the key in + * @param {string} key The key to set + * @param {any} value The value to set + */ +export const setOrtb2KeyIfNotEmpty = (obj, key, value) => { + if (!key) { + throw new Error(LOG_PREFIX + ' Key is required'); + } + + if (value) { + obj[key] = value; + } +} + +/** + * Converts 51Degrees device data to ORTB2 format + * + * @param {Object} device + * @param {string} [device.deviceid] Device ID (unique 51Degrees identifier) + * @param {string} [device.devicetype] + * @param {string} [device.hardwarevendor] + * @param {string} [device.hardwaremodel] + * @param {string[]} [device.hardwarename] + * @param {string} [device.platformname] + * @param {string} [device.platformversion] + * @param {number} [device.screenpixelsheight] + * @param {number} [device.screenpixelswidth] + * @param {number} [device.pixelratio] + * @param {number} [device.screeninchesheight] + * + * @returns {Object} + */ +export const convert51DegreesDeviceToOrtb2 = (device) => { + const ortb2Device = {}; + + if (!device) { + return ortb2Device; + } + + const deviceModel = + device.hardwaremodel || ( + device.hardwarename && device.hardwarename.length + ? device.hardwarename.join(',') + : null + ); + + const devicePPI = device.screenpixelsheight && device.screeninchesheight + ? Math.round(device.screenpixelsheight / device.screeninchesheight) + : null; + + setOrtb2KeyIfNotEmpty(ortb2Device, 'devicetype', ORTB_DEVICE_TYPE_MAP.get(device.devicetype)); + setOrtb2KeyIfNotEmpty(ortb2Device, 'make', device.hardwarevendor); + setOrtb2KeyIfNotEmpty(ortb2Device, 'model', deviceModel); + setOrtb2KeyIfNotEmpty(ortb2Device, 'os', device.platformname); + setOrtb2KeyIfNotEmpty(ortb2Device, 'osv', device.platformversion); + setOrtb2KeyIfNotEmpty(ortb2Device, 'h', device.screenpixelsheight); + setOrtb2KeyIfNotEmpty(ortb2Device, 'w', device.screenpixelswidth); + setOrtb2KeyIfNotEmpty(ortb2Device, 'pxratio', device.pixelratio); + setOrtb2KeyIfNotEmpty(ortb2Device, 'ppi', devicePPI); + + if (device.deviceid) { + ortb2Device.ext = { + 'fiftyonedegrees_deviceId': device.deviceid + }; + } + + return ortb2Device; +} + +/** + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Function} callback Called on completion + * @param {Object} moduleConfig Configuration for 1plusX RTD module + * @param {Object} userConsent + */ +export const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => { + try { + // Get the required config + const {resourceKey, onPremiseJSUrl} = extractConfig(moduleConfig, reqBidsConfigObj); + logMessage('Resource key: ', resourceKey); + logMessage('On-premise JS URL: ', onPremiseJSUrl); + + // Get 51Degrees JS URL, which is either cloud or on-premise + const scriptURL = get51DegreesJSURL(resourceKey ? {resourceKey} : {onPremiseJSUrl}); + logMessage('URL of the script to be injected: ', scriptURL); + + // Check if 51Degrees meta is present (cloud only) + if (resourceKey) { + logMessage('Checking if 51Degrees meta is present in the document head'); + if (!is51DegreesMetaPresent()) { + logWarn('Delegate-CH meta tag is not present in the document head'); + } + } + + // Inject 51Degrees script, get device data and merge it into the ORTB2 object + inject51DegreesScript(scriptURL) + .then(() => { + logMessage('Successfully injected 51Degrees script'); + const fod = /** @type {Object} */ (window.fod); + // Convert and merge device data in the callback + fod.complete((data) => { + logMessage('51Degrees raw data: ', data); + mergeDeep( + reqBidsConfigObj.ortb2Fragments.global, + {device: convert51DegreesDeviceToOrtb2(data.device)}, + ) + logMessage('reqBidsConfigObj: ', reqBidsConfigObj); + callback(); + }); + }) + .catch((error) => { + logError('Error injecting 51Degrees script: ', error); + callback(); + }); + } catch (error) { + // In case of an error, log it and continue + logError(error); + callback(); + } +} + +/** + * Init + * @param {Object} config Module configuration + * @param {boolean} userConsent User consent + * @returns true + */ +const init = (config, userConsent) => { + return true; +} + +// 51Degrees RTD submodule object to be registered +export const fiftyOneDegreesSubmodule = { + name: '51Degrees', + init, + getBidRequestData, +} + +submodule('realTimeData', fiftyOneDegreesSubmodule); diff --git a/modules/51DegreesRtdProvider.md b/modules/51DegreesRtdProvider.md new file mode 100644 index 00000000000..6231e4820a1 --- /dev/null +++ b/modules/51DegreesRtdProvider.md @@ -0,0 +1,114 @@ +# 51Degrees RTD Submodule + +## Overview + + Module Name: 51Degrees Rtd Provider + Module Type: Rtd Provider + Maintainer: engineering@51degrees.com + +## Description + +51Degrees module enriches an OpenRTB request with the device data using [51Degrees Cloud API](https://51degrees.com/documentation/4.4/index.html) (can also be used with a self-hosted 51Degrees service). 51Degrees detects and sets the following fields of the device object: `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pixelratio` - interested bidder adapters may use these fields as needed. In addition the module sets `device.ext.fiftyonedegrees_deviceId` to a permanent device ID which may be used with a 51Degrees data file to look up over 250 properties on the backend. + +## Usage + +### Integration + +Compile the 51Degrees RTD Module with other modules and adapters into your Prebid.js build: + +``` +gulp build --modules="rtdModule,51DegreesRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the 51Degrees RTD module is dependent on the global real-time data module, `rtdModule`. + +### Prerequisites + +#### Resource Key +In order to use the module please first obtain a Resource Key using the [Configurator tool](https://configure.51degrees.com/AQRVdgJ-9x1dNvxk3Eg) - choose the following properties: +* DeviceId +* DeviceType +* HardwareVendor +* HardwareName +* HardwareModel +* PlatformName +* PlatformVersion +* ScreenPixelsHeight +* ScreenPixelsWidth +* ScreenInchesHeight +* ScreenInchesWidth +* PixelRatio (optional) + +PixelRatio is desirable, but it's a paid property so will work either in a free trial mode or with a license. Also free API service is limited to 500,000 requests per month - consider picking a [51Degrees pricing plan](https://51degrees.com/pricing) that fits your needs. + +#### Client Hint Permissions + +Some client-hint headers are not available to third parties. To allow 51Degrees cloud service to access these headers for more accurate detection and lower latency, it is highly recommended to set `Permissions-Policy` in one of two ways: + +In the HTML of the publisher's web page where Prebid.js wrapper is integrated: + +```html + +``` + +Or in the Response Headers of the publisher's web server: + +```http +Permissions-Policy: ch-ua-arch=(self "https://cloud.51degrees.com"), ch-ua-full-version=(self "https://cloud.51degrees.com"), ch-ua-full-version-list=(self "https://cloud.51degrees.com"), ch-ua-model=(self "https://cloud.51degrees.com"), ch-ua-platform=(self "https://cloud.51degrees.com"), ch-ua-platform-version=(self "https://cloud.51degrees.com") + +Accept-CH: sec-ch-ua-arch, sec-ch-ua-full-version, sec-ch-ua-full-version-list, sec-ch-ua-model, sec-ch-ua-platform, sec-ch-ua-platform-version +``` + +### Configuration + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +pbjs.setConfig({ + debug: true, // we recommend turning this on for testing as it adds more logging + realTimeData: { + auctionDelay: 1000, // should be set lower in production use + dataProviders: [ + { + name: '51Degrees', + waitForIt: true, // should be true, otherwise the auctionDelay will be ignored + params: { + // Get your resource key from https://configure.51degrees.com/AQRVdgJ-9x1dNvxk3Eg + resourceKey: '', + // alternatively, you can use the on-premise version of the 51Degrees service + // onPremiseJSUrl: 'https://localhost/51Degrees.core.js' + }, + }, + ], + }, +}); +``` + +### Parameters + +> Note that `resourceKey` and `onPremiseJSUrl` are mutually exclusive parameters. Use strictly one of them: either a `resourceKey` for cloud integration and `onPremiseJSUrl` for the on-premise self-hosted integration. + +| Name | Type | Description | Default | +|:----------------------|:--------|:---------------------------------------------------------------------------------------------|:-------------------| +| name | String | Real time data module name | Always '51Degrees' | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (mandatory) | `false` | +| params | Object | | | +| params.resourceKey | String | Your 51Degrees Cloud Resource Key | | +| params.onPremiseJSUrl | String | Direct URL to your self-hosted on-premise JS file (e.g. https://localhost/51Degrees.core.js) | | + +## Example + +> Note: you need to have a valid resource key to run the example.\ +> It should be set in the configuration instead of ``.\ +> It is located in the `integrationExamples/gpt/51DegreesRtdProvider_example.html` file. + +If you want to see an example of how the 51Degrees RTD module works,\ +run the following command: + +`gulp serve --modules=rtdModule,51DegreesRtdProvider,appnexusBidAdapter` + +and then open the following URL in your browser: + +`http://localhost:9999/integrationExamples/gpt/51DegreesRtdProvider_example.html` + +Open the browser console to see the logs. diff --git a/test/spec/modules/51DegreesRtdProvider_spec.js b/test/spec/modules/51DegreesRtdProvider_spec.js new file mode 100644 index 00000000000..f3a40b66a23 --- /dev/null +++ b/test/spec/modules/51DegreesRtdProvider_spec.js @@ -0,0 +1,232 @@ +import { + extractConfig, + get51DegreesJSURL, + inject51DegreesScript, + is51DegreesMetaPresent, + setOrtb2KeyIfNotEmpty, + convert51DegreesDeviceToOrtb2, + getBidRequestData, + fiftyOneDegreesSubmodule, +} from 'modules/51DegreesRtdProvider'; + +describe('51DegreesRtdProvider', function() { + describe('extractConfig', function() { + it('returns the resourceKey from the moduleConfig', function() { + const reqBidsConfigObj = {}; + const moduleConfig = {params: {resourceKey: 'TEST_RESOURCE_KEY'}}; + expect(extractConfig(moduleConfig, reqBidsConfigObj)).to.deep.equal({ + resourceKey: 'TEST_RESOURCE_KEY', + onPremiseJSUrl: undefined, + }); + }); + + it('returns the onPremiseJSUrl from the moduleConfig', function() { + const reqBidsConfigObj = {}; + const moduleConfig = {params: {onPremiseJSUrl: 'https://example.com/51Degrees.core.js'}}; + expect(extractConfig(moduleConfig, reqBidsConfigObj)).to.deep.equal({ + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + resourceKey: undefined, + }); + }); + + it('throws an error if neither resourceKey nor onPremiseJSUrl is provided', function() { + const reqBidsConfigObj = {}; + const moduleConfig = {params: {}}; + expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); + }); + + it('throws an error if both resourceKey and onPremiseJSUrl are provided', function() { + const reqBidsConfigObj = {}; + const moduleConfig = {params: { + resourceKey: 'TEST_RESOURCE_KEY', + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + }}; + expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); + }); + }); + + describe('get51DegreesJSURL', function() { + it('returns the cloud URL if the resourceKey is provided', function() { + const config = {resourceKey: 'TEST_RESOURCE_KEY'}; + expect(get51DegreesJSURL(config)).to.equal( + 'https://cloud.51degrees.com/api/v4/TEST_RESOURCE_KEY.js?fod-js-enable-cookies=false' + ); + }); + + it('returns the on-premise URL if the onPremiseJSUrl is provided', function() { + const config = {onPremiseJSUrl: 'https://example.com/51Degrees.core.js'}; + expect(get51DegreesJSURL(config)).to.equal('https://example.com/51Degrees.core.js'); + }); + }); + + describe('inject51DegreesScript', function() { + let initialHeadInnerHTML; + + before(function() { + initialHeadInnerHTML = document.head.innerHTML; + }); + + afterEach(function() { + document.head.innerHTML = initialHeadInnerHTML; + }); + + it('injects the 51Degrees script into the document head', async function() { + await inject51DegreesScript('https://localhost/51Degrees.core.js').catch(() => { + // Ignore the error, since the script is not available + }); + const script = document.head.querySelector('script[src="https://localhost/51Degrees.core.js"]'); + expect(script).to.not.be.null; + }); + }); + + describe('is51DegreesMetaPresent', function() { + let initialHeadInnerHTML; + const inject51DegreesMeta = () => { + const meta = document.createElement('meta'); + meta.httpEquiv = 'Delegate-CH'; + meta.content = 'sec-ch-ua-full-version-list https://cloud.51degrees.com; sec-ch-ua-model https://cloud.51degrees.com; sec-ch-ua-platform https://cloud.51degrees.com; sec-ch-ua-platform-version https://cloud.51degrees.com'; + document.head.appendChild(meta); + }; + + before(function() { + initialHeadInnerHTML = document.head.innerHTML; + }); + + afterEach(function() { + document.head.innerHTML = initialHeadInnerHTML; + }); + + it('returns true if the 51Degrees meta tag is present', function () { + inject51DegreesMeta(); + expect(is51DegreesMetaPresent()).to.be.true; + }); + + it('returns false if the 51Degrees meta tag is not present', function() { + expect(is51DegreesMetaPresent()).to.be.false; + }); + }); + + describe('setOrtb2KeyIfNotEmpty', function() { + it('sets value of ORTB2 key if it is not empty', function() { + const data = {}; + setOrtb2KeyIfNotEmpty(data, 'TEST_ORTB2_KEY', 'TEST_ORTB2_VALUE'); + expect(data).to.deep.equal({TEST_ORTB2_KEY: 'TEST_ORTB2_VALUE'}); + }); + + it('throws an error if the key is empty', function() { + const data = {}; + expect(() => setOrtb2KeyIfNotEmpty(data, '', 'TEST_ORTB2_VALUE')).to.throw(); + }); + + it('does not set value of ORTB2 key if it is empty', function() { + const data = {}; + setOrtb2KeyIfNotEmpty(data, 'TEST_ORTB2_KEY', ''); + setOrtb2KeyIfNotEmpty(data, 'TEST_ORTB2_KEY', 0); + setOrtb2KeyIfNotEmpty(data, 'TEST_ORTB2_KEY', null); + setOrtb2KeyIfNotEmpty(data, 'TEST_ORTB2_KEY', undefined); + expect(data).to.deep.equal({}); + }); + }); + + describe('convert51DegreesDeviceToOrtb2', function() { + const fiftyOneDegreesDevice = { + 'screenpixelswidth': 5120, + 'screenpixelsheight': 1440, + 'hardwarevendor': 'Apple', + 'hardwaremodel': 'Macintosh', + 'hardwarename': [ + 'Macintosh', + ], + 'platformname': 'macOS', + 'platformversion': '14.1.2', + 'screeninchesheight': 13.27, + 'screenincheswidth': 47.17, + 'devicetype': 'Desktop', + 'pixelratio': 1, + 'deviceid': '17595-131215-132535-18092', + }; + + it('converts 51Degrees device data to ORTB2 format', function() { + expect(convert51DegreesDeviceToOrtb2(fiftyOneDegreesDevice)).to.deep.equal({ + devicetype: 2, + make: 'Apple', + model: 'Macintosh', + os: 'macOS', + osv: '14.1.2', + h: 1440, + w: 5120, + ppi: 109, + pxratio: 1, + ext: { + fiftyonedegrees_deviceId: '17595-131215-132535-18092', + }, + }); + }); + + it('returns an empty object if the device data is not provided', function() { + expect(convert51DegreesDeviceToOrtb2()).to.deep.equal({}); + }); + + it('does not set the deviceid if it is not provided', function() { + const device = {...fiftyOneDegreesDevice}; + delete device.deviceid; + expect(convert51DegreesDeviceToOrtb2(device)).to.not.have.any.keys('ext'); + }); + + it('sets the model to hardwarename if hardwaremodel is not provided', function() { + const device = {...fiftyOneDegreesDevice}; + delete device.hardwaremodel; + expect(convert51DegreesDeviceToOrtb2(device)).to.deep.include({model: 'Macintosh'}); + }); + + it('does not set the model if hardwarename is empty', function() { + const device = {...fiftyOneDegreesDevice}; + delete device.hardwaremodel; + device.hardwarename = []; + expect(convert51DegreesDeviceToOrtb2(device)).to.not.have.any.keys('model'); + }); + + it('does not set the ppi if screeninchesheight is not provided', function() { + const device = {...fiftyOneDegreesDevice}; + delete device.screeninchesheight; + expect(convert51DegreesDeviceToOrtb2(device)).to.not.have.any.keys('ppi'); + }); + }); + + describe('getBidRequestData', function() { + it('calls the callback even if submodule fails (wrong config)', function() { + const callback = sinon.spy(); + const moduleConfig = {params: {}}; + getBidRequestData({}, callback, moduleConfig, {}); + expect(callback.calledOnce).to.be.true; + }); + + it('calls the callback even if submodule fails (on-premise, non-working URL)', async function() { + const callback = sinon.spy(); + const moduleConfig = {params: {onPremiseJSUrl: 'http://localhost:12345/test/51Degrees.core.js'}}; + + getBidRequestData({}, callback, moduleConfig, {}); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(() => getBidRequestData( + {}, callback, moduleConfig, {} + )).to.not.throw(); + }); + + it('calls the callback even if submodule fails (invalid resource key)', async function() { + const callback = sinon.spy(); + const moduleConfig = {params: {resourceKey: 'INVALID_RESOURCE_KEY'}}; + + getBidRequestData({}, callback, moduleConfig, {}); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(() => getBidRequestData( + {}, callback, moduleConfig, {} + )).to.not.throw(); + }); + }); + + describe('init', function() { + it('initialises the 51Degrees RTD provider', function() { + expect(fiftyOneDegreesSubmodule.init()).to.be.true; + }); + }); +}); From c3b69ec80a4a4d99bf50f1b74bc219de3a86549b Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 29 Apr 2024 10:17:36 +0100 Subject: [PATCH 02/18] Amended comments and documentation to improve references for obtaining additional information and resources. Added an exception if the resourceKey parameter is not updated from the example. Added a customer notices section to the readme. Used User Agent Client Hint (UA-CH) consistently in the documentation. --- .../gpt/51DegreesRtdProvider_example.html | 3 +- modules/51DegreesRtdProvider.js | 9 ++++-- modules/51DegreesRtdProvider.md | 32 ++++++++++++++----- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/integrationExamples/gpt/51DegreesRtdProvider_example.html b/integrationExamples/gpt/51DegreesRtdProvider_example.html index a5ad1bd153d..9a6e8a1f48f 100644 --- a/integrationExamples/gpt/51DegreesRtdProvider_example.html +++ b/integrationExamples/gpt/51DegreesRtdProvider_example.html @@ -95,8 +95,9 @@ name: '51Degrees', waitForIt: true, params: { - // Get your resource key from https://configure.51degrees.com/AQRVdgJ-9x1dNvxk3Eg + // Get your resource key from https://configure.51degrees.com/tWrhNfY6 resourceKey: '', + // alternatively, you can use the on-premise version of the 51Degrees service and connect to your chosen end point // onPremiseJSUrl: 'https://localhost/51Degrees.core.js' } } diff --git a/modules/51DegreesRtdProvider.js b/modules/51DegreesRtdProvider.js index dbb852cdc50..21c8d8d00e7 100644 --- a/modules/51DegreesRtdProvider.js +++ b/modules/51DegreesRtdProvider.js @@ -17,7 +17,9 @@ const ORTB_DEVICE_TYPE = { OOH_DEVICE: 8 }; -// Map of 51Degrees device types to ORTB device types +// Map of 51Degrees device types to ORTB device types. See +// https://51degrees.com/developers/property-dictionary?item=Device%7CDevice +// for available properties and values. const ORTB_DEVICE_TYPE_MAP = new Map([ ['Phone', ORTB_DEVICE_TYPE.PHONE], ['Console', ORTB_DEVICE_TYPE.SET_TOP_BOX], @@ -53,6 +55,9 @@ export const extractConfig = (moduleConfig, reqBidsConfigObj) => { } else if (resourceKey && onPremiseJSUrl) { throw new Error(LOG_PREFIX + ' Only one of resourceKey or onPremiseJSUrl should be provided in moduleConfig'); } + if (resourceKey === '') { + throw new Error(LOG_PREFIX + ' replace in configuration with a resource key obtained from https://configure.51degrees.com/tWrhNfY6'); + } return {resourceKey, onPremiseJSUrl}; } @@ -68,7 +73,7 @@ export const get51DegreesJSURL = (pathData) => { if (pathData.onPremiseJSUrl) { return pathData.onPremiseJSUrl; } - return `https://cloud.51degrees.com/api/v4/${pathData.resourceKey}.js?fod-js-enable-cookies=false`; + return `https://cloud.51degrees.com/api/v4/${pathData.resourceKey}.js`; } /** diff --git a/modules/51DegreesRtdProvider.md b/modules/51DegreesRtdProvider.md index 6231e4820a1..f22f482a73a 100644 --- a/modules/51DegreesRtdProvider.md +++ b/modules/51DegreesRtdProvider.md @@ -8,7 +8,17 @@ ## Description -51Degrees module enriches an OpenRTB request with the device data using [51Degrees Cloud API](https://51degrees.com/documentation/4.4/index.html) (can also be used with a self-hosted 51Degrees service). 51Degrees detects and sets the following fields of the device object: `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pixelratio` - interested bidder adapters may use these fields as needed. In addition the module sets `device.ext.fiftyonedegrees_deviceId` to a permanent device ID which may be used with a 51Degrees data file to look up over 250 properties on the backend. +51Degrees module enriches an OpenRTB request with [51Degrees Device Data](https://51degrees.com/documentation/index.html). + +51Degrees module sets the following fields of the device object: `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pixelratio` - interested bidder adapters may use these fields as needed. In addition the module sets `device.ext.fiftyonedegrees_deviceId` to a permanent device ID which can be rapidly looked up in on premise data exposing over 250 properties including the device age, chip set, codec support, and price, operating system and app/browser versions, age, and embedded features. + +The module supports on premise and cloud device detection services with free options for both. + +A free resource key for use with 51Degrees cloud service can be obtained from [51Degrees cloud configuration](https://configure.51degrees.com/tWrhNfY6). This is the simplest approach to trial the module. + +An interface compatible self hosted service can be used with .NET, Java, Node, PHP, and Python. See [51Degrees examples](https://51degrees.com/documentation/_examples__device_detection__getting_started__web__on_premise.html). + +Free cloud and on premise solutions can be expanded to support unlimited requests, additional properties, and automatic daily on premise data updates via a [subscription](https://51degrees.com/pricing). ## Usage @@ -25,7 +35,7 @@ gulp build --modules="rtdModule,51DegreesRtdProvider,appnexusBidAdapter,..." ### Prerequisites #### Resource Key -In order to use the module please first obtain a Resource Key using the [Configurator tool](https://configure.51degrees.com/AQRVdgJ-9x1dNvxk3Eg) - choose the following properties: +In order to use the module please first obtain a Resource Key using the [Configurator tool](https://configure.51degrees.com/tWrhNfY6) - choose the following properties: * DeviceId * DeviceType * HardwareVendor @@ -39,11 +49,11 @@ In order to use the module please first obtain a Resource Key using the [Configu * ScreenInchesWidth * PixelRatio (optional) -PixelRatio is desirable, but it's a paid property so will work either in a free trial mode or with a license. Also free API service is limited to 500,000 requests per month - consider picking a [51Degrees pricing plan](https://51degrees.com/pricing) that fits your needs. +PixelRatio is desirable, but it's a paid property requiring a paid license. Also free API service is limited to 500,000 requests per month - consider picking a [51Degrees pricing plan](https://51degrees.com/pricing) that fits your needs. -#### Client Hint Permissions +#### User Agent Client Hint (UA-CH) Permissions -Some client-hint headers are not available to third parties. To allow 51Degrees cloud service to access these headers for more accurate detection and lower latency, it is highly recommended to set `Permissions-Policy` in one of two ways: +Some UA-CH headers are not available to third parties. To allow 51Degrees cloud service to access these headers for more accurate detection and lower latency, it is highly recommended to set `Permissions-Policy` in one of two ways: In the HTML of the publisher's web page where Prebid.js wrapper is integrated: @@ -59,6 +69,8 @@ Permissions-Policy: ch-ua-arch=(self "https://cloud.51degrees.com"), ch-ua-full- Accept-CH: sec-ch-ua-arch, sec-ch-ua-full-version, sec-ch-ua-full-version-list, sec-ch-ua-model, sec-ch-ua-platform, sec-ch-ua-platform-version ``` +See the [51Degrees documentation](https://51degrees.com/documentation/_device_detection__features__u_a_c_h__overview.html) for more information concerning UA-CH and permissions. + ### Configuration This module is configured as part of the `realTimeData.dataProviders` @@ -73,9 +85,9 @@ pbjs.setConfig({ name: '51Degrees', waitForIt: true, // should be true, otherwise the auctionDelay will be ignored params: { - // Get your resource key from https://configure.51degrees.com/AQRVdgJ-9x1dNvxk3Eg + // Get your resource key from https://configure.51degrees.com/tWrhNfY6 to connect to cloud.51degrees.com resourceKey: '', - // alternatively, you can use the on-premise version of the 51Degrees service + // alternatively, you can use the on-premise version of the 51Degrees service and connect to your chosen end point // onPremiseJSUrl: 'https://localhost/51Degrees.core.js' }, }, @@ -86,7 +98,7 @@ pbjs.setConfig({ ### Parameters -> Note that `resourceKey` and `onPremiseJSUrl` are mutually exclusive parameters. Use strictly one of them: either a `resourceKey` for cloud integration and `onPremiseJSUrl` for the on-premise self-hosted integration. +> Note that `resourceKey` and `onPremiseJSUrl` are mutually exclusive parameters. Use strictly one of them: either a `resourceKey` for cloud integration and `onPremiseJSUrl` for the on-premise self-hosted integration. | Name | Type | Description | Default | |:----------------------|:--------|:---------------------------------------------------------------------------------------------|:-------------------| @@ -112,3 +124,7 @@ and then open the following URL in your browser: `http://localhost:9999/integrationExamples/gpt/51DegreesRtdProvider_example.html` Open the browser console to see the logs. + +## Customer Notices + +When using the 51Degrees cloud service publishers need to reference the 51Degrees [client services privacy policy](https://51degrees.com/terms/client-services-privacy-policy) in their customer notices. \ No newline at end of file From 989a5abf865dc4bacb2b626859f8c56da2d7c08a Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 29 Apr 2024 10:22:45 +0100 Subject: [PATCH 03/18] Modified the maintainer email address. --- modules/51DegreesRtdProvider.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/51DegreesRtdProvider.md b/modules/51DegreesRtdProvider.md index f22f482a73a..92618725621 100644 --- a/modules/51DegreesRtdProvider.md +++ b/modules/51DegreesRtdProvider.md @@ -4,7 +4,7 @@ Module Name: 51Degrees Rtd Provider Module Type: Rtd Provider - Maintainer: engineering@51degrees.com + Maintainer: support@51degrees.com ## Description From f50b14b295995fe2b9ecdc0f15ece9186bf0e784 Mon Sep 17 00:00:00 2001 From: Bohdan V <25197509+bohdanvv@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:26:53 +0300 Subject: [PATCH 04/18] Change outdated URL --- test/spec/modules/51DegreesRtdProvider_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/modules/51DegreesRtdProvider_spec.js b/test/spec/modules/51DegreesRtdProvider_spec.js index f3a40b66a23..519d0f2d975 100644 --- a/test/spec/modules/51DegreesRtdProvider_spec.js +++ b/test/spec/modules/51DegreesRtdProvider_spec.js @@ -49,7 +49,7 @@ describe('51DegreesRtdProvider', function() { it('returns the cloud URL if the resourceKey is provided', function() { const config = {resourceKey: 'TEST_RESOURCE_KEY'}; expect(get51DegreesJSURL(config)).to.equal( - 'https://cloud.51degrees.com/api/v4/TEST_RESOURCE_KEY.js?fod-js-enable-cookies=false' + 'https://cloud.51degrees.com/api/v4/TEST_RESOURCE_KEY.js' ); }); From e0663e87a13eb02bba9a35431bdf7bd039fb5c47 Mon Sep 17 00:00:00 2001 From: Bohdan V <25197509+bohdanvv@users.noreply.github.com> Date: Mon, 29 Apr 2024 16:54:54 +0300 Subject: [PATCH 05/18] Adjust code to work on legacy browsers --- modules/51DegreesRtdProvider.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/51DegreesRtdProvider.js b/modules/51DegreesRtdProvider.js index 21c8d8d00e7..4f492bb8ec8 100644 --- a/modules/51DegreesRtdProvider.js +++ b/modules/51DegreesRtdProvider.js @@ -104,7 +104,11 @@ export const is51DegreesMetaPresent = () => { if (!meta51.length) { return false; } - return meta51.values().some(meta => meta.content.includes('cloud.51degrees.com')); + return Array.from(meta51).some( + meta => !meta.content + ? false + : meta.content.includes('cloud.51degrees.com') + ); } /** From 45761cc40823247a776e08faf6f17fde4d2720fe Mon Sep 17 00:00:00 2001 From: Bohdan V <25197509+bohdanvv@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:37:45 +0300 Subject: [PATCH 06/18] Refactor a test of the `inject` method --- test/spec/modules/51DegreesRtdProvider_spec.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/spec/modules/51DegreesRtdProvider_spec.js b/test/spec/modules/51DegreesRtdProvider_spec.js index 519d0f2d975..0d9b74d7ae6 100644 --- a/test/spec/modules/51DegreesRtdProvider_spec.js +++ b/test/spec/modules/51DegreesRtdProvider_spec.js @@ -70,12 +70,16 @@ describe('51DegreesRtdProvider', function() { document.head.innerHTML = initialHeadInnerHTML; }); - it('injects the 51Degrees script into the document head', async function() { - await inject51DegreesScript('https://localhost/51Degrees.core.js').catch(() => { - // Ignore the error, since the script is not available - }); - const script = document.head.querySelector('script[src="https://localhost/51Degrees.core.js"]'); - expect(script).to.not.be.null; + it('injects the 51Degrees script into the document head', function(done) { + inject51DegreesScript('https://localhost/51Degrees.core.js') + .catch(() => { + // Ignore the error, since the script is not available + }) + .finally(() => { + const script = document.head.querySelector('script[src="https://localhost/51Degrees.core.js"]'); + expect(script).to.not.be.null; + done(); + }); }); }); From bd246314b4607da48fe07ea2275130e7dc10fcc2 Mon Sep 17 00:00:00 2001 From: Bohdan V <25197509+bohdanvv@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:53:38 +0300 Subject: [PATCH 07/18] Replace URL in a test method --- test/spec/modules/51DegreesRtdProvider_spec.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/spec/modules/51DegreesRtdProvider_spec.js b/test/spec/modules/51DegreesRtdProvider_spec.js index 0d9b74d7ae6..e35841d4746 100644 --- a/test/spec/modules/51DegreesRtdProvider_spec.js +++ b/test/spec/modules/51DegreesRtdProvider_spec.js @@ -71,12 +71,14 @@ describe('51DegreesRtdProvider', function() { }); it('injects the 51Degrees script into the document head', function(done) { - inject51DegreesScript('https://localhost/51Degrees.core.js') + inject51DegreesScript('https://localhost.fake:12345/51Degrees.core.js') .catch(() => { // Ignore the error, since the script is not available }) .finally(() => { - const script = document.head.querySelector('script[src="https://localhost/51Degrees.core.js"]'); + const script = document.head.querySelector( + 'script[src="https://localhost.fake:12345/51Degrees.core.js"]' + ); expect(script).to.not.be.null; done(); }); From d4d6f2c14400cc5b2c037f0f075ba4fa81bc42f2 Mon Sep 17 00:00:00 2001 From: Bohdan V <25197509+BohdanVV@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:56:40 +0300 Subject: [PATCH 08/18] 51Degrees RTD provider: remove redundant parameter from the example --- integrationExamples/gpt/51DegreesRtdProvider_example.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/integrationExamples/gpt/51DegreesRtdProvider_example.html b/integrationExamples/gpt/51DegreesRtdProvider_example.html index 9a6e8a1f48f..dd5d90c3327 100644 --- a/integrationExamples/gpt/51DegreesRtdProvider_example.html +++ b/integrationExamples/gpt/51DegreesRtdProvider_example.html @@ -85,9 +85,6 @@ pbjs.setConfig({ debug: true, - cache: { - url: false - }, realTimeData: { auctionDelay: 1000, // should be set lower in production use dataProviders: [ From b7a42589554ca43a6d7f598880402eff49469625 Mon Sep 17 00:00:00 2001 From: Bohdan V <25197509+BohdanVV@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:01:29 +0300 Subject: [PATCH 09/18] 51Degrees RTD provider: update gpt.js URL in the example file --- integrationExamples/gpt/51DegreesRtdProvider_example.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrationExamples/gpt/51DegreesRtdProvider_example.html b/integrationExamples/gpt/51DegreesRtdProvider_example.html index dd5d90c3327..17d130144dc 100644 --- a/integrationExamples/gpt/51DegreesRtdProvider_example.html +++ b/integrationExamples/gpt/51DegreesRtdProvider_example.html @@ -2,7 +2,7 @@ - +