diff --git a/modules/userId/index.js b/modules/userId/index.js index d0299427603..2a9d1f27072 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -165,9 +165,6 @@ export const dep = { isAllowed: isActivityAllowed } -/** @type {boolean} */ -let addedUserIdHook = false; - /** @type {SubmoduleContainer[]} */ let submodules = []; @@ -693,6 +690,25 @@ export const startAuctionHook = timedAuctionHook('userId', function requestBidsH }); }); +/** + * Append user id data from config to bids to be accessed in adapters when there are no submodules. + * @param {function} fn required; The next function in the chain, used by hook.js + * @param {Object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + */ +export const addUserIdsHook = timedAuctionHook('userId', function requestBidsHook(fn, reqBidsConfigObj) { + addIdData(reqBidsConfigObj); + // calling fn allows prebid to continue processing + fn.call(this, reqBidsConfigObj); +}); + +/** + * Is startAuctionHook added + * @returns {boolean} + */ +function addedStartAuctionHook() { + return !!startAuction.getHooks({hook: startAuctionHook}).length; +} + /** * This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well. * Simple use case will be passing these UserIds to A9 wrapper solution @@ -1110,11 +1126,11 @@ function updateSubmodules() { .forEach((sm) => submodules.push(sm)); if (submodules.length) { - if (!addedUserIdHook) { + if (!addedStartAuctionHook()) { + startAuction.getHooks({hook: addUserIdsHook}).remove(); startAuction.before(startAuctionHook, 100) // use higher priority than dataController / rtd adapterManager.callDataDeletionRequest.before(requestDataDeletion); coreGetPPID.after((next) => next(getPPID())); - addedUserIdHook = true; } logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules: `, submodules.map(a => a.submodule.name)); } @@ -1221,6 +1237,10 @@ export function init(config, {delay = GreedyPromise.timeout} = {}) { (getGlobal()).refreshUserIds = normalizePromise(refreshUserIds); (getGlobal()).getUserIdsAsync = normalizePromise(getUserIdsAsync); (getGlobal()).getUserIdsAsEidBySource = getUserIdsAsEidBySource; + if (!addedStartAuctionHook()) { + // Add ortb2.user.ext.eids even if 0 submodules are added + startAuction.before(addUserIdsHook, 100); // use higher priority than dataController / rtd + } } // init config update listener to start the application diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 6ebe533e260..c4f333e56ac 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -8,6 +8,7 @@ import { init, PBJS_USER_ID_OPTOUT_NAME, startAuctionHook, + addUserIdsHook, requestDataDeletion, setStoredValue, setSubmoduleRegistry, @@ -27,6 +28,7 @@ import {sharedIdSystemSubmodule} from 'modules/sharedIdSystem.js'; import {pubProvidedIdSubmodule} from 'modules/pubProvidedIdSystem.js'; import * as mockGpt from '../integration/faker/googletag.js'; import 'src/prebid.js'; +import {startAuction} from 'src/prebid'; import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; import {getPPID} from '../../../src/adserver.js'; @@ -175,6 +177,8 @@ describe('User ID', function () { afterEach(() => { sandbox.restore(); config.resetConfig(); + startAuction.getHooks({hook: startAuctionHook}).remove(); + startAuction.getHooks({hook: addUserIdsHook}).remove(); }); after(() => { @@ -2423,6 +2427,58 @@ describe('User ID', function () { }) }) }); + + describe('submodules not added', () => { + const eid = { + source: 'example.com', + uids: [{id: '1234', atype: 3}] + }; + let adUnits; + let startAuctionStub; + function saHook(fn, ...args) { + return startAuctionStub(...args); + } + beforeEach(() => { + adUnits = [{code: 'au1', bids: [{bidder: 'sampleBidder'}]}]; + startAuctionStub = sinon.stub(); + startAuction.before(saHook); + config.resetConfig(); + }); + afterEach(() => { + startAuction.getHooks({hook: saHook}).remove(); + }) + + it('addUserIdsHook', function (done) { + addUserIdsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userIdAsEids.0.source'); + expect(bid).to.have.deep.nested.property('userIdAsEids.0.uids.0.id'); + expect(bid.userIdAsEids[0].source).to.equal('example.com'); + expect(bid.userIdAsEids[0].uids[0].id).to.equal('1234'); + }); + }); + done(); + }, { + adUnits, + ortb2Fragments: { + global: {user: {ext: {eids: [eid]}}}, + bidder: {} + } + }); + }); + + it('should add userIdAsEids and merge ortb2.user.ext.eids even if no User ID submodules', () => { + init(config); + config.setConfig({ + ortb2: {user: {ext: {eids: [eid]}}} + }) + expect(startAuction.getHooks({hook: startAuctionHook}).length).equal(0); + expect(startAuction.getHooks({hook: addUserIdsHook}).length).equal(1); + $$PREBID_GLOBAL$$.requestBids({adUnits}); + sinon.assert.calledWith(startAuctionStub, sinon.match.hasNested('adUnits[0].bids[0].userIdAsEids[0]', eid)); + }); + }); }); describe('handles config with ESP configuration in user sync object', function() {