Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions libraries/intentIqUtils/getCmpData.js
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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;
}
21 changes: 16 additions & 5 deletions modules/intentIqAnalyticsAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
21 changes: 12 additions & 9 deletions modules/intentIqIdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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) {
Expand Down
160 changes: 160 additions & 0 deletions test/spec/modules/intentIqAnalyticsAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
97 changes: 97 additions & 0 deletions test/spec/modules/intentIqIdSystem_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading