From fe1e8bd101d042addf437a2d7a88075c4b46734e Mon Sep 17 00:00:00 2001 From: Antoine Niek Date: Wed, 24 Apr 2024 17:28:42 -0400 Subject: [PATCH 1/2] Add lmpIdSystem userId submodule --- modules/.submodules.json | 1 + modules/lmpIdSystem.js | 61 +++++++++++++ modules/lmpIdSystem.md | 27 ++++++ modules/userId/userId.md | 3 + test/spec/modules/lmpIdSystem_spec.js | 124 ++++++++++++++++++++++++++ 5 files changed, 216 insertions(+) create mode 100644 modules/lmpIdSystem.js create mode 100644 modules/lmpIdSystem.md create mode 100644 test/spec/modules/lmpIdSystem_spec.js diff --git a/modules/.submodules.json b/modules/.submodules.json index cfa98b5ab32..2a890d0630d 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -23,6 +23,7 @@ "justIdSystem", "kinessoIdSystem", "liveIntentIdSystem", + "lmpIdSystem", "lotamePanoramaIdSystem", "merkleIdSystem", "mwOpenLinkIdSystem", diff --git a/modules/lmpIdSystem.js b/modules/lmpIdSystem.js new file mode 100644 index 00000000000..b6dcae3118b --- /dev/null +++ b/modules/lmpIdSystem.js @@ -0,0 +1,61 @@ +/** + * This module adds lmpId support to the User ID module + * The {@link module:modules/userId} module is required. + * @module modules/lmpIdSystem + * @requires module:modules/userId + */ + +import { submodule } from '../src/hook.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const MODULE_NAME = 'lmpid'; +const STORAGE_KEY = '__lmpid'; +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); + +function readFromLocalStorage() { + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(STORAGE_KEY) : null; +} + +function getLmpid() { + return window[STORAGE_KEY] || readFromLocalStorage(); +} + +/** @type {Submodule} */ +export const lmpIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param { string | undefined } value + * @return { {lmpid: string} | undefined } + */ + decode(value) { + return value ? { lmpid: value } : undefined; + }, + + /** + * Retrieve the LMPID + * @function + * @param {SubmoduleConfig} config + * @return {{id: string | undefined} | undefined} + */ + getId(config) { + const id = getLmpid(); + return id ? { id } : undefined; + }, + + eids: { + 'lmpid': { + source: 'loblawmedia.ca', + atype: 3 + }, + } +}; + +submodule('userId', lmpIdSubmodule); diff --git a/modules/lmpIdSystem.md b/modules/lmpIdSystem.md new file mode 100644 index 00000000000..a56c9dbb3d6 --- /dev/null +++ b/modules/lmpIdSystem.md @@ -0,0 +1,27 @@ +# LMPID + +The Loblaw Media Private ID (LMPID) is the Loblaw Advance identity solution deployed by its media partners. LMPID leverages encrypted user registration information to provide a privacy-conscious, secure, and reliable identifier to power Loblaw Advance's digital advertising ecosystem. + +## LMPID Registration + +If you're a media company looking to partner with Loblaw Advance, please reach out to us through our [Contact page](https://www.loblawadvance.ca/contact-us) + +## LMPID Configuration + +First, make sure to add the LMPID submodule to your Prebid.js package with: + +``` +gulp build --modules=lmpIdSystem,userId +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'lmpid' + }] + } +}); +``` diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 7a01e128814..1ec109ff309 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -358,6 +358,9 @@ pbjs.setConfig({ }, { name: 'naveggId', + }, + { + name: 'lmpid', }], syncDelay: 5000 } diff --git a/test/spec/modules/lmpIdSystem_spec.js b/test/spec/modules/lmpIdSystem_spec.js new file mode 100644 index 00000000000..37c7351f143 --- /dev/null +++ b/test/spec/modules/lmpIdSystem_spec.js @@ -0,0 +1,124 @@ +import { expect } from 'chai'; +import { find } from 'src/polyfill.js'; +import { config } from 'src/config.js'; +import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; +import { storage, lmpIdSubmodule } from 'modules/lmpIdSystem.js'; +import { mockGdprConsent } from '../../helpers/consentData.js'; + +function getConfigMock() { + return { + userSync: { + syncDelay: 0, + userIds: [{ + name: 'lmpid' + }] + } + } +} + +function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: { banner: {}, native: {} }, + sizes: [ + [300, 200], + [300, 600] + ], + bids: [{ + bidder: 'sampleBidder', + params: { placementId: 'banner-only-bidder' } + }] + }; +} + +describe('LMPID System', () => { + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let windowLmpidStub; + + beforeEach(() => { + window.__lmpid = undefined; + windowLmpidStub = sinon.stub(window, '__lmpid'); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + }); + + afterEach(() => { + getDataFromLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); + windowLmpidStub.restore(); + }); + + describe('LMPID: test "getId" method', () => { + it('prefers the window cached LMPID', () => { + localStorageIsEnabledStub.returns(true); + getDataFromLocalStorageStub.withArgs('__lmpid').returns('stored-lmpid'); + + windowLmpidStub.value('lmpid'); + expect(lmpIdSubmodule.getId()).to.deep.equal({ id: 'lmpid' }); + }); + + it('fallbacks on localStorage when window cache is falsy', () => { + localStorageIsEnabledStub.returns(true); + getDataFromLocalStorageStub.withArgs('__lmpid').returns('stored-lmpid'); + + windowLmpidStub.value(''); + expect(lmpIdSubmodule.getId()).to.deep.equal({ id: 'stored-lmpid' }); + + windowLmpidStub.value(false); + expect(lmpIdSubmodule.getId()).to.deep.equal({ id: 'stored-lmpid' }); + }); + + it('fallbacks only if localStorageIsEnabled', () => { + localStorageIsEnabledStub.returns(false); + getDataFromLocalStorageStub.withArgs('__lmpid').returns('stored-lmpid'); + + expect(lmpIdSubmodule.getId()).to.be.undefined; + }); + }); + + describe('LMPID: test "decode" method', () => { + it('provides the lmpid from a stored object', () => { + expect(lmpIdSubmodule.decode('lmpid')).to.deep.equal({ lmpid: 'lmpid' }); + }); + }); + + describe('LMPID: requestBids hook', () => { + let adUnits; + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockGdprConsent(sandbox); + adUnits = [getAdUnitMock()]; + init(config); + setSubmoduleRegistry([lmpIdSubmodule]); + getDataFromLocalStorageStub.withArgs('__lmpid').returns('stored-lmpid'); + localStorageIsEnabledStub.returns(true); + config.setConfig(getConfigMock()); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('when a stored LMPID exists it is added to bids', (done) => { + requestBidsHook(() => { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.lmpid'); + expect(bid.userId.lmpid).to.equal('stored-lmpid'); + const lmpidAsEid = find(bid.userIdAsEids, e => e.source == 'loblawmedia.ca'); + expect(lmpidAsEid).to.deep.equal({ + source: 'loblawmedia.ca', + uids: [{ + id: 'stored-lmpid', + atype: 3, + }] + }); + }); + }); + done(); + }, { adUnits }); + }); + }); +}); From a2d7a9be457bf475a2bc651a417f016fbe6ab517 Mon Sep 17 00:00:00 2001 From: Antoine Niek Date: Wed, 1 May 2024 11:43:27 -0400 Subject: [PATCH 2/2] Use ortbConverter facility in mabidder --- modules/mabidderBidAdapter.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/mabidderBidAdapter.js b/modules/mabidderBidAdapter.js index 632403c6643..8d97f48f604 100644 --- a/modules/mabidderBidAdapter.js +++ b/modules/mabidderBidAdapter.js @@ -1,9 +1,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'mabidder'; export const baseUrl = 'https://prebid.ecdrsvc.com/bid'; +const converter = ortbConverter({}) + export const spec = { supportedMediaTypes: [BANNER], code: BIDDER_CODE, @@ -14,7 +17,8 @@ export const spec = { return !!(bid.params.ppid && bid.sizes && Array.isArray(bid.sizes) && Array.isArray(bid.sizes[0])) }, buildRequests: function(validBidRequests, bidderRequest) { - const fpd = bidderRequest.ortb2; + const fpd = converter.toORTB({ bidRequests: validBidRequests, bidderRequest: bidderRequest }); + const bids = []; validBidRequests.forEach(bidRequest => { const sizes = [];