diff --git a/libraries/intentIqConstants/performanceConstants.js b/libraries/intentIqConstants/performanceConstants.js new file mode 100644 index 00000000000..3b143cff152 --- /dev/null +++ b/libraries/intentIqConstants/performanceConstants.js @@ -0,0 +1,166 @@ +export const PERFORMANCE_EVENT = { + SET_IIQ_OBJECT: 1, + SET_USER_CONFIG: 2, + BROWSER_BLACKLIST_DETECTED: 3, + SET_DEFAULT_VALUES: 6, + INIT_COUNTERS: 7, + INIT_STORAGE: 8, + READ_FIRST_PARTY_DATA: 11, + GENERATED_NEW_PCID: 12, + DEFINE_FIRST_PARTY_DATA: 13, + READ_CLIENT_HINTS_FROM_LS: 14, + READ_TCF: 16, + RECEIVED_TCF: 17, + CREATE_REQUEST_URL: 20, + MAKE_VR_REQUEST: 21, + READ_USP_START: 25, + READ_USP_END: 26, + START_GET_CLIENT_HINTS: 27, + RECEIVED_CLIENT_HINTS: 28, + GROUP_DEFINING: 29, + VR_RESPONSE_RECEIVED: 38, + CALLBACK_FIRE: 39, + READ_PARTNER_DATA_FROM_LS: 40, + REFRESH_USER_IDS_START: 41, + USER_IDS_SET_IN_PREBID: 42, + BROWSER_DETECTED: 43, + DEFINE_GROUP_FROM_SERVER: 45, + START_READ_LS_ENCODED_DATA: 46, + END_READ_LS_ENCODED_DATA: 47, + GET_DEVICE_INFO: 48, + SYNC_REQUEST: 49, + PROVIDED_GDPR: 51, + PROVIDED_USP: 53, + TIMEOUT_VS_VR_RESPONSE: 56, + TIME_SINCE_LAST_VR_REQUEST: 57, + ANALYTICS_ADAPTER_DETECTED: 61, + VR_RESPONSE_FINISHED: 62, + DEFINE_GROUP_BASED_ON_SERVER_RESPONSE: 63, + GAM_TARGETING_SET: 64, + CH_FETCH_FAILED: 65, + VR_CALLBACK_TIMEOUT: 66, + VR_REQUEST_SKIPPED: 67, + EIDS_FOUND_IN_LS: 68, + EIDS_FOUND_IN_RESPONSE: 69, + STORE_FIRST_PARTY_DATA: 70, + STORE_PARTNER_DATA: 71, + UPDATE_GLOBAL_OBJECT: 72, + BUILD_SYNC_PIXEL_URL: 73, + SEND_SYNC_REQUEST: 74, + ANALYTICS_REPORT_SENT: 75, + ANALYTICS_REPORT_FAILED: 76, + PREPARE_ANALYTICS_PAYLOAD: 77, + CONSTRUCT_ANALYTICS_URL: 78, + BID_WON_TRIGGERED: 79, + ENABLE_ANALYTICS: 80, + DISABLE_ANALYTICS: 81 +}; + +export const ERROR_CODES = { + ALL: -1, + VR_BAD_RESPONSE: 0, + VR_REQUEST_TIMEOUT: 1, + MULTIPLE_VR_REQUESTS: 6, + MULTIPLE_VR_RESPONSES: 7, + CALLBACK_TRIGGERED_FEW_TIMES: 8, + CONFIG_OBJECT_INVALID: 9, + PARTNER_ID_NOT_PROVIDED: 10, + ANALYTICS_ADAPTER_MISSING: 11, + CONFIG_OBJECT_NOT_PROVIDED_CORRECTLY: 12, + CH_FETCH_FAILED: 13, + GAM_TARGETING_FAILED: 14, + ENCRYPTION_FAILED: 15, + DECRYPTION_FAILED: 16, + STORE_DATA_FAILED: 17, + READ_DATA_FAILED: 18, + INVALID_RESPONSE_DATA: 19, + ANALYTICS_REPORT_FAILED: 20, + ANALYTICS_PAYLOAD_FAILED: 21, + DUPLICATE_ANALYTICS_REPORT: 22 +}; + +export const WARNING_CODES = { + ALL: -1, + CMP_TRIGGERED_FEW_TIMES: 0, + MULTIPLE_AGENTS_SAME_ID: 1, + AUCTION_EIDS_DISCREPANCY: 2, + AUCTION_STARTED_BEFORE_AGENT: 3, + CALLBACK_TIMEOUT_REACHED: 4, + EMPTY_EIDS_RESPONSE: 5, + USING_LS_EIDS: 6, + VR_SKIPPED: 7, + ANALYTICS_DISABLED: 8 +}; + +export const ADDITIONAL_ANALYTIC_PARAMS = { + ALL: -1, + RUNNING_IN_TOP: 0, + LS_ACCESS: 1, + GAM_PRESENT: 2, + HAVE_EIDS_IN_LS: 3, + AUCTION_EIDS_DIFF: 4, + AUCTION_TO_BIDWON_PERCENTAGE: 5, + BIDWON_MINUS_IMPRESSIONS: 6, + CALLBACK_WITH_EIDS: 7, + CH_SUPPORTED: 8, + GDPR_APPLIES: 9, + USP_APPLIES: 10, + GPP_APPLIES: 11, + SERVER_CALLED: 12, + RESPONSE_HAD_EIDS: 13, + RESPONSE_HAD_PID: 14, + USER_OPTED_OUT: 15, + GAM_OBJECT_PRESENT: 16, + ANALYTICS_ENABLED: 17, + MANUAL_REPORTING_ENABLED: 18 +}; + +export const ErrorType = { + ERROR: 'ed', + WARNING: 'ld', + FATAL: 'fd' +}; + +export const emptyWarning = { + [ErrorType.ERROR]: {}, + [ErrorType.WARNING]: {}, + [ErrorType.FATAL]: {} +}; + +export const edErrors = Object.values(ERROR_CODES).filter(id => id !== ERROR_CODES.ALL); +export const ldErrors = Object.values(WARNING_CODES).filter(id => id !== WARNING_CODES.ALL); +export const fdErrors = edErrors; + +export const ErrorsMap = { + [ErrorType.ERROR]: edErrors, + [ErrorType.WARNING]: ldErrors, + [ErrorType.FATAL]: fdErrors +}; + +export const multipleErrors = [ + { + id: PERFORMANCE_EVENT.MAKE_VR_REQUEST, + warningCode: ERROR_CODES.MULTIPLE_VR_REQUESTS, + errorType: ErrorType.ERROR + }, + { + id: PERFORMANCE_EVENT.VR_RESPONSE_RECEIVED, + warningCode: ERROR_CODES.MULTIPLE_VR_RESPONSES, + errorType: ErrorType.ERROR + }, + { + id: PERFORMANCE_EVENT.CALLBACK_FIRE, + warningCode: ERROR_CODES.CALLBACK_TRIGGERED_FEW_TIMES, + errorType: ErrorType.ERROR + } +]; + +export const businessAdapters = { + 0: 'liveIntentAnalyticsAdapter', + 1: 'euidIdSystem', + 2: 'id5AnalyticsAdapter', + 3: 'lotamePanoramaIdSystem', + 4: 'identityLinkIdSystem', + 5: 'merkleIdSystem', + 6: 'fabrickIdSystem' +}; \ No newline at end of file diff --git a/libraries/intentIqUtils/performanceTracker.js b/libraries/intentIqUtils/performanceTracker.js new file mode 100644 index 00000000000..699f2d57f95 --- /dev/null +++ b/libraries/intentIqUtils/performanceTracker.js @@ -0,0 +1,136 @@ +import { + ADDITIONAL_ANALYTIC_PARAMS as AAP, + businessAdapters, + emptyWarning, + ErrorsMap, + ErrorType, + multipleErrors +} from '../intentIqConstants/performanceConstants.js'; + +class PerformanceTracker { + constructor() { + this.performanceStart = performance.now(); + this.performanceData = {}; + this.warningData = JSON.parse(JSON.stringify(emptyWarning)); + this.additionalAnalyticParams = {}; + } + + addPerformance(id, value) { + const raw = typeof value === 'number' + ? value + : id === 0 + ? this.performanceStart + : id === 60 + ? performance.now() + : performance.now() - this.performanceStart; + + this.performanceData[id] = this.performanceData[id] || []; + this.performanceData[id].push(parseFloat(raw.toFixed(1))); + } + + addAdditionalAnalyticParam(id, value) { + const rawIds = [ + AAP.AUCTION_EIDS_DIFF, + AAP.AUCTION_TO_BIDWON_PERCENTAGE, + AAP.BIDWON_MINUS_IMPRESSIONS + ]; + + this.additionalAnalyticParams[id] = rawIds.includes(id) ? value : value ? 1 : 0; + } + + addWarning(id, errorType) { + if (!ErrorsMap[errorType]?.includes(id)) return; + this.warningData[errorType][id] = (this.warningData[errorType][id] || 0) + 1; + } + + checkWarnings(config = []) { + config.forEach(({ id, warningCode, errorType }) => { + if (this.performanceData[id]?.length > 1) this.addWarning(warningCode, errorType); + }); + } + + filterWarningData(data, ids = []) { + return ids.reduce((acc, id) => { + if (data[id]) acc[id] = data[id]; + return acc; + }, {}); + } + + updatePerformanceData(ids = []) { + if (ids.includes(-1)) return { ...this.performanceData }; + return ids.reduce((acc, id) => { + if (this.performanceData[id]) acc[id] = this.performanceData[id]; + return acc; + }, {}); + } + + getFilteredData(pd, installedModules = [], force = false) { + this.checkWarnings(multipleErrors); + + const performanceData = force ? { ...this.performanceData } : {}; + const warningData = JSON.parse(JSON.stringify(emptyWarning)); + const businessData = {}; + let additionalAnalyticData = {}; + + if (!force && pd?.p?.ct && pd?.p?.d?.length) { + Object.assign(performanceData, this.updatePerformanceData(pd.p.d)); + pd.p.ct--; + } + + if (force) { + Object.assign(warningData.ed, this.warningData.ed); + Object.assign(warningData.ld, this.warningData.ld); + Object.assign(warningData.fd, this.warningData.fd); + additionalAnalyticData = { ...this.additionalAnalyticParams }; + } else if (pd?.w?.ct) { + Object.values(ErrorType).forEach(key => { + const ids = pd?.w?.[key] || []; + warningData[key] = ids.includes(-1) ? this.warningData[key] : this.filterWarningData(this.warningData[key], ids); + }); + + if (Object.values(warningData).some(data => Object.keys(data).length)) pd.w.ct--; + } + + if (pd?.b?.ct && pd?.b?.d?.length) { + const ids = pd.b.d.includes(-1) ? Object.keys(businessAdapters).map(Number) : pd.b.d; + ids.forEach(id => { + if (businessAdapters[id]) businessData[id] = installedModules.includes(businessAdapters[id]) ? 1 : 0; + }); + pd.b.ct--; + } + + if (!force && pd?.ad?.ct && pd?.ad?.d?.length) { + const keys = Object.keys(this.additionalAnalyticParams); + if (keys.length) { + additionalAnalyticData = pd.ad.d.includes(-1) + ? { ...this.additionalAnalyticParams } + : pd.ad.d.reduce((acc, key) => { + acc[key] = this.additionalAnalyticParams[key]; + return acc; + }, {}); + pd.ad.ct--; + } + } + + return { + apd: performanceData, + aed: warningData.ed, + ald: warningData.ld, + afd: warningData.fd, + abd: businessData, + add: additionalAnalyticData + }; + } + + hasPerformance(id) { + return this.performanceData[id] !== undefined; + } + + resetData() { + this.performanceData = {}; + this.warningData = JSON.parse(JSON.stringify(emptyWarning)); + this.additionalAnalyticParams = {}; + } +} + +export const performanceTracker = new PerformanceTracker(); \ No newline at end of file diff --git a/libraries/intentIqUtils/performanceUtils.js b/libraries/intentIqUtils/performanceUtils.js new file mode 100644 index 00000000000..cdcbdd2dfee --- /dev/null +++ b/libraries/intentIqUtils/performanceUtils.js @@ -0,0 +1,88 @@ +import adapterManager from '../../src/adapterManager.js'; +import { getGlobal } from '../../src/prebidGlobal.js'; + +import { + PERFORMANCE_EVENT, + ERROR_CODES, + ErrorType, + ADDITIONAL_ANALYTIC_PARAMS +} from '../intentIqConstants/performanceConstants.js'; + +import { performanceTracker } from './performanceTracker.js'; +import { appendVrrefAndFui } from './getRefferer.js'; +import { getIiqServerAddress } from './intentIqConfig.js'; +import { VERSION } from '../intentIqConstants/intentIqConstants.js'; + +const pbjs = getGlobal(); + +let missingAnalyticsReported = false; + +export function hasIIQAnalytics(partnerId) { + try { + const wrapper = adapterManager.getAnalyticsAdapter('iiqAnalytics'); + const enabled = wrapper?.adapter?.enabled === true; + const globalExists = !!window[`intentIqAnalyticsAdapter_${partnerId}`]; + return enabled || globalExists; + } catch (e) { + return false; + } +} + +export function collectPerformanceData({ configParams, firstPartyData, partnerData, runtimeEids, force = false }) { + const payload = performanceTracker.getFilteredData(partnerData?.pd, pbjs?.installedModules || [], force); + const hasPayload = Object.values(payload).some(data => data && typeof data === 'object' && Object.keys(data).length); + + if (!hasPayload) return {}; + + return { + pcid: firstPartyData?.pcid, + dpi: configParams.partner, + jsver: configParams.version || VERSION, + pbjsver: pbjs?.version, + pid: firstPartyData?.pid, + vrref: appendVrrefAndFui('', configParams.domainName).replace('&vrref=', ''), + eidLn: runtimeEids?.eids?.length || 0, + dm: partnerData?.dm || [], + ...payload + }; +} + +export function makePerformanceRequest({ configParams, firstPartyData, partnerData, runtimeEids, force = false }) { + const performanceData = collectPerformanceData({ configParams, firstPartyData, partnerData, runtimeEids, force }); + if (!Object.keys(performanceData).length) return false; + + const queryParams = new URLSearchParams(); + + Object.entries(performanceData).forEach(([key, value]) => { + if (value === undefined || value === null) return; + queryParams.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value)); + }); + + const endpoint = partnerData?.ae || `${getIiqServerAddress(configParams)}/report`; + new Image().src = `${endpoint}?${queryParams.toString()}`; + + performanceTracker.addPerformance(PERFORMANCE_EVENT.ANALYTICS_REPORT_SENT); + performanceTracker.resetData(); + return true; +} + +export function scheduleAnalyticsDetection({ configParams, firstPartyData, partnerData, runtimeEids, timeout = 5000 }) { + const detect = () => { + if (missingAnalyticsReported) return; + + const analyticsDetected = hasIIQAnalytics(configParams.partner); + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.ANALYTICS_ENABLED, analyticsDetected); + + if (analyticsDetected) { + performanceTracker.addPerformance(PERFORMANCE_EVENT.ANALYTICS_ADAPTER_DETECTED); + return; + } + + missingAnalyticsReported = true; + performanceTracker.addWarning(ERROR_CODES.ANALYTICS_ADAPTER_MISSING, ErrorType.ERROR); + + makePerformanceRequest({ configParams, firstPartyData, partnerData, runtimeEids, force: true }); + }; + + setTimeout(detect, timeout); +} \ No newline at end of file diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index c064719ae88..62f03fd3b86 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -13,7 +13,7 @@ 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 { readData, storeData, defineStorageType, removeDataByKey, tryParse } from '../libraries/intentIqUtils/storageUtils.js'; +import { readData, storeData, defineStorageType, removeDataByKey, tryParse, storage } from '../libraries/intentIqUtils/storageUtils.js'; import { FIRST_PARTY_KEY, CLIENT_HINTS_KEY, @@ -27,6 +27,9 @@ import { iiqPixelServerAddress, getIiqServerAddress } from '../libraries/intentI import { handleAdditionalParams } from '../libraries/intentIqUtils/handleAdditionalParams.js'; import { decryptData, encryptData } from '../libraries/intentIqUtils/cryptionUtils.js'; import { defineABTestingGroup } from '../libraries/intentIqUtils/defineABTestingGroupUtils.js'; +import { performanceTracker } from '../libraries/intentIqUtils/performanceTracker.js'; +import { scheduleAnalyticsDetection } from '../libraries/intentIqUtils/performanceUtils.js'; +import { ErrorType, PERFORMANCE_EVENT, ERROR_CODES, WARNING_CODES, ADDITIONAL_ANALYTIC_PARAMS } from '../libraries/intentIqConstants/performanceConstants.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -150,6 +153,7 @@ export function initializeGlobalIIQ(partnerId) { if (!globalName || !window[globalName]) { globalName = `iiq_identity_${partnerId}` window[globalName] = {} + performanceTracker.addPerformance(PERFORMANCE_EVENT.SET_IIQ_OBJECT); return true } return false @@ -183,6 +187,7 @@ function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) if (firstPartyData.isOptedOut) { const needToDoSync = (Date.now() - (firstPartyData?.date || firstPartyData?.sCal || Date.now())) > SYNC_REFRESH_MILL if (newUser || needToDoSync) { + performanceTracker.addPerformance(PERFORMANCE_EVENT.SYNC_REQUEST); ajax(url, () => { }, undefined, { method: 'GET', withCredentials: true }); if (firstPartyData?.date) { @@ -192,6 +197,7 @@ function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) } } else if (!lastSyncDate || lastSyncElapsedTime > SYNC_REFRESH_MILL) { storeData(SYNC_KEY(partner), Date.now() + '', allowedStorage); + performanceTracker.addPerformance(PERFORMANCE_EVENT.SYNC_REQUEST); ajax(url, () => { }, undefined, { method: 'GET', withCredentials: true }); } @@ -206,19 +212,21 @@ function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) */ export function setGamReporting(gamObjectReference, gamParameterName, userGroup, isBlacklisted = false) { if (isBlacklisted) return; - if (isPlainObject(gamObjectReference) && gamObjectReference.cmd) { - gamObjectReference.cmd.push(() => { - if (typeof gamObjectReference.setConfig === 'function') { - gamObjectReference.setConfig({ - targeting: { - [gamParameterName]: userGroup - } - }); - return; - } - // Fallback in case an older version of Google Publisher Tag is used. - gamObjectReference?.pubads?.()?.setTargeting?.(gamParameterName, userGroup); - }); + + try { + if (isPlainObject(gamObjectReference) && gamObjectReference.cmd) { + gamObjectReference.cmd.push(() => { + if (typeof gamObjectReference.setConfig === 'function') { + gamObjectReference.setConfig({ targeting: { [gamParameterName]: userGroup } }); + } else { + // Fallback in case an older version of Google Publisher Tag is used. + gamObjectReference?.pubads?.()?.setTargeting?.(gamParameterName, userGroup); + } + performanceTracker.addPerformance(PERFORMANCE_EVENT.GAM_TARGETING_SET); + }); + } + } catch (e) { + performanceTracker.addWarning(ERROR_CODES.GAM_TARGETING_FAILED, ErrorType.ERROR); } } @@ -302,14 +310,26 @@ export const intentIqIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config) { + if (!config || !config.params) performanceTracker.addWarning(ERROR_CODES.CONFIG_OBJECT_NOT_PROVIDED_CORRECTLY, ErrorType.FATAL); const configParams = (config?.params) || {}; + performanceTracker.addPerformance(PERFORMANCE_EVENT.SET_USER_CONFIG); + + let decryptedData; + let callbackTimeoutID; + let callbackFired = false; + let runtimeEids = { eids: [] }; + let responseReceived = false; + let requestSent = false; const firePartnerCallback = () => { + if (callbackFired) performanceTracker.addWarning(ERROR_CODES.CALLBACK_TRIGGERED_FEW_TIMES, ErrorType.ERROR); if (configParams.callback && !callbackFired) { callbackFired = true; if (callbackTimeoutID) clearTimeout(callbackTimeoutID); let data = runtimeEids; if (data?.eids?.length === 1 && typeof data.eids[0] === 'string') data = data.eids[0]; + performanceTracker.addPerformance(PERFORMANCE_EVENT.CALLBACK_FIRE); + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.CALLBACK_WITH_EIDS, !!runtimeEids?.eids?.length); configParams.callback(data); } updateGlobalObj() @@ -317,52 +337,73 @@ export const intentIqIdSubmodule = { if (typeof configParams.partner !== 'number') { logError('User ID - intentIqId submodule requires a valid partner to be defined'); - firePartnerCallback() + performanceTracker.addWarning(ERROR_CODES.PARTNER_ID_NOT_PROVIDED, ErrorType.FATAL); + firePartnerCallback(); return; } - initializeGlobalIIQ(configParams.partner) - - let decryptedData, callbackTimeoutID; - let callbackFired = false; - let runtimeEids = { eids: [] }; + initializeGlobalIIQ(configParams.partner); const gamObjectReference = isPlainObject(configParams.gamObjectReference) ? configParams.gamObjectReference : undefined; - const gamParameterName = configParams.gamParameterName ? configParams.gamParameterName : 'intent_iq_group'; + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.GAM_OBJECT_PRESENT, !!gamObjectReference); + const gamParameterName = configParams.gamParameterName || 'intent_iq_group'; const groupChanged = typeof configParams.groupChanged === 'function' ? configParams.groupChanged : undefined; const siloEnabled = typeof configParams.siloEnabled === 'boolean' ? configParams.siloEnabled : false; sourceMetaData = isStr(configParams.sourceMetaData) ? translateMetadata(configParams.sourceMetaData) : ''; sourceMetaDataExternal = isNumber(configParams.sourceMetaDataExternal) ? configParams.sourceMetaDataExternal : undefined; - const additionalParams = configParams.additionalParams ? configParams.additionalParams : undefined; + const additionalParams = configParams.additionalParams || undefined; const chTimeout = Number(configParams?.chTimeout) >= 0 ? Number(configParams.chTimeout) : 10; PARTNER_DATA_KEY = `${FIRST_PARTY_KEY}_${configParams.partner}`; + performanceTracker.addPerformance(PERFORMANCE_EVENT.SET_DEFAULT_VALUES); const allowedStorage = defineStorageType(config.enabledStorageTypes); + performanceTracker.addPerformance(PERFORMANCE_EVENT.INIT_STORAGE); + performanceTracker.addAdditionalAnalyticParam( + ADDITIONAL_ANALYTIC_PARAMS.LS_ACCESS, + storage.hasLocalStorage() && allowedStorage.includes('html5') + ); + partnerData = tryParse(readData(PARTNER_DATA_KEY, allowedStorage)) || {}; + performanceTracker.addPerformance(PERFORMANCE_EVENT.READ_PARTNER_DATA_FROM_LS); + if (partnerData?.date) performanceTracker.addPerformance(PERFORMANCE_EVENT.TIME_SINCE_LAST_VR_REQUEST, Date.now() - partnerData.date); let rrttStrtTime = 0; let shouldCallServer = false; FIRST_PARTY_KEY_FINAL = `${FIRST_PARTY_KEY}${siloEnabled ? '_p_' + configParams.partner : ''}`; + performanceTracker.addPerformance(PERFORMANCE_EVENT.READ_TCF); const cmpData = getCmpData(); + performanceTracker.addPerformance(PERFORMANCE_EVENT.RECEIVED_TCF); + if (cmpData.gdprString) performanceTracker.addPerformance(PERFORMANCE_EVENT.PROVIDED_GDPR); + if (cmpData.uspString) performanceTracker.addPerformance(PERFORMANCE_EVENT.PROVIDED_USP); + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.GDPR_APPLIES, !!cmpData.gdprApplies); + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.USP_APPLIES, !!cmpData.uspString); + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.GPP_APPLIES, !!cmpData.gppString); + const gdprDetected = cmpData.gdprString; + firstPartyData = tryParse(readData(FIRST_PARTY_KEY_FINAL, allowedStorage)); + performanceTracker.addPerformance(PERFORMANCE_EVENT.READ_FIRST_PARTY_DATA); actualABGroup = defineABTestingGroup(configParams, partnerData?.terminationCause); + performanceTracker.addPerformance(PERFORMANCE_EVENT.GROUP_DEFINING); const currentBrowserLowerCase = detectBrowser(); + performanceTracker.addPerformance(PERFORMANCE_EVENT.BROWSER_DETECTED); + performanceTracker.addPerformance(PERFORMANCE_EVENT.GET_DEVICE_INFO); const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : ''; const isBlacklisted = browserBlackList?.includes(currentBrowserLowerCase); let newUser = false; setGamReporting(gamObjectReference, gamParameterName, actualABGroup, isBlacklisted); - if (groupChanged) groupChanged(actualABGroup, partnerData?.terminationCause); - + callbackTimeoutID = setTimeout(() => { + performanceTracker.addPerformance(PERFORMANCE_EVENT.VR_CALLBACK_TIMEOUT); + performanceTracker.addWarning(WARNING_CODES.CALLBACK_TIMEOUT_REACHED, ErrorType.WARNING); firePartnerCallback(); - }, configParams.timeoutInMillis || 500 - ); + }, configParams.timeoutInMillis || 500); if (!firstPartyData?.pcid) { const firstPartyId = generateGUID(); + performanceTracker.addPerformance(PERFORMANCE_EVENT.GENERATED_NEW_PCID); firstPartyData = { pcid: firstPartyId, pcidDate: Date.now(), @@ -371,25 +412,34 @@ export const intentIqIdSubmodule = { gdprString: EMPTY, date: Date.now() }; + performanceTracker.addPerformance(PERFORMANCE_EVENT.DEFINE_FIRST_PARTY_DATA); newUser = true; storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + performanceTracker.addPerformance(PERFORMANCE_EVENT.STORE_FIRST_PARTY_DATA); } else if (!firstPartyData.pcidDate) { firstPartyData.pcidDate = Date.now(); storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + performanceTracker.addPerformance(PERFORMANCE_EVENT.STORE_FIRST_PARTY_DATA); } if (gdprDetected && !('isOptedOut' in firstPartyData)) { firstPartyData.isOptedOut = true; } + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.USER_OPTED_OUT, !!firstPartyData.isOptedOut); // Read client hints from storage clientHints = readData(CLIENT_HINTS_KEY, allowedStorage); + performanceTracker.addPerformance(PERFORMANCE_EVENT.READ_CLIENT_HINTS_FROM_LS); const chSupported = isCHSupported(); + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.CH_SUPPORTED, !!chSupported); let chPromise = null; function fetchAndHandleCH() { + performanceTracker.addPerformance(PERFORMANCE_EVENT.START_GET_CLIENT_HINTS); + return navigator.userAgentData.getHighEntropyValues(CH_KEYS) .then(raw => { + performanceTracker.addPerformance(PERFORMANCE_EVENT.RECEIVED_CLIENT_HINTS); const nextCH = handleClientHints(raw) || ''; const prevCH = clientHints || ''; if (nextCH !== prevCH) { @@ -400,6 +450,8 @@ export const intentIqIdSubmodule = { }) .catch(err => { logError('CH fetch failed', err); + performanceTracker.addPerformance(PERFORMANCE_EVENT.CH_FETCH_FAILED); + performanceTracker.addWarning(ERROR_CODES.CH_FETCH_FAILED, ErrorType.ERROR); if (clientHints !== '') { clientHints = ''; removeDataByKey(CLIENT_HINTS_KEY, allowedStorage) @@ -426,15 +478,20 @@ export const intentIqIdSubmodule = { if (typeof partnerData.callCount === 'number') callCount = partnerData.callCount; if (typeof partnerData.failCount === 'number') failCount = partnerData.failCount; if (typeof partnerData.noDataCounter === 'number') noDataCount = partnerData.noDataCounter; + performanceTracker.addPerformance(PERFORMANCE_EVENT.INIT_COUNTERS); if (partnerData.wsrvcll) { partnerData.wsrvcll = false; storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + performanceTracker.addPerformance(PERFORMANCE_EVENT.STORE_PARTNER_DATA); } if (partnerData.data) { if (partnerData.data.length) { // encrypted data + performanceTracker.addPerformance(PERFORMANCE_EVENT.START_READ_LS_ENCODED_DATA); decryptedData = tryParse(decryptData(partnerData.data)); + performanceTracker.addPerformance(PERFORMANCE_EVENT.END_READ_LS_ENCODED_DATA); runtimeEids = decryptedData; + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.HAVE_EIDS_IN_LS, runtimeEids?.eids?.length ? 1 : 0); } } @@ -457,12 +514,15 @@ export const intentIqIdSubmodule = { shouldCallServer = true; storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + performanceTracker.addPerformance(PERFORMANCE_EVENT.STORE_FIRST_PARTY_DATA); + performanceTracker.addPerformance(PERFORMANCE_EVENT.STORE_PARTNER_DATA); } if (!shouldCallServer) { if (!hasPartnerData && !firstPartyData.isOptedOut) { shouldCallServer = true; } else shouldCallServer = Date.now() > firstPartyData.sCal + HOURS_72; } + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.SERVER_CALLED, !!shouldCallServer); if (firstPartyData.isOptedOut) { partnerData.data = runtimeEids = { eids: [] }; @@ -474,13 +534,16 @@ export const intentIqIdSubmodule = { } function buildAndSendPixel(ch) { + performanceTracker.addPerformance(PERFORMANCE_EVENT.BUILD_SYNC_PIXEL_URL); const url = createPixelUrl(firstPartyData, ch, configParams, partnerData, cmpData); + performanceTracker.addPerformance(PERFORMANCE_EVENT.SEND_SYNC_REQUEST); sendSyncRequest(allowedStorage, url, configParams.partner, firstPartyData, newUser); } // Check if current browser is in blacklist if (isBlacklisted) { logError('User ID - intentIqId submodule: browser is in blacklist! Data will be not provided.'); + performanceTracker.addPerformance(PERFORMANCE_EVENT.BROWSER_BLACKLIST_DETECTED); if (configParams.callback) configParams.callback(''); if (chSupported) { @@ -497,6 +560,8 @@ export const intentIqIdSubmodule = { } if (!shouldCallServer) { + performanceTracker.addPerformance(PERFORMANCE_EVENT.VR_REQUEST_SKIPPED); + performanceTracker.addWarning(WARNING_CODES.VR_SKIPPED, ErrorType.WARNING); firePartnerCallback(); updateCountersAndStore(runtimeEids, allowedStorage, partnerData); return { id: runtimeEids.eids }; @@ -505,10 +570,12 @@ export const intentIqIdSubmodule = { updateGlobalObj() // update global object before server request, to make sure analytical adapter will have it even if the server is "not in time" // use protocol relative urls for http or https + performanceTracker.addPerformance(PERFORMANCE_EVENT.CREATE_REQUEST_URL); let url = `${getIiqServerAddress(configParams)}/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; url += configParams.pai ? '&pai=' + encodeURIComponent(configParams.pai) : ''; url = appendFirstPartyData(url, firstPartyData, partnerData); url = appendPartnersFirstParty(url, configParams); + url = appendPartnersFirstParty(url, configParams); url += (partnerData.cttl) ? '&cttl=' + encodeURIComponent(partnerData.cttl) : ''; url += (partnerData.rrtt) ? '&rrtt=' + encodeURIComponent(partnerData.rrtt) : ''; url = appendCMPData(url, cmpData); @@ -530,13 +597,20 @@ export const intentIqIdSubmodule = { partnerData.eidl = runtimeEids?.eids?.length || -1 storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + performanceTracker.addPerformance(PERFORMANCE_EVENT.STORE_FIRST_PARTY_DATA); + performanceTracker.addPerformance(PERFORMANCE_EVENT.STORE_PARTNER_DATA); } const resp = function (callback) { const callbacks = { success: response => { + if (responseReceived) performanceTracker.addWarning(ERROR_CODES.MULTIPLE_VR_RESPONSES, ErrorType.ERROR); + responseReceived = true; + performanceTracker.addPerformance(PERFORMANCE_EVENT.VR_RESPONSE_RECEIVED); + if (rrttStrtTime && rrttStrtTime > 0) { partnerData.rrtt = Date.now() - rrttStrtTime; + performanceTracker.addPerformance(PERFORMANCE_EVENT.TIMEOUT_VS_VR_RESPONSE, partnerData.rrtt); } const respJson = tryParse(response); // If response is a valid json and should save is true @@ -557,12 +631,13 @@ export const intentIqIdSubmodule = { if ('tc' in respJson) { partnerData.terminationCause = respJson.tc; actualABGroup = defineABTestingGroup(configParams, respJson.tc,); + performanceTracker.addPerformance(PERFORMANCE_EVENT.DEFINE_GROUP_BASED_ON_SERVER_RESPONSE); if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, actualABGroup); if (groupChanged) groupChanged(actualABGroup, partnerData?.terminationCause); } if ('isOptedOut' in respJson) { - if (respJson.isOptedOut !== firstPartyData.isOptedOut) { + if (respJson.isOptedOut !== firstPartyData.isOptedOut) { firstPartyData.isOptedOut = respJson.isOptedOut; } if (respJson.isOptedOut === true) { @@ -576,6 +651,7 @@ export const intentIqIdSubmodule = { keysToRemove.forEach(key => removeDataByKey(key, allowedStorage)); storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + performanceTracker.addPerformance(PERFORMANCE_EVENT.STORE_FIRST_PARTY_DATA); firePartnerCallback(); callback(runtimeEids); return @@ -629,9 +705,12 @@ export const intentIqIdSubmodule = { } else { delete partnerData.gpr // remove prediction flag in case server doesn't provide it } + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.RESPONSE_HAD_EIDS, !!respJson.data?.eids); + performanceTracker.addAdditionalAnalyticParam(ADDITIONAL_ANALYTIC_PARAMS.RESPONSE_HAD_PID, !!respJson.pid); if (respJson.data?.eids) { runtimeEids = respJson.data + performanceTracker.addPerformance(PERFORMANCE_EVENT.EIDS_FOUND_IN_RESPONSE); callback(respJson.data.eids); firePartnerCallback() const encryptedData = encryptData(JSON.stringify(respJson.data)); @@ -642,12 +721,15 @@ export const intentIqIdSubmodule = { } updateCountersAndStore(runtimeEids, allowedStorage, partnerData); storeFirstPartyData(); + performanceTracker.addPerformance(PERFORMANCE_EVENT.VR_RESPONSE_FINISHED); } else { + performanceTracker.addWarning(ERROR_CODES.VR_BAD_RESPONSE, ErrorType.ERROR); callback(runtimeEids); firePartnerCallback() } }, error: error => { + performanceTracker.addWarning(ERROR_CODES.VR_BAD_RESPONSE, ErrorType.ERROR); logError(MODULE_NAME + ': ID fetch encountered an error', error); failCount++; updateCountersAndStore(runtimeEids, allowedStorage, partnerData); @@ -655,14 +737,17 @@ export const intentIqIdSubmodule = { } }; + if (requestSent) performanceTracker.addWarning(ERROR_CODES.MULTIPLE_VR_REQUESTS, ErrorType.ERROR); + requestSent = true; partnerData.wsrvcll = true; storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + performanceTracker.addPerformance(PERFORMANCE_EVENT.STORE_PARTNER_DATA); clearCountersAndStore(allowedStorage, partnerData); - rrttStrtTime = Date.now(); const sendAjax = uh => { if (uh) url += '&uh=' + encodeURIComponent(uh); + performanceTracker.addPerformance(PERFORMANCE_EVENT.MAKE_VR_REQUEST); ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); } @@ -682,8 +767,9 @@ export const intentIqIdSubmodule = { sendAjax(''); } }; + scheduleAnalyticsDetection({ configParams, firstPartyData, partnerData, runtimeEids, timeout: 10000 }); + // @todo: add page unload handling to trigger final analytics sending const respObj = { callback: resp }; - if (runtimeEids?.eids?.length) respObj.id = runtimeEids.eids; return respObj },