From 80565144db66d240a7ea6de05cfe8bde7fc0d244 Mon Sep 17 00:00:00 2001 From: dmytro-po Date: Wed, 27 Nov 2024 14:35:35 +0200 Subject: [PATCH] IntentIq ID & Analytics Modules : CMP values and browser detection bug fix (#12511) * update intentIqAnalyticsAdapter.js && intentIqIdSystem.js * fix lint issues * fix tests * move info * resolve issues * update storeFirstPartyData * remove unused code * update defineEmptyDataAndFireCallback * update fix lint * update reportExternalWin * small fixes * update test && add docs * AGT-347: Support domain name * AGT-347: Support domain name * AGT-374: Support domainName to vrref * AGT-374: tests in progress * AGT-374: Remove duplicate encoded in getRelevantRefferer and fix tests * AGT-374: Add test domainName, changes in documentation * AGT-374: Change js version value * AGT-374: Remove extra coma * Remove unused method * AGT-384: gpp string value * AGT-384: GPC value, browserDetector fix * AGT-384: Remove gpc logic * AGT-384: Reduce getGppValue method * AGT-384: Gpp tests and prevent send ids from LS in group B * AGT-384: Change empty array of eids * AGT-384: Some fixes after review --------- Co-authored-by: dlepetynskyi Co-authored-by: DimaIntentIQ Co-authored-by: DimaIntentIQ <139111483+DimaIntentIQ@users.noreply.github.com> --- .../intentIqConstants/intentIqConstants.js | 2 +- libraries/intentIqUtils/detectBrowserUtils.js | 10 ++ libraries/intentIqUtils/getGppValue.js | 16 +++ modules/intentIqAnalyticsAdapter.js | 6 +- modules/intentIqIdSystem.js | 51 +++----- test/spec/modules/intentIqIdSystem_spec.js | 118 ++++++++---------- 6 files changed, 99 insertions(+), 104 deletions(-) create mode 100644 libraries/intentIqUtils/getGppValue.js diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index 56f014be4c1..af7f7722213 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -7,4 +7,4 @@ export const OPT_OUT = 'O'; export const BLACK_LIST = 'L'; export const CLIENT_HINTS_KEY = '_iiq_ch'; export const EMPTY = 'EMPTY' -export const VERSION = 0.22 +export const VERSION = 0.23 diff --git a/libraries/intentIqUtils/detectBrowserUtils.js b/libraries/intentIqUtils/detectBrowserUtils.js index 0606388a346..c7004c77ae9 100644 --- a/libraries/intentIqUtils/detectBrowserUtils.js +++ b/libraries/intentIqUtils/detectBrowserUtils.js @@ -32,6 +32,16 @@ export function detectBrowserFromUserAgent(userAgent) { ie: /MSIE|Trident/, }; + // Check for Edge first + if (browserRegexPatterns.edge.test(userAgent)) { + return 'edge'; + } + + // Check for Opera next + if (browserRegexPatterns.opera.test(userAgent)) { + return 'opera'; + } + // Check for Chrome first to avoid confusion with Safari if (browserRegexPatterns.chrome.test(userAgent)) { return 'chrome'; diff --git a/libraries/intentIqUtils/getGppValue.js b/libraries/intentIqUtils/getGppValue.js new file mode 100644 index 00000000000..9c538b4f753 --- /dev/null +++ b/libraries/intentIqUtils/getGppValue.js @@ -0,0 +1,16 @@ +import {gppDataHandler} from '../../src/consentHandler.js'; + +/** + * Retrieves the GPP string value and additional GPP-related information. + * This function extracts the GPP data, encodes it, and determines specific GPP flags such as GPI and applicable sections. + * @return {Object} An object containing: + * - `gppString` (string): The encoded GPP string value. + * - `gpi` (number): An indicator representing whether GPP consent is available (0 if available, 1 if not). + */ +export function getGppValue() { + const gppData = gppDataHandler.getConsentData(); + const gppString = gppData?.gppString || ''; + const gpi = gppString ? 0 : 1; + + return { gppString, gpi }; +} diff --git a/modules/intentIqAnalyticsAdapter.js b/modules/intentIqAnalyticsAdapter.js index a3113b7b089..1779152c5f0 100644 --- a/modules/intentIqAnalyticsAdapter.js +++ b/modules/intentIqAnalyticsAdapter.js @@ -8,6 +8,7 @@ import {EVENTS} from '../src/constants.js'; import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js'; import {appendVrrefAndFui, getReferrer} from '../libraries/intentIqUtils/getRefferer.js'; +import {getGppValue} from '../libraries/intentIqUtils/getGppValue.js'; import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, VERSION} from '../libraries/intentIqConstants/intentIqConstants.js'; const MODULE_NAME = 'iiqAnalytics' @@ -264,6 +265,7 @@ function constructFullUrl(data) { let report = []; data = btoa(JSON.stringify(data)); report.push(data); + const gppData = getGppValue(); let url = defaultUrl + '?pid=' + iiqAnalyticsAnalyticsAdapter.initOptions.partner + '&mct=1' + @@ -273,7 +275,9 @@ function constructFullUrl(data) { '&jsver=' + VERSION + '&source=pbjs' + '&payload=' + JSON.stringify(report) + - '&uh=' + iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints; + '&uh=' + iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints + + (gppData.gppString ? '&gpp=' + encodeURIComponent(gppData.gppString) : ''); + url = appendVrrefAndFui(url, iiqAnalyticsAnalyticsAdapter.initOptions.domainName); return url; } diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 2a57f602973..c854b6c4746 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -10,11 +10,12 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -import {gppDataHandler, uspDataHandler} from '../src/consentHandler.js'; +import {uspDataHandler} from '../src/consentHandler.js'; import AES from 'crypto-js/aes.js'; import Utf8 from 'crypto-js/enc-utf8.js'; import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js'; import {appendVrrefAndFui} from '../libraries/intentIqUtils/getRefferer.js'; +import {getGppValue} from '../libraries/intentIqUtils/getGppValue.js'; import { FIRST_PARTY_KEY, WITH_IIQ, WITHOUT_IIQ, @@ -158,22 +159,6 @@ function tryParse(data) { } } -/** - * Convert GPP data to an object - * @param {Object} data - * @return {string} The JSON string representation of the input data. - */ -export function handleGPPData(data = {}) { - if (Array.isArray(data)) { - let obj = {}; - for (const element of data) { - obj = Object.assign(obj, element); - } - return JSON.stringify(obj); - } - return JSON.stringify(data); -} - /** * Processes raw client hints data into a structured format. * @param {object} clientHints - Raw client hints data @@ -231,17 +216,19 @@ export const intentIqIdSubmodule = { getId(config) { const configParams = (config?.params) || {}; let decryptedData, callbackTimeoutID; - let callbackFired = false - let runtimeEids = {} + let callbackFired = false; + let runtimeEids = { eids: [] }; const allowedStorage = defineStorageType(config.enabledStorageTypes); let firstPartyData = tryParse(readData(FIRST_PARTY_KEY, allowedStorage)); + const isGroupB = firstPartyData?.group === WITHOUT_IIQ; const firePartnerCallback = () => { if (configParams.callback && !callbackFired) { callbackFired = true; if (callbackTimeoutID) clearTimeout(callbackTimeoutID); + if (isGroupB) runtimeEids = { eids: [] }; configParams.callback(runtimeEids, firstPartyData?.group || NOT_YET_DEFINED); } } @@ -276,22 +263,14 @@ export const intentIqIdSubmodule = { // Get consent information const cmpData = {}; const uspData = uspDataHandler.getConsentData(); - const gppData = gppDataHandler.getConsentData(); + const gppData = getGppValue(); if (uspData) { cmpData.us_privacy = uspData; } - if (gppData) { - cmpData.gpp = ''; - cmpData.gpi = 1; - - if (gppData.parsedSections && 'usnat' in gppData.parsedSections) { - cmpData.gpp = handleGPPData(gppData.parsedSections['usnat']); - cmpData.gpi = 0 - } - cmpData.gpp_sid = gppData.applicableSections; - } + cmpData.gpp = gppData.gppString; + cmpData.gpi = gppData.gpi; // Read client hints from storage let clientHints = readData(CLIENT_HINTS_KEY, allowedStorage); @@ -345,9 +324,9 @@ export const intentIqIdSubmodule = { } } - if (!firstPartyData.cttl || Date.now() - firstPartyData.date > firstPartyData.cttl || firstPartyData.uspapi_value !== cmpData.us_privacy || firstPartyData.gpp_value !== cmpData.gpp) { + if (!firstPartyData.cttl || Date.now() - firstPartyData.date > firstPartyData.cttl || firstPartyData.uspapi_value !== cmpData.us_privacy || firstPartyData.gpp_string_value !== cmpData.gpp) { firstPartyData.uspapi_value = cmpData.us_privacy; - firstPartyData.gpp_value = cmpData.gpp; + firstPartyData.gpp_string_value = cmpData.gpp; firstPartyData.isOptedOut = false firstPartyData.cttl = 0 shouldCallServer = true; @@ -364,8 +343,9 @@ export const intentIqIdSubmodule = { } if (!shouldCallServer) { + if (isGroupB) runtimeEids = { eids: [] }; firePartnerCallback(); - return {id: runtimeEids?.eids || []}; + return { id: runtimeEids.eids }; } // use protocol relative urls for http or https @@ -378,7 +358,7 @@ export const intentIqIdSubmodule = { url += (partnerData.rrtt) ? '&rrtt=' + encodeURIComponent(partnerData.rrtt) : ''; url += firstPartyData.pcidDate ? '&iiqpciddate=' + encodeURIComponent(firstPartyData.pcidDate) : ''; url += cmpData.us_privacy ? '&pa=' + encodeURIComponent(cmpData.us_privacy) : ''; - url += cmpData.gpp ? '&gpv=' + encodeURIComponent(cmpData.gpp) : ''; + url += cmpData.gpp ? '&gpp=' + encodeURIComponent(cmpData.gpp) : ''; url += cmpData.gpi ? '&gpi=' + cmpData.gpi : ''; url += clientHints ? '&uh=' + encodeURIComponent(clientHints) : ''; url += VERSION ? '&jsver=' + VERSION : ''; @@ -402,8 +382,7 @@ export const intentIqIdSubmodule = { partnerData.date = Date.now(); firstPartyData.date = Date.now(); const defineEmptyDataAndFireCallback = () => { - respJson.data = partnerData.data = runtimeEids = {}; - partnerData.data = '' + respJson.data = partnerData.data = runtimeEids = { eids: [] }; storeFirstPartyData() firePartnerCallback() callback(runtimeEids) diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index b2ce17470ff..b95c3603e3a 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -2,7 +2,8 @@ import { expect } from 'chai'; import { intentIqIdSubmodule, storage } from 'modules/intentIqIdSystem.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; -import { decryptData, handleClientHints, handleGPPData, readData } from '../../../modules/intentIqIdSystem'; +import { decryptData, handleClientHints, readData } from '../../../modules/intentIqIdSystem'; +import {getGppValue} from '../../../libraries/intentIqUtils/getGppValue.js'; import { gppDataHandler, uspDataHandler } from '../../../src/consentHandler'; import { clearAllCookies } from '../../helpers/cookies'; import { detectBrowserFromUserAgent, detectBrowserFromUserAgentData } from '../../../libraries/intentIqUtils/detectBrowserUtils'; @@ -252,7 +253,7 @@ describe('IntentIQ tests', function () { JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: false }) ); expect(callBackSpy.calledOnce).to.be.true; - expect(callBackSpy.args[0][0]).to.deep.equal({}); + expect(callBackSpy.args[0][0]).to.deep.equal({eids: []}); }); it('send addition parameters if were found in localstorage', function () { @@ -306,36 +307,39 @@ describe('IntentIQ tests', function () { expect(logErrorStub.called).to.be.true; }); - describe('handleGPPData', function () { - it('should convert array of objects to a single JSON string', function () { - const input = [ - { key1: 'value1' }, - { key2: 'value2' } - ]; - const expectedOutput = JSON.stringify({ key1: 'value1', key2: 'value2' }); - const result = handleGPPData(input); - expect(result).to.equal(expectedOutput); - }); - - it('should convert a single object to a JSON string', function () { - const input = { key1: 'value1', key2: 'value2' }; - const expectedOutput = JSON.stringify(input); - const result = handleGPPData(input); - expect(result).to.equal(expectedOutput); - }); - - it('should handle empty object', function () { - const input = {}; - const expectedOutput = JSON.stringify(input); - const result = handleGPPData(input); - expect(result).to.equal(expectedOutput); - }); - - it('should handle empty array', function () { - const input = []; - const expectedOutput = JSON.stringify({}); - const result = handleGPPData(input); - expect(result).to.equal(expectedOutput); + describe('getGppValue', function () { + const testCases = [ + { + description: 'should return gppString and gpi=0 when GPP data exists', + input: { gppString: '{"key1":"value1","key2":"value2"}' }, + expectedOutput: { gppString: '{"key1":"value1","key2":"value2"}', gpi: 0 } + }, + { + description: 'should return empty gppString and gpi=1 when GPP data does not exist', + input: null, + expectedOutput: { gppString: '', gpi: 1 } + }, + { + description: 'should return empty gppString and gpi=1 when gppString is not set', + input: {}, + expectedOutput: { gppString: '', gpi: 1 } + }, + { + description: 'should handle GPP data with empty string', + input: { gppString: '' }, + expectedOutput: { gppString: '', gpi: 1 } + } + ]; + + testCases.forEach(({ description, input, expectedOutput }) => { + it(description, function () { + sinon.stub(gppDataHandler, 'getConsentData').returns(input); + + const result = getGppValue(); + expect(result).to.deep.equal(expectedOutput); + + gppDataHandler.getConsentData.restore(); + }); }); }); @@ -431,54 +435,36 @@ describe('IntentIQ tests', function () { expect(firstPartyData.uspapi_value).to.equal(uspData); }); - it('should set cmpData.gpp and cmpData.gpp_sid if gppData exists and has parsedSections with usnat', function () { - const gppData = { - parsedSections: { - usnat: { key1: 'value1', key2: 'value2' } - }, - applicableSections: ['usnat'] + it('should create a request with gpp data if gppData exists and has gppString', function () { + const mockGppValue = { + gppString: '{"key1":"value1","key2":"value2"}', + gpi: 0 }; - gppDataHandlerStub.returns(gppData); - let callBackSpy = sinon.spy(); - let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&iiqidtype=2&iiqpcid='); - request.respond( - 200, - responseHeader, - JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: false, isOptedOut: false }) - ); - expect(callBackSpy.calledOnce).to.be.true; - - const firstPartyData = JSON.parse(localStorage.getItem(FIRST_PARTY_KEY)); - expect(firstPartyData.gpp_value).to.equal(JSON.stringify({ key1: 'value1', key2: 'value2' })); - }); - - it('should handle gppData without usnat in parsedSections', function () { - const gppData = { - parsedSections: { - euconsent: { key1: 'value1' } - }, - applicableSections: ['euconsent'] + const mockConfig = { + params: { partner: partner }, + enabledStorageTypes: ['localStorage'] }; - gppDataHandlerStub.returns(gppData); + + gppDataHandlerStub.returns(mockGppValue); let callBackSpy = sinon.spy(); - let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = intentIqIdSubmodule.getId(mockConfig).callback; submoduleCallback(callBackSpy); + let request = server.requests[0]; - expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&iiqidtype=2&iiqpcid='); + request.respond( 200, responseHeader, - JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: false, isOptedOut: true }) + JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: true }) ); + + expect(request.url).to.contain(`&gpp=${encodeURIComponent(mockGppValue.gppString)}`); expect(callBackSpy.calledOnce).to.be.true; const firstPartyData = JSON.parse(localStorage.getItem(FIRST_PARTY_KEY)); - expect(firstPartyData.gpp_value).to.equal(''); + expect(firstPartyData.gpp_string_value).to.equal(mockGppValue.gppString); }); });