diff --git a/integrationExamples/gpt/mgidRtdProvider_example.html b/integrationExamples/gpt/mgidRtdProvider_example.html new file mode 100644 index 00000000000..e3e4f720586 --- /dev/null +++ b/integrationExamples/gpt/mgidRtdProvider_example.html @@ -0,0 +1,143 @@ + + + + + + + + + + +JS Bin + + + +

Basic Prebid.js Example

+
Div-1
+ +
+ +
+ + + diff --git a/modules/.submodules.json b/modules/.submodules.json index e4d09b8c9df..aad778be67b 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -64,6 +64,7 @@ "iasRtdProvider", "jwplayerRtdProvider", "medianetRtdProvider", + "mgidRtdProvider", "oneKeyRtdProvider", "optimeraRtdProvider", "permutiveRtdProvider", diff --git a/modules/mgidRtdProvider.js b/modules/mgidRtdProvider.js new file mode 100644 index 00000000000..f30f14ea528 --- /dev/null +++ b/modules/mgidRtdProvider.js @@ -0,0 +1,190 @@ +import { submodule } from '../src/hook.js'; +import {ajax} from '../src/ajax.js'; +import {deepAccess, logError, logInfo, mergeDeep} from '../src/utils.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {getRefererInfo} from '../src/refererDetection.js'; + +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'mgid'; +const MGID_RTD_API_URL = 'https://servicer.mgid.com/sda'; +const MGUID_LOCAL_STORAGE_KEY = 'mguid'; +const ORTB2_NAME = 'www.mgid.com' + +const GVLID = 358; +/** @type {?Object} */ +export const storage = getStorageManager({ + gvlid: GVLID, + moduleName: SUBMODULE_NAME +}); + +function init(moduleConfig) { + if (!moduleConfig?.params?.clientSiteId) { + logError('Mgid clientSiteId is not set!'); + return false; + } + return true; +} + +function getBidRequestData(reqBidsConfigObj, onDone, moduleConfig, userConsent) { + let mguid; + try { + mguid = storage.getDataFromLocalStorage(MGUID_LOCAL_STORAGE_KEY); + } catch (e) { + logInfo(`Can't get mguid from localstorage`); + } + + const params = [ + { + name: 'gdprApplies', + data: typeof userConsent?.gdpr?.gdprApplies !== 'undefined' ? userConsent?.gdpr?.gdprApplies + '' : undefined, + }, + { + name: 'consentData', + data: userConsent?.gdpr?.consentString, + }, + { + name: 'uspString', + data: userConsent?.usp, + }, + { + name: 'cxurl', + data: encodeURIComponent(getContextUrl()), + }, + { + name: 'muid', + data: mguid, + }, + { + name: 'clientSiteId', + data: moduleConfig?.params?.clientSiteId, + }, + { + name: 'cxlang', + data: deepAccess(reqBidsConfigObj.ortb2Fragments.global, 'site.content.language'), + }, + ]; + + const url = MGID_RTD_API_URL + '?' + params.filter((p) => p.data).map((p) => p.name + '=' + p.data).join('&'); + + let isDone = false; + + ajax(url, { + success: (response, req) => { + if (req.status === 200) { + try { + const data = JSON.parse(response); + const ortb2 = reqBidsConfigObj?.ortb2Fragments?.global || {}; + + mergeDeep(ortb2, getDataForMerge(data)); + + if (data?.muid) { + try { + mguid = storage.setDataInLocalStorage(MGUID_LOCAL_STORAGE_KEY, data.muid); + } catch (e) { + logInfo(`Can't set mguid to localstorage`); + } + } + + onDone(); + isDone = true; + } catch (e) { + onDone(); + isDone = true; + + logError('Unable to parse Mgid RTD data', e); + } + } else { + onDone(); + isDone = true; + + logError('Mgid RTD wrong response status'); + } + }, + error: () => { + onDone(); + isDone = true; + + logError('Unable to get Mgid RTD data'); + } + }, + null, { + method: 'GET', + withCredentials: false, + }); + + setTimeout(function () { + if (!isDone) { + onDone(); + logInfo('Mgid RTD timeout'); + isDone = true; + } + }, moduleConfig.params.timeout || 1000); +} + +function getContextUrl() { + const refererInfo = getRefererInfo(); + + let resultUrl = refererInfo.canonicalUrl || refererInfo.topmostLocation; + + const metaElements = document.getElementsByTagName('meta'); + for (let i = 0; i < metaElements.length; i++) { + if (metaElements[i].getAttribute('property') === 'og:url') { + resultUrl = metaElements[i].content; + } + } + + return resultUrl; +} + +function getDataForMerge(responseData) { + let siteData = { + name: ORTB2_NAME + }; + let userData = { + name: ORTB2_NAME + }; + + if (responseData.siteSegments) { + siteData.segment = responseData.siteSegments.map((segmentId) => ({ id: segmentId })); + } + if (responseData.siteSegtax) { + siteData.ext = { + segtax: responseData.siteSegtax + } + } + + if (responseData.userSegments) { + userData.segment = responseData.userSegments.map((segmentId) => ({ id: segmentId })); + } + if (responseData.userSegtax) { + userData.ext = { + segtax: responseData.userSegtax + } + } + + let result = {}; + if (siteData.segment || siteData.ext) { + result.site = { + content: { + data: [siteData], + } + } + } + + if (userData.segment || userData.ext) { + result.user = { + data: [userData], + } + } + + return result; +} + +/** @type {RtdSubmodule} */ +export const mgidSubmodule = { + name: SUBMODULE_NAME, + init: init, + getBidRequestData: getBidRequestData, +}; + +submodule(MODULE_NAME, mgidSubmodule); diff --git a/modules/mgidRtdProvider.md b/modules/mgidRtdProvider.md new file mode 100644 index 00000000000..58d4564e14e --- /dev/null +++ b/modules/mgidRtdProvider.md @@ -0,0 +1,51 @@ +# Overview + +``` +Module Name: Mgid RTD Provider +Module Type: RTD Provider +Maintainer: prebid@mgid.com +``` + +# Description + +Mgid RTD module allows you to enrich bid data with contextual and audience signals, based on IAB taxonomies. + +## Configuration + +This module is configured as part of the `realTimeData.dataProviders` object. + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +|------------|----------|----------------------------------------|---------------|----------| +| `name ` | required | Real time data module name | `'mgid'` | `string` | +| `params` | required | | | `Object` | +| `params.clientSiteId` | required | The client site id provided by Mgid. | `'123456'` | `string` | +| `params.timeout` | optional | Maximum amount of milliseconds allowed for module to finish working | `1000` | `number` | + +#### Example + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'mgid', + params: { + clientSiteId: '123456' + } + }] + } +}); +``` + +## Integration +To install the module, follow these instructions: + +#### Step 1: Prepare the base Prebid file + +- Option 1: Use Prebid [Download](/download.html) page to build the prebid package. Ensure that you do check *Mgid Realtime Module* module + +- Option 2: From the command line, run `gulp build --modules=mgidRtdProvider,...` + +#### Step 2: Set configuration + +Enable Mgid Real Time Module using `pbjs.setConfig`. Example is provided in Configuration section. diff --git a/test/spec/modules/mgidRtdProvider_spec.js b/test/spec/modules/mgidRtdProvider_spec.js new file mode 100644 index 00000000000..4f70b4d8b7c --- /dev/null +++ b/test/spec/modules/mgidRtdProvider_spec.js @@ -0,0 +1,366 @@ +import { mgidSubmodule, storage } from '../../../modules/mgidRtdProvider.js'; +import {expect} from 'chai'; +import * as refererDetection from '../../../src/refererDetection'; + +describe('Mgid RTD submodule', () => { + let server; + let clock; + let getRefererInfoStub; + let getDataFromLocalStorageStub; + + beforeEach(() => { + server = sinon.fakeServer.create(); + + clock = sinon.useFakeTimers(); + + getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); + getRefererInfoStub.returns({ + canonicalUrl: 'https://www.test.com/abc' + }); + + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage').returns('qwerty654321'); + }); + + afterEach(() => { + server.restore(); + clock.restore(); + getRefererInfoStub.restore(); + getDataFromLocalStorageStub.restore(); + }); + + it('init is successfull, when clientSiteId is defined', () => { + expect(mgidSubmodule.init({params: {clientSiteId: 123}})).to.be.true; + }); + + it('init is unsuccessfull, when clientSiteId is not defined', () => { + expect(mgidSubmodule.init({})).to.be.false; + }); + + it('getBidRequestData send all params to our endpoint and succesfully modifies ortb2', () => { + const responseObj = { + userSegments: ['100', '200'], + userSegtax: 5, + siteSegments: ['300', '400'], + siteSegtax: 7, + muid: 'qwerty654321', + }; + + let reqBidsConfigObj = { + ortb2Fragments: { + global: { + site: { + content: { + language: 'en', + } + } + }, + } + }; + + let onDone = sinon.stub(); + + mgidSubmodule.getBidRequestData( + reqBidsConfigObj, + onDone, + {params: {clientSiteId: 123}}, + { + gdpr: { + gdprApplies: true, + consentString: 'testConsent', + }, + usp: '1YYY', + } + ); + + server.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(responseObj) + ); + + const requestUrl = new URL(server.requests[0].url); + expect(requestUrl.host).to.be.eq('servicer.mgid.com'); + expect(requestUrl.searchParams.get('gdprApplies')).to.be.eq('true'); + expect(requestUrl.searchParams.get('consentData')).to.be.eq('testConsent'); + expect(requestUrl.searchParams.get('uspString')).to.be.eq('1YYY'); + expect(requestUrl.searchParams.get('muid')).to.be.eq('qwerty654321'); + expect(requestUrl.searchParams.get('clientSiteId')).to.be.eq('123'); + expect(requestUrl.searchParams.get('cxurl')).to.be.eq('https://www.test.com/abc'); + expect(requestUrl.searchParams.get('cxlang')).to.be.eq('en'); + + assert.deepInclude( + reqBidsConfigObj.ortb2Fragments.global, + { + site: { + content: { + language: 'en', + data: [ + { + name: 'www.mgid.com', + ext: { + segtax: 7 + }, + segment: [ + { id: '300' }, + { id: '400' }, + ] + } + ], + } + }, + user: { + data: [ + { + name: 'www.mgid.com', + ext: { + segtax: 5 + }, + segment: [ + { id: '100' }, + { id: '200' }, + ] + } + ], + }, + }); + }); + + it('getBidRequestData doesn\'t send params (consent and cxlang), if we haven\'t received them', () => { + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + } + }; + + let onDone = sinon.stub(); + + mgidSubmodule.getBidRequestData( + reqBidsConfigObj, + onDone, + {params: {clientSiteId: 123}}, + {} + ); + + server.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify({}) + ); + + const requestUrl = new URL(server.requests[0].url); + expect(requestUrl.host).to.be.eq('servicer.mgid.com'); + expect(requestUrl.searchParams.get('gdprApplies')).to.be.null; + expect(requestUrl.searchParams.get('consentData')).to.be.null; + expect(requestUrl.searchParams.get('uspString')).to.be.null; + expect(requestUrl.searchParams.get('muid')).to.be.eq('qwerty654321'); + expect(requestUrl.searchParams.get('clientSiteId')).to.be.eq('123'); + expect(requestUrl.searchParams.get('cxurl')).to.be.eq('https://www.test.com/abc'); + expect(requestUrl.searchParams.get('cxlang')).to.be.null; + expect(onDone.calledOnce).to.be.true; + }); + + it('getBidRequestData send gdprApplies event if it is false', () => { + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + } + }; + + let onDone = sinon.stub(); + + mgidSubmodule.getBidRequestData( + reqBidsConfigObj, + onDone, + {params: {clientSiteId: 123}}, + { + gdpr: { + gdprApplies: false, + consentString: 'testConsent', + }, + usp: '1YYY', + } + ); + + server.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify({}) + ); + + const requestUrl = new URL(server.requests[0].url); + expect(requestUrl.host).to.be.eq('servicer.mgid.com'); + expect(requestUrl.searchParams.get('gdprApplies')).to.be.eq('false'); + expect(requestUrl.searchParams.get('consentData')).to.be.eq('testConsent'); + expect(requestUrl.searchParams.get('uspString')).to.be.eq('1YYY'); + expect(requestUrl.searchParams.get('muid')).to.be.eq('qwerty654321'); + expect(requestUrl.searchParams.get('clientSiteId')).to.be.eq('123'); + expect(requestUrl.searchParams.get('cxurl')).to.be.eq('https://www.test.com/abc'); + expect(requestUrl.searchParams.get('cxlang')).to.be.null; + expect(onDone.calledOnce).to.be.true; + }); + + it('getBidRequestData use og:url for cxurl, if it is available', () => { + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + } + }; + + let onDone = sinon.stub(); + + let metaStub = sinon.stub(document, 'getElementsByTagName').returns([ + { getAttribute: () => 'og:test', content: 'fake' }, + { getAttribute: () => 'og:url', content: 'https://realOgUrl.com/' } + ]); + + mgidSubmodule.getBidRequestData( + reqBidsConfigObj, + onDone, + {params: {clientSiteId: 123}}, + {} + ); + + server.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify({}) + ); + + const requestUrl = new URL(server.requests[0].url); + expect(requestUrl.searchParams.get('cxurl')).to.be.eq('https://realOgUrl.com/'); + expect(onDone.calledOnce).to.be.true; + + metaStub.restore(); + }); + + it('getBidRequestData use topMostLocation for cxurl, if nothing else left', () => { + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + } + }; + + let onDone = sinon.stub(); + + getRefererInfoStub.returns({ + topmostLocation: 'https://www.test.com/topMost' + }); + + mgidSubmodule.getBidRequestData( + reqBidsConfigObj, + onDone, + {params: {clientSiteId: 123}}, + {} + ); + + server.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify({}) + ); + + const requestUrl = new URL(server.requests[0].url); + expect(requestUrl.searchParams.get('cxurl')).to.be.eq('https://www.test.com/topMost'); + expect(onDone.calledOnce).to.be.true; + }); + + it('getBidRequestData won\'t modify ortb2 if response is broken', () => { + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + } + }; + + let onDone = sinon.stub(); + + mgidSubmodule.getBidRequestData( + reqBidsConfigObj, + onDone, + {params: {clientSiteId: 123}}, + {} + ); + + server.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + '{' + ); + + assert.deepEqual(reqBidsConfigObj.ortb2Fragments.global, {}); + expect(onDone.calledOnce).to.be.true; + }); + + it('getBidRequestData won\'t modify ortb2 if response status is not 200', () => { + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + } + }; + + let onDone = sinon.stub(); + + mgidSubmodule.getBidRequestData( + reqBidsConfigObj, + onDone, + {params: {clientSiteId: 123}}, + {} + ); + + server.requests[0].respond( + 204, + {'Content-Type': 'application/json'}, + '{}' + ); + + assert.deepEqual(reqBidsConfigObj.ortb2Fragments.global, {}); + expect(onDone.calledOnce).to.be.true; + }); + + it('getBidRequestData won\'t modify ortb2 if response results in error', () => { + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + } + }; + + let onDone = sinon.stub(); + + mgidSubmodule.getBidRequestData( + reqBidsConfigObj, + onDone, + {params: {clientSiteId: 123}}, + {} + ); + + server.requests[0].respond( + 500, + {'Content-Type': 'application/json'}, + '{}' + ); + + assert.deepEqual(reqBidsConfigObj.ortb2Fragments.global, {}); + expect(onDone.calledOnce).to.be.true; + }); + + it('getBidRequestData won\'t modify ortb2 if response time hits timeout', () => { + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + } + }; + + let onDone = sinon.stub(); + + mgidSubmodule.getBidRequestData( + reqBidsConfigObj, + onDone, + {params: {clientSiteId: 123, timeout: 500}}, + {} + ); + + clock.tick(510); + + assert.deepEqual(reqBidsConfigObj.ortb2Fragments.global, {}); + expect(onDone.calledOnce).to.be.true; + }); +});