diff --git a/libraries/intentIqUtils/getCmpData.js b/libraries/intentIqUtils/getCmpData.js index b23f0fbaffe..cdf5ecf0e73 100644 --- a/libraries/intentIqUtils/getCmpData.js +++ b/libraries/intentIqUtils/getCmpData.js @@ -1,4 +1,4 @@ -import { allConsent } from '../../src/consentHandler.js'; +import { allConsent } from "../../src/consentHandler.js"; /** * Retrieves consent data from the Consent Management Platform (CMP). @@ -12,8 +12,26 @@ export function getCmpData() { return { gdprApplies: consentData?.gdpr?.gdprApplies || false, - gdprString: typeof consentData?.gdpr?.consentString === 'string' ? consentData.gdpr.consentString : null, - uspString: typeof consentData?.usp === 'string' ? consentData.usp : null, - gppString: typeof consentData?.gpp?.gppString === 'string' ? consentData.gpp.gppString : null, + gdprString: + typeof consentData?.gdpr?.consentString === "string" + ? consentData.gdpr.consentString + : null, + uspString: typeof consentData?.usp === "string" ? consentData.usp : null, + gppString: + typeof consentData?.gpp?.gppString === "string" + ? consentData.gpp.gppString + : null, }; } + +export function isValidValue(val) { + return !!val && val !== "undefined"; +} + +export function areCmpValuesEqual(a, b) { + const aValid = isValidValue(a); + const bValid = isValidValue(b); + if (!aValid && !bValid) return true; + if (aValid !== bValid) return false; + return a === b; +} diff --git a/modules/intentIqAnalyticsAdapter.js b/modules/intentIqAnalyticsAdapter.js index 2838b8ea1d4..8b24105f85f 100644 --- a/modules/intentIqAnalyticsAdapter.js +++ b/modules/intentIqAnalyticsAdapter.js @@ -6,7 +6,7 @@ import { EVENTS } from '../src/constants.js'; import { detectBrowser } from '../libraries/intentIqUtils/detectBrowserUtils.js'; import { appendSPData } from '../libraries/intentIqUtils/urlUtils.js'; import { appendVrrefAndFui, getCurrentUrl, getRelevantRefferer } from '../libraries/intentIqUtils/getRefferer.js'; -import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js'; +import { getCmpData, areCmpValuesEqual, isValidValue } from '../libraries/intentIqUtils/getCmpData.js'; import { getUnitPosition } from '../libraries/intentIqUtils/getUnitPosition.js'; import { VERSION, @@ -107,12 +107,23 @@ const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({ url: DEFAULT_URL, a case BID_WON: bidWon(args); break; - case BID_REQUESTED: + case BID_REQUESTED: { if (!alreadySubscribedOnGAM && shouldSubscribeOnGAM()) { alreadySubscribedOnGAM = true; gamPredictionReport(iiqConfig?.gamObjectReference, bidWon); } + const fpdFromGlobalObject = window[identityGlobalName]?.firstPartyData; + if (fpdFromGlobalObject) { + const currentCmpData = getCmpData(); + const hasCmpMismatch = ['gdprString', 'gppString', 'uspString'].some(field => + !areCmpValuesEqual(fpdFromGlobalObject[field], currentCmpData[field]) + ); + if (hasCmpMismatch) { + pbjs.refreshUserIds({ submoduleNames: ['intentIqId'] }); + } + } break; + } default: break; } @@ -460,9 +471,9 @@ function constructFullUrl(data) { PREBID + '&uh=' + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.clientHints) + - (cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : '') + - (cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : '') + - (cmpData.gdprString ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1' : '&gdpr=0'); + (isValidValue(cmpData.uspString) ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : '') + + (isValidValue(cmpData.gppString) ? '&gpp=' + encodeURIComponent(cmpData.gppString) : '') + + (isValidValue(cmpData.gdprString) ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1' : '&gdpr=0'); url = appendSPData(url, partnerData); url = appendVrrefAndFui(url, iiqAnalyticsAnalyticsAdapter.initOptions.domainName); diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 65b939c684f..57f9c7b1703 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -12,7 +12,7 @@ import { detectBrowser } from '../libraries/intentIqUtils/detectBrowserUtils.js' import { appendSPData } from '../libraries/intentIqUtils/urlUtils.js'; import { isCHSupported } from '../libraries/intentIqUtils/chUtils.js' import { appendVrrefAndFui } from '../libraries/intentIqUtils/getRefferer.js'; -import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js'; +import { getCmpData, areCmpValuesEqual, isValidValue } from '../libraries/intentIqUtils/getCmpData.js'; import { readData, storeData, defineStorageType, removeDataByKey, tryParse } from '../libraries/intentIqUtils/storageUtils.js'; import { FIRST_PARTY_KEY, @@ -112,11 +112,14 @@ function appendPartnersFirstParty(url, configParams) { } function appendCMPData(url, cmpData) { - url += cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : ''; - url += cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : ''; - url += cmpData.gdprApplies - ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1' - : '&gdpr=0'; + url += isValidValue(cmpData.uspString) ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : ''; + url += isValidValue(cmpData.gppString) ? '&gpp=' + encodeURIComponent(cmpData.gppString) : ''; + if (cmpData.gdprApplies) { + url += isValidValue(cmpData.gdprString) ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) : ''; + url += '&gdpr=1'; + } else { + url += '&gdpr=0'; + } return url } @@ -260,9 +263,9 @@ export function handleClientHints(clientHints) { } export function isCMPStringTheSame(fpData, cmpData) { - const firstPartyDataCPString = `${fpData.gdprString}${fpData.gppString}${fpData.uspString}`; - const cmpDataString = `${cmpData.gdprString}${cmpData.gppString}${cmpData.uspString}`; - return firstPartyDataCPString === cmpDataString; + return ['gdprString', 'gppString', 'uspString'].every(field => + areCmpValuesEqual(fpData[field], cmpData[field]) + ); } function updateCountersAndStore(runtimeEids, allowedStorage, partnerData) { diff --git a/test/spec/modules/intentIqAnalyticsAdapter_spec.js b/test/spec/modules/intentIqAnalyticsAdapter_spec.js index 1853ba511f1..ef0da85de3b 100644 --- a/test/spec/modules/intentIqAnalyticsAdapter_spec.js +++ b/test/spec/modules/intentIqAnalyticsAdapter_spec.js @@ -1096,4 +1096,164 @@ describe("IntentIQ tests all", function () { expect(decoded.abPercentage).to.equal(95); expect(decoded).to.not.have.property('userPercentage'); }); + + describe('BID_REQUESTED CMP mismatch detection', function () { + let refreshUserIdsStub; + let gppStub, uspStub, gdprStub; + + beforeEach(function () { + getGlobal().refreshUserIds = sinon.stub(); + refreshUserIdsStub = getGlobal().refreshUserIds; + gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns(null); + uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns(null); + gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns(null); + }); + + afterEach(function () { + delete getGlobal().refreshUserIds; + gppStub.restore(); + uspStub.restore(); + gdprStub.restore(); + }); + + it('should call refreshUserIds with intentIqId when gdprString changes', function () { + window[identityName].firstPartyData.gdprString = 'oldConsent'; + gdprStub.returns({ consentString: 'newConsent', gdprApplies: true }); + + events.emit(EVENTS.BID_REQUESTED); + + expect(refreshUserIdsStub.calledOnce).to.be.true; + expect(refreshUserIdsStub.calledWith({ submoduleNames: ['intentIqId'] })).to.be.true; + }); + + it('should call refreshUserIds when uspString changes from valid to another valid value', function () { + window[identityName].firstPartyData.uspString = '1YNN'; + uspStub.returns('1NNN'); + + events.emit(EVENTS.BID_REQUESTED); + + expect(refreshUserIdsStub.calledOnce).to.be.true; + }); + + it('should not call refreshUserIds when CMP data matches stored firstPartyData', function () { + window[identityName].firstPartyData.gdprString = 'sameConsent'; + window[identityName].firstPartyData.gppString = null; + window[identityName].firstPartyData.uspString = null; + gdprStub.returns({ consentString: 'sameConsent', gdprApplies: true }); + + events.emit(EVENTS.BID_REQUESTED); + + expect(refreshUserIdsStub.called).to.be.false; + }); + + it('should not call refreshUserIds when null and empty string are compared (both invalid)', function () { + window[identityName].firstPartyData.gdprString = null; + window[identityName].firstPartyData.gppString = null; + window[identityName].firstPartyData.uspString = null; + + events.emit(EVENTS.BID_REQUESTED); + + expect(refreshUserIdsStub.called).to.be.false; + }); + + it('should not call refreshUserIds when stored value is "undefined" string and current is null (both invalid)', function () { + window[identityName].firstPartyData.gdprString = ''; + window[identityName].firstPartyData.gppString = 'undefined'; + window[identityName].firstPartyData.uspString = 'undefined'; + + events.emit(EVENTS.BID_REQUESTED); + + expect(refreshUserIdsStub.called).to.be.false; + }); + + it('should call refreshUserIds when valid gdprString is replaced by null (invalid)', function () { + window[identityName].firstPartyData.gdprString = 'validConsent'; + window[identityName].firstPartyData.gppString = null; + window[identityName].firstPartyData.uspString = null; + + events.emit(EVENTS.BID_REQUESTED); + + expect(refreshUserIdsStub.calledOnce).to.be.true; + }); + + it('should not call refreshUserIds when firstPartyData is absent in global object', function () { + delete window[identityName].firstPartyData; + + events.emit(EVENTS.BID_REQUESTED); + + expect(refreshUserIdsStub.called).to.be.false; + }); + }); + + describe('constructFullUrl CMP isValidValue filtering', function () { + let gppStub, uspStub, gdprStub; + + afterEach(function () { + if (gppStub) gppStub.restore(); + if (uspStub) uspStub.restore(); + if (gdprStub) gdprStub.restore(); + }); + + it('should not include us_privacy when uspString is null', function () { + uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns(null); + gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns(null); + gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns(null); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + expect(server.requests[0].url).to.not.include('us_privacy'); + }); + + it('should not include us_privacy when uspString is the string "undefined"', function () { + uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns('undefined'); + gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns(null); + gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns(null); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + expect(server.requests[0].url).to.not.include('us_privacy'); + }); + + it('should not include gpp when gppString is null', function () { + uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns(null); + gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns(null); + gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns(null); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + expect(server.requests[0].url).to.not.include('&gpp='); + }); + + it('should not include gdpr_consent when gdprString is null', function () { + uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns(null); + gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns(null); + gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns(null); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + expect(server.requests[0].url).to.not.include('gdpr_consent'); + }); + + it('should not include gdpr_consent when gdprString is the string "undefined"', function () { + uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns(null); + gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns(null); + gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns({ consentString: 'undefined', gdprApplies: false }); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + expect(server.requests[0].url).to.not.include('gdpr_consent'); + }); + + it('should include gdpr_consent and gdpr=1 when gdprString is valid', function () { + const consentString = 'validConsent'; + uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns(null); + gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns(null); + gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns({ consentString, gdprApplies: true }); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + expect(server.requests[0].url).to.include(`gdpr_consent=${encodeURIComponent(consentString)}`); + expect(server.requests[0].url).to.include('gdpr=1'); + }); + }); }); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index e0e56338a3c..8325ec780aa 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -1345,6 +1345,103 @@ describe('IntentIQ tests', function () { expect(isCMPStringTheSame(fpData, cmpData)).to.be.false; }); + it('should return true when null and empty string are compared (both invalid)', function () { + const fpData = { gdprString: null, gppString: null, uspString: null }; + const cmpData = { gdprString: '', gppString: '', uspString: '' }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.true; + }); + + it('should return true when null and undefined are compared (both invalid)', function () { + const fpData = { gdprString: null, gppString: null, uspString: null }; + const cmpData = { gdprString: undefined, gppString: undefined, uspString: undefined }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.true; + }); + + it('should return true when "undefined" string and null are compared (both invalid)', function () { + const fpData = { gdprString: 'undefined', gppString: 'undefined', uspString: 'undefined' }; + const cmpData = { gdprString: null, gppString: null, uspString: null }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.true; + }); + + it('should return false when a valid value is compared against null', function () { + const fpData = { gdprString: 'consent123', gppString: null, uspString: null }; + const cmpData = { gdprString: null, gppString: null, uspString: null }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.false; + }); + + it('should return false when null is compared against a valid value', function () { + const fpData = { gdprString: null, gppString: null, uspString: null }; + const cmpData = { gdprString: 'consent123', gppString: null, uspString: null }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.false; + }); + + describe('appendCMPData via createPixelUrl', function () { + const baseParams = { partner: 'testPartner', domainName: 'example.com' }; + + it('should not include us_privacy in URL when uspString is null', function () { + const cmpData = { uspString: null, gppString: null, gdprApplies: false, gdprString: null }; + const url = createPixelUrl({}, undefined, baseParams, {}, cmpData); + expect(url).to.not.include('us_privacy'); + }); + + it('should not include us_privacy in URL when uspString is the string "undefined"', function () { + const cmpData = { uspString: 'undefined', gppString: null, gdprApplies: false, gdprString: null }; + const url = createPixelUrl({}, undefined, baseParams, {}, cmpData); + expect(url).to.not.include('us_privacy'); + }); + + it('should include us_privacy in URL when uspString is a valid string', function () { + const cmpData = { uspString: '1NYN', gppString: null, gdprApplies: false, gdprString: null }; + const url = createPixelUrl({}, undefined, baseParams, {}, cmpData); + expect(url).to.include(`&us_privacy=${encodeURIComponent('1NYN')}`); + }); + + it('should not include gpp in URL when gppString is null', function () { + const cmpData = { uspString: null, gppString: null, gdprApplies: false, gdprString: null }; + const url = createPixelUrl({}, undefined, baseParams, {}, cmpData); + expect(url).to.not.include('&gpp='); + }); + + it('should not include gpp in URL when gppString is the string "undefined"', function () { + const cmpData = { uspString: null, gppString: 'undefined', gdprApplies: false, gdprString: null }; + const url = createPixelUrl({}, undefined, baseParams, {}, cmpData); + expect(url).to.not.include('&gpp='); + }); + + it('should include gdpr=1 without gdpr_consent when gdprApplies is true and gdprString is null', function () { + const cmpData = { uspString: null, gppString: null, gdprApplies: true, gdprString: null }; + const url = createPixelUrl({}, undefined, baseParams, {}, cmpData); + expect(url).to.include('&gdpr=1'); + expect(url).to.not.include('gdpr_consent'); + }); + + it('should include gdpr=1 without gdpr_consent when gdprApplies is true and gdprString is "undefined"', function () { + const cmpData = { uspString: null, gppString: null, gdprApplies: true, gdprString: 'undefined' }; + const url = createPixelUrl({}, undefined, baseParams, {}, cmpData); + expect(url).to.include('&gdpr=1'); + expect(url).to.not.include('gdpr_consent'); + }); + + it('should include gdpr=1 and gdpr_consent when gdprApplies is true and gdprString is valid', function () { + const cmpData = { uspString: null, gppString: null, gdprApplies: true, gdprString: 'validConsent' }; + const url = createPixelUrl({}, undefined, baseParams, {}, cmpData); + expect(url).to.include('&gdpr=1'); + expect(url).to.include(`&gdpr_consent=${encodeURIComponent('validConsent')}`); + }); + + it('should include gdpr=0 and no gdpr_consent when gdprApplies is false', function () { + const cmpData = { uspString: null, gppString: null, gdprApplies: false, gdprString: null }; + const url = createPixelUrl({}, undefined, baseParams, {}, cmpData); + expect(url).to.include('&gdpr=0'); + expect(url).to.not.include('gdpr_consent'); + }); + }); + it('should run callback from params', async () => { let wasCallbackCalled = false const callbackConfigParams = {