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

Basic Prebid.js Example

+
Div-1
+ +
+ +
+ + + diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 2653f157196..036effecd88 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -66,6 +66,7 @@ _each(NATIVE_ASSETS, anAsset => { _NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = export const spec = { VERSION: '1.5', code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, NATIVE], reId: /^[1-9][0-9]*$/, NATIVE_ASSET_ID_TO_KEY_MAP: _NATIVE_ASSET_ID_TO_KEY_MAP, @@ -177,6 +178,8 @@ export const spec = { return; } + const ortb2Data = bidderRequest?.ortb2 || {}; + let request = { id: deepAccess(bidderRequest, 'bidderRequestId'), site: {domain, page}, @@ -190,7 +193,11 @@ export const spec = { w: screen.width, language: getLanguage() }, - ext: {mgid_ver: spec.VERSION, prebid_ver: '$prebid.version$'}, + ext: { + mgid_ver: spec.VERSION, + prebid_ver: '$prebid.version$', + ...ortb2Data + }, imp }; if (bidderRequest && bidderRequest.gdprConsent) { 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..7a2f5908d00 --- /dev/null +++ b/modules/mgidRtdProvider.md @@ -0,0 +1,9 @@ +# Overview + +Module Name: Mgid RTD Provider +Module Type: RTD Provider +Maintainer: prebid@mgid.com + +# Description + +RTD provider for mgid.com. 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; + }); +});