From b4eccb043b7374cd8c469e70f424aaadcfee0e70 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 9 Oct 2024 15:22:38 +0100 Subject: [PATCH] 51Degrees RTD submodule: small improvements and fixes (#12302) * 51Degrees RTD submodule: calculate PPI based on physical screen size & small refactor in preparation for the next 51Degrees RTD submodule update * 51Degrees RTD submodule: add 51Degrees module to `.submodules.json` * 51Degrees RTD submodule: update documentation --------- Co-authored-by: Bohdan V <25197509+BohdanVV@users.noreply.github.com> --- modules/.submodules.json | 1 + modules/51DegreesRtdProvider.js | 130 +++++++--- modules/51DegreesRtdProvider.md | 62 +++-- .../spec/modules/51DegreesRtdProvider_spec.js | 241 +++++++++++++----- 4 files changed, 309 insertions(+), 125 deletions(-) diff --git a/modules/.submodules.json b/modules/.submodules.json index 3ac541ce4ea..36daa70e75b 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -58,6 +58,7 @@ ], "rtdModule": [ "1plusXRtdProvider", + "51DegreesRtdProvider", "a1MediaRtdProvider", "aaxBlockmeterRtdProvider", "adagioRtdProvider", diff --git a/modules/51DegreesRtdProvider.js b/modules/51DegreesRtdProvider.js index ec2e5235445..123eeeaddc3 100644 --- a/modules/51DegreesRtdProvider.js +++ b/modules/51DegreesRtdProvider.js @@ -1,6 +1,11 @@ import {loadExternalScript} from '../src/adloader.js'; import {submodule} from '../src/hook.js'; -import {prefixLog, deepAccess, mergeDeep} from '../src/utils.js'; +import { + deepAccess, + deepSetValue, + mergeDeep, + prefixLog, +} from '../src/utils.js'; const MODULE_NAME = '51Degrees'; export const LOG_PREFIX = `[${MODULE_NAME} RTD Submodule]:`; @@ -48,17 +53,49 @@ const ORTB_DEVICE_TYPE_MAP = new Map([ */ export const extractConfig = (moduleConfig, reqBidsConfigObj) => { // Resource key - const resourceKey = deepAccess(moduleConfig, 'params.resourceKey'); + let resourceKey = deepAccess(moduleConfig, 'params.resourceKey'); // On-premise JS URL - const onPremiseJSUrl = deepAccess(moduleConfig, 'params.onPremiseJSUrl'); + let onPremiseJSUrl = deepAccess(moduleConfig, 'params.onPremiseJSUrl'); + // Trim the values + if (typeof resourceKey === 'string') { + resourceKey = resourceKey.trim(); + } + if (typeof onPremiseJSUrl === 'string') { + onPremiseJSUrl = onPremiseJSUrl.trim(); + } + + // If this module is configured via a 3rd party wrapper, both form inputs + // might be mandatory. To handle this, 0 can be used as a value to skip + // the parameter. + if (typeof resourceKey === 'string' && resourceKey.trim() === '0') { + resourceKey = undefined; + } + if (typeof onPremiseJSUrl === 'string' && onPremiseJSUrl.trim() === '0') { + onPremiseJSUrl = undefined; + } + + // Verify that onPremiseJSUrl is a valid URL: either a full URL, relative + // path (/path/to/file.js), or a protocol-relative URL (//example.com/path/to/file.js) + if (typeof onPremiseJSUrl === 'string' && onPremiseJSUrl.length && !( + onPremiseJSUrl.startsWith('https://') || + onPremiseJSUrl.startsWith('http://') || + onPremiseJSUrl.startsWith('/')) + ) { + throw new Error(LOG_PREFIX + ' Invalid URL format for onPremiseJSUrl in moduleConfig'); + } + + // Verify that one of the parameters is provided, + // but not both at the same time 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'); } + + // Verify that the resource key is not the one provided as an example if (resourceKey === '') { - throw new Error(LOG_PREFIX + ' replace in configuration with a resource key obtained from https://configure.51degrees.com/tWrhNfY6'); + throw new Error(LOG_PREFIX + ' replace in configuration with a resource key obtained from https://configure.51degrees.com/HNZ75HT1'); } return {resourceKey, onPremiseJSUrl}; @@ -112,33 +149,57 @@ export const is51DegreesMetaPresent = () => { * @param {string} key The key to set * @param {any} value The value to set */ -export const setOrtb2KeyIfNotEmpty = (obj, key, value) => { +export const deepSetNotEmptyValue = (obj, key, value) => { if (!key) { throw new Error(LOG_PREFIX + ' Key is required'); } if (value) { - obj[key] = value; + deepSetValue(obj, key, value); } } +/** + * Converts all 51Degrees data to ORTB2 format + * + * @param {Object} data51 Response from 51Degrees API + * @param {Object} [data51.device] Device data + * + * @returns {Object} Enriched ORTB2 object + */ +export const convert51DegreesDataToOrtb2 = (data51) => { + let ortb2Data = {}; + + if (!data51) { + return ortb2Data; + } + + ortb2Data = convert51DegreesDeviceToOrtb2(data51.device); + + // placeholder for the next 51Degrees RTD submodule update + + return ortb2Data; +}; + /** * Converts 51Degrees device data to ORTB2 format * - * @param {Object} device + * @param {Object} device 51Degrees device object * @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] + * @param {string} [device.devicetype] Device type + * @param {string} [device.hardwarevendor] Hardware vendor + * @param {string} [device.hardwaremodel] Hardware model + * @param {string[]} [device.hardwarename] Hardware name + * @param {string} [device.platformname] Platform name + * @param {string} [device.platformversion] Platform version + * @param {number} [device.screenpixelsheight] Screen height in pixels + * @param {number} [device.screenpixelswidth] Screen width in pixels + * @param {number} [device.screenpixelsphysicalheight] Screen physical height in pixels + * @param {number} [device.screenpixelsphysicalwidth] Screen physical width in pixels + * @param {number} [device.pixelratio] Pixel ratio + * @param {number} [device.screeninchesheight] Screen height in inches * - * @returns {Object} + * @returns {Object} Enriched ORTB2 object */ export const convert51DegreesDeviceToOrtb2 = (device) => { const ortb2Device = {}; @@ -154,27 +215,26 @@ export const convert51DegreesDeviceToOrtb2 = (device) => { : null ); + const devicePhysicalPPI = device.screenpixelsphysicalheight && device.screeninchesheight + ? Math.round(device.screenpixelsphysicalheight / device.screeninchesheight) + : 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 - }; - } + deepSetNotEmptyValue(ortb2Device, 'devicetype', ORTB_DEVICE_TYPE_MAP.get(device.devicetype)); + deepSetNotEmptyValue(ortb2Device, 'make', device.hardwarevendor); + deepSetNotEmptyValue(ortb2Device, 'model', deviceModel); + deepSetNotEmptyValue(ortb2Device, 'os', device.platformname); + deepSetNotEmptyValue(ortb2Device, 'osv', device.platformversion); + deepSetNotEmptyValue(ortb2Device, 'h', device.screenpixelsphysicalheight || device.screenpixelsheight); + deepSetNotEmptyValue(ortb2Device, 'w', device.screenpixelsphysicalwidth || device.screenpixelswidth); + deepSetNotEmptyValue(ortb2Device, 'pxratio', device.pixelratio); + deepSetNotEmptyValue(ortb2Device, 'ppi', devicePhysicalPPI || devicePPI); + deepSetNotEmptyValue(ortb2Device, 'ext.fiftyonedegrees_deviceId', device.deviceid); - return ortb2Device; + return {device: ortb2Device}; } /** @@ -211,7 +271,7 @@ export const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, user logMessage('51Degrees raw data: ', data); mergeDeep( reqBidsConfigObj.ortb2Fragments.global, - {device: convert51DegreesDeviceToOrtb2(data.device)}, + convert51DegreesDataToOrtb2(data), ); logMessage('reqBidsConfigObj: ', reqBidsConfigObj); callback(); diff --git a/modules/51DegreesRtdProvider.md b/modules/51DegreesRtdProvider.md index 18f346d37a8..76fa73803c9 100644 --- a/modules/51DegreesRtdProvider.md +++ b/modules/51DegreesRtdProvider.md @@ -2,23 +2,23 @@ ## Overview - Module Name: 51Degrees Rtd Provider - Module Type: Rtd Provider + Module Name: 51Degrees RTD Provider + Module Type: RTD Provider Maintainer: support@51degrees.com ## Description -51Degrees module enriches an OpenRTB request with [51Degrees Device Data](https://51degrees.com/documentation/index.html). +The 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`, `pxratio` - 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 51Degrees module sets the following fields of the device object: `devicetype`, `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pxratio`. 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 device age, chipset, codec support, 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. +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. +A free resource key for use with 51Degrees cloud service can be obtained from [51Degrees cloud configuration](https://configure.51degrees.com/HNZ75HT1). 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). +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). +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 @@ -27,7 +27,7 @@ Free cloud and on premise solutions can be expanded to support unlimited request Compile the 51Degrees RTD Module with other modules and adapters into your Prebid.js build: ``` -gulp build --modules="rtdModule,51DegreesRtdProvider,appnexusBidAdapter,..." +gulp build --modules=rtdModule,51DegreesRtdProvider,appnexusBidAdapter,... ``` > Note that the 51Degrees RTD module is dependent on the global real-time data module, `rtdModule`. @@ -36,14 +36,14 @@ gulp build --modules="rtdModule,51DegreesRtdProvider,appnexusBidAdapter,..." #### Resource Key -In order to use the module please first obtain a Resource Key using the [Configurator tool](https://configure.51degrees.com/HNZ75HT1) - choose the following properties: +In order to use the module, please first obtain a Resource Key using the [Configurator tool](https://configure.51degrees.com/HNZ75HT1) - choose the following properties: * DeviceId * DeviceType * HardwareVendor * HardwareName * HardwareModel -* PlatformName +* PlatformName * PlatformVersion * ScreenPixelsHeight * ScreenPixelsWidth @@ -53,13 +53,13 @@ In order to use the module please first obtain a Resource Key using the [Configu * ScreenInchesWidth * PixelRatio -The Cloud API is **free** to integrate and use. To increase limits please check [51Degrees pricing](https://51degrees.com/pricing). +The Cloud API is **free** to integrate and use. To increase limits, please check [51Degrees pricing](https://51degrees.com/pricing). #### User Agent Client Hint (UA-CH) Permissions -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: +Some UA-CH headers are not available to third parties. To allow the 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: +In the HTML of the publisher's web page where the Prebid.js wrapper is integrated: ```html @@ -75,25 +75,25 @@ Accept-CH: sec-ch-ua-arch, sec-ch-ua-full-version, sec-ch-ua-full-version-list, 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. -##### Why not use GetHighEntropyValues API instead? +##### Why not use the GetHighEntropyValues API instead? Thanks for asking. -The script this module injects has a fall back to the GetHighEntropyValues API, but does not rely on it as a first (or only) choice route - please see the illustrative cases below. Albeit it seems easier, GHEV API is not supported by all browsers (so the decision to call it should be conditional) and also even in Chrome this API will likely be a subject to the Privacy Budget in the future. +The script this module injects has a fallback to the GetHighEntropyValues API but does not rely on it as a first (or only) choice route. Please see the illustrative cases below. Although it seems easier, the GHEV API is not supported by all browsers (so the decision to call it should be conditional). Also, even in Chrome, this API will likely be subject to the Privacy Budget in the future. -In summary we recommend using `Delegate-CH` http-equiv as the preferred method of obtaining the necessary evidence because it is the fastest and future proof method. +In summary, we recommend using `Delegate-CH` http-equiv as the preferred method of obtaining the necessary evidence because it is the fastest and most future-proof method. ##### Illustrative Cases -* if the device is iPhone/iPad then there is no point checking for or calling GetHighEntropyValues at the moment because iOS does not support this API. However this might change in the future. Platforms like iOS require additional techniques to identify the model which are not covered via a single API call, and change from version to version of the operating system and browser rendering engine. **When used with iOS 51Degrees resolves the [iPhone/iPad model groups](https://51degrees.com/documentation/4.4/_device_detection__features__apple_device_table.html) using these techniques.** That is one of the benefits the module brings to the Prebid community as most solutions do not resolve iPhone/iPad model groups. More on Apple Device Detection [here](https://51degrees.com/documentation/4.4/_device_detection__features__apple_detection.html). +* If the device is iPhone/iPad, there is no point in checking for or calling GetHighEntropyValues at the moment because iOS does not support this API. However, this might change in the future. Platforms like iOS require additional techniques to identify the model, which are not covered via a single API call, and change from version to version of the operating system and browser rendering engine. **When used with iOS, 51Degrees resolves the [iPhone/iPad model groups](https://51degrees.com/documentation/4.4/_device_detection__features__apple_device_table.html) using these techniques.** That is one of the benefits the module brings to the Prebid community, as most solutions do not resolve iPhone/iPad model groups. More on Apple Device Detection [here](https://51degrees.com/documentation/4.4/_device_detection__features__apple_detection.html). -* if the browser is Firefox on Android or Desktop then there is similarly no point requesting GHEV as the API is not supported. +* If the browser is Firefox on Android or Desktop, there is similarly no point in requesting GHEV, as the API is not supported. -* if the browser is Chrome then the `Delegate-CH` if enabled by the publisher would enable the browser to provide the necessary evidence. However if this is not implemented - then the dynamic script would fall back to GHEV which is slower. +* If the browser is Chrome, the `Delegate-CH`, if enabled by the publisher, would allow the browser to provide the necessary evidence. However, if this is not implemented, then the dynamic script would fall back to GHEV, which is slower. ### 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 `51Degrees` RTD provider. +This module is configured as part of the `realTimeData.dataProviders`. We recommend setting `auctionDelay` to at least 250 ms and ensuring `waitForIt` is set to `true` for the `51Degrees` RTD provider. ```javascript pbjs.setConfig({ @@ -107,7 +107,7 @@ pbjs.setConfig({ params: { resourceKey: '', // Get your resource key from https://configure.51degrees.com/HNZ75HT1 - // alternatively, you can use the on-premise version of the 51Degrees service and connect to your chosen end point + // alternatively, you can use the on-premise version of the 51Degrees service and connect to your chosen endpoint // onPremiseJSUrl: 'https://localhost/51Degrees.core.js' }, }, @@ -118,16 +118,18 @@ 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 or `onPremiseJSUrl` for the on-premise self-hosted integration. | Name | Type | Description | Default | |:----------------------|:--------|:---------------------------------------------------------------------------------------------|:-------------------| -| name | String | Real time data module name | Always '51Degrees' | +| 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) | | +> Note: if you use a third-party Prebid.js wrapper, there might be a chance that the UI will force you to input both `resourceKey` and `onPremiseJSUrl`. In this case, you can set a redundant parameter to a string equal to "0", which will be ignored by the module. + ## Example > Note: you need to have a valid resource key to run the example.\ @@ -147,4 +149,14 @@ 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 +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. + +## Optimisation + +To reduce latency when loading the 51Degrees cloud service script, it's recommended to preconnect to the 51Degrees domain. This will establish an early connection, allowing the browser to resolve DNS, set up TCP, and perform the TLS handshake ahead of time, speeding up the script download. + +To enable `preconnect`, add the following in the `` of your HTML: + +```html + +``` diff --git a/test/spec/modules/51DegreesRtdProvider_spec.js b/test/spec/modules/51DegreesRtdProvider_spec.js index 9b634970ebb..4847587c7d3 100644 --- a/test/spec/modules/51DegreesRtdProvider_spec.js +++ b/test/spec/modules/51DegreesRtdProvider_spec.js @@ -2,11 +2,13 @@ import { extractConfig, get51DegreesJSURL, is51DegreesMetaPresent, - setOrtb2KeyIfNotEmpty, + deepSetNotEmptyValue, + convert51DegreesDataToOrtb2, convert51DegreesDeviceToOrtb2, getBidRequestData, fiftyOneDegreesSubmodule, } from 'modules/51DegreesRtdProvider'; +import {mergeDeep} from '../../../src/utils'; const inject51DegreesMeta = () => { const meta = document.createElement('meta'); @@ -16,6 +18,60 @@ const inject51DegreesMeta = () => { }; describe('51DegreesRtdProvider', 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', + }; + + const fiftyOneDegreesDeviceX2scaling = { + ...fiftyOneDegreesDevice, + screenpixelsheight: fiftyOneDegreesDevice.screenpixelsheight / 2, + screenpixelswidth: fiftyOneDegreesDevice.screenpixelswidth / 2, + screenpixelsphysicalheight: fiftyOneDegreesDevice.screenpixelsheight, + screenpixelsphysicalwidth: fiftyOneDegreesDevice.screenpixelswidth, + pixelratio: fiftyOneDegreesDevice.pixelratio * 2, + }; + + const fiftyOneDegreesData = { + device: fiftyOneDegreesDevice, + }; + + const expectedORTB2DeviceResult = { + device: { + 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', + }, + }, + }; + + const expectedORTB2Result = {}; + mergeDeep( + expectedORTB2Result, + expectedORTB2DeviceResult, + // placeholder for the next 51Degrees RTD submodule update + ); + describe('extractConfig', function() { it('returns the resourceKey from the moduleConfig', function() { const reqBidsConfigObj = {}; @@ -55,6 +111,52 @@ describe('51DegreesRtdProvider', function() { const moduleConfig = {params: {resourceKey: ''}}; expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); }); + + it('sets the resourceKey to undefined if it was set to "0"', function() { + const moduleConfig = {params: { + resourceKey: '0', + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + }}; + expect(extractConfig(moduleConfig, {})).to.deep.equal({ + resourceKey: undefined, + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + }); + }); + + it('sets the onPremiseJSUrl to undefined if it was set to "0"', function() { + const moduleConfig = {params: { + resourceKey: 'TEST_RESOURCE_KEY', + onPremiseJSUrl: '0', + }}; + expect(extractConfig(moduleConfig, {})).to.deep.equal({ + resourceKey: 'TEST_RESOURCE_KEY', + onPremiseJSUrl: undefined, + }); + }); + + it('throws an error if the onPremiseJSUrl is not a valid URL', function() { + expect(() => extractConfig({ + params: {onPremiseJSUrl: 'invalid URL'} + }, {})).to.throw(); + expect(() => extractConfig({ + params: {onPremiseJSUrl: 'www.example.com/51Degrees.core.js'} + }, {})).to.throw(); + }); + + it('allows the onPremiseJSUrl to be a valid URL', function() { + const VALID_URLS = [ + 'https://www.example.com/51Degrees.core.js', + 'http://example.com/51Degrees.core.js', + '//example.com/51Degrees.core.js', + '/51Degrees.core.js', + ]; + + VALID_URLS.forEach(url => { + expect(() => extractConfig({ + params: {onPremiseJSUrl: url} + }, {})).to.not.throw(); + }); + }); }); describe('get51DegreesJSURL', function() { @@ -106,61 +208,53 @@ describe('51DegreesRtdProvider', function() { }); }); - describe('setOrtb2KeyIfNotEmpty', function() { + describe('deepSetNotEmptyValue', function() { it('sets value of ORTB2 key if it is not empty', function() { const data = {}; - setOrtb2KeyIfNotEmpty(data, 'TEST_ORTB2_KEY', 'TEST_ORTB2_VALUE'); + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', 'TEST_ORTB2_VALUE'); expect(data).to.deep.equal({TEST_ORTB2_KEY: 'TEST_ORTB2_VALUE'}); + deepSetNotEmptyValue(data, 'test2.TEST_ORTB2_KEY_2', 'TEST_ORTB2_VALUE_2'); + expect(data).to.deep.equal({ + TEST_ORTB2_KEY: 'TEST_ORTB2_VALUE', + test2: { + TEST_ORTB2_KEY_2: 'TEST_ORTB2_VALUE_2' + }, + }); }); it('throws an error if the key is empty', function() { const data = {}; - expect(() => setOrtb2KeyIfNotEmpty(data, '', 'TEST_ORTB2_VALUE')).to.throw(); + expect(() => deepSetNotEmptyValue(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); + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', ''); + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', 0); + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', null); + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', undefined); + deepSetNotEmptyValue(data, 'TEST.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', - }; + describe('convert51DegreesDataToOrtb2', function() { + it('returns empty object if data is null, undefined or empty', () => { + expect(convert51DegreesDataToOrtb2(null)).to.deep.equal({}); + expect(convert51DegreesDataToOrtb2(undefined)).to.deep.equal({}); + expect(convert51DegreesDataToOrtb2({})).to.deep.equal({}); + }); + it('converts all 51Degrees data to ORTB2 format', function() { + expect(convert51DegreesDataToOrtb2(fiftyOneDegreesData)).to.deep.equal(expectedORTB2Result); + }); + }); + + describe('convert51DegreesDeviceToOrtb2', function() { 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', - }, - }); + expect( + convert51DegreesDeviceToOrtb2(fiftyOneDegreesDevice) + ).to.deep.equal(expectedORTB2DeviceResult); }); it('returns an empty object if the device data is not provided', function() { @@ -170,37 +264,57 @@ describe('51DegreesRtdProvider', function() { 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'); + expect(convert51DegreesDeviceToOrtb2(device).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'}); + expect(convert51DegreesDeviceToOrtb2(device).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'); + expect(convert51DegreesDeviceToOrtb2(device).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'); + expect(convert51DegreesDeviceToOrtb2(device).device).to.not.have.any.keys('ppi'); + }); + + it('sets correct ppi if screenpixelsphysicalheight & screeninchesheight are provided', function() { + expect(convert51DegreesDeviceToOrtb2(fiftyOneDegreesDeviceX2scaling).device).to.deep.include({ + ppi: expectedORTB2DeviceResult.device.ppi, + }); + }); + + it('if screenpixelsphysical properties are available, use them for screen size', function() { + expect(fiftyOneDegreesDevice.screenpixelswidth).to.not.equal(fiftyOneDegreesDeviceX2scaling.screenpixelswidth); + expect(fiftyOneDegreesDevice.screenpixelsheight).to.not.equal(fiftyOneDegreesDeviceX2scaling.screenpixelsheight); + expect(fiftyOneDegreesDevice.screenpixelsphysicalwidth).to.equal(undefined); + expect(fiftyOneDegreesDevice.screenpixelsphysicalheight).to.equal(undefined); + expect(convert51DegreesDeviceToOrtb2(fiftyOneDegreesDeviceX2scaling).device).to.deep.include({ + h: expectedORTB2DeviceResult.device.h, + w: expectedORTB2DeviceResult.device.w, + }); }); }); describe('getBidRequestData', function() { let initialHeadInnerHTML; - const reqBidsConfigObj = { - ortb2Fragments: { - global: { - device: {}, + let reqBidsConfigObj = {}; + const resetReqBidsConfigObj = () => { + reqBidsConfigObj = { + ortb2Fragments: { + global: { + device: {}, + }, }, - }, + }; }; before(function() { @@ -208,27 +322,15 @@ describe('51DegreesRtdProvider', function() { const mockScript = document.createElement('script'); mockScript.innerHTML = ` - 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', - }; - window.fod = {complete: (_callback) => _callback({device: fiftyOneDegreesDevice})}; + window.fod = {complete: (_callback) => _callback(${JSON.stringify(fiftyOneDegreesData)})}; `; document.head.appendChild(mockScript); }); + beforeEach(function() { + resetReqBidsConfigObj(); + }); + after(function() { document.head.innerHTML = initialHeadInnerHTML; }); @@ -266,6 +368,15 @@ describe('51DegreesRtdProvider', function() { await new Promise(resolve => setTimeout(resolve, 100)); expect(callback.calledOnce).to.be.true; }); + + it('has the correct ORTB2 data', async function() { + const callback = sinon.spy(); + const moduleConfig = {params: {resourceKey: 'INVALID_RESOURCE_KEY'}}; + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(callback.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.equal(expectedORTB2Result); + }); }); describe('init', function() {