From d6664f079d099e8695199605f873bdd668f57df1 Mon Sep 17 00:00:00 2001 From: Kiko Ruiz Date: Mon, 30 Mar 2026 11:27:48 +0200 Subject: [PATCH 1/2] fix(packages/sui-segment-wrapper): fix ga4 session id inconsistencies --- packages/sui-segment-wrapper/src/index.js | 3 +- .../src/repositories/adobeRepository.js | 88 ++++----------- .../src/repositories/googleRepository.js | 86 +++++++++++++- .../src/scripts/adobeVisitorApi.js | 12 -- .../sui-segment-wrapper/src/segmentWrapper.js | 14 +-- .../sui-segment-wrapper/src/utils/cookies.js | 25 +++++ .../optimizelyMiddlewaresIntegrationSpec.js | 15 +-- .../test/repositories/googleRepositorySpec.js | 106 +++++++++++++++++- .../test/segmentWrapperSpec.js | 86 +------------- packages/sui-segment-wrapper/test/stubs.js | 19 +++- .../test/utils/cookiesSpec.js | 73 ++++++++++++ 11 files changed, 333 insertions(+), 194 deletions(-) delete mode 100644 packages/sui-segment-wrapper/src/scripts/adobeVisitorApi.js create mode 100644 packages/sui-segment-wrapper/test/utils/cookiesSpec.js diff --git a/packages/sui-segment-wrapper/src/index.js b/packages/sui-segment-wrapper/src/index.js index 75d89163c..868ee3413 100644 --- a/packages/sui-segment-wrapper/src/index.js +++ b/packages/sui-segment-wrapper/src/index.js @@ -77,6 +77,7 @@ if (isClient && window.analytics) { } export default analytics -export {getAdobeVisitorData, getAdobeMCVisitorID} from './repositories/adobeRepository.js' export {getUniversalId} from './universalId.js' export {EVENTS} from './events.js' +export {getGoogleClientId, getGoogleSessionId} from './repositories/googleRepository.js' +export {getAdobeVisitorData, getAdobeMCVisitorID} from './repositories/adobeRepository.js' diff --git a/packages/sui-segment-wrapper/src/repositories/adobeRepository.js b/packages/sui-segment-wrapper/src/repositories/adobeRepository.js index 632b9f875..8f3dd378a 100644 --- a/packages/sui-segment-wrapper/src/repositories/adobeRepository.js +++ b/packages/sui-segment-wrapper/src/repositories/adobeRepository.js @@ -1,72 +1,24 @@ -import {getConfig} from '../config.js' - -let mcvid - -const getGlobalConfig = () => { - return { - ADOBE_ORG_ID: window.__SEGMENT_WRAPPER?.ADOBE_ORG_ID, - DEFAULT_DEMDEX_VERSION: window.__SEGMENT_WRAPPER?.DEFAULT_DEMDEX_VERSION ?? '3.3.0', - TIME_BETWEEN_RETRIES: window.__SEGMENT_WRAPPER?.TIME_BETWEEN_RETRIES ?? 15, - TIMES_TO_RETRY: window.__SEGMENT_WRAPPER?.TIMES_TO_RETRY ?? 80, - SERVERS: { - trackingServer: window.__SEGMENT_WRAPPER?.TRACKING_SERVER, - trackingServerSecure: window.__SEGMENT_WRAPPER?.TRACKING_SERVER - } - } -} - -const getDemdex = () => { - const config = getGlobalConfig() - - return window.Visitor && window.Visitor.getInstance(config.ADOBE_ORG_ID, config.SERVERS) -} - -const getMarketingCloudVisitorID = demdex => { - const mcvid = demdex && demdex.getMarketingCloudVisitorID() - return mcvid -} - -const getAdobeVisitorData = () => { - const demdex = getDemdex() || {} - const config = getGlobalConfig() - const {version = config.DEFAULT_DEMDEX_VERSION} = demdex - const {trackingServer} = config.SERVERS - - return Promise.resolve({trackingServer, version}) -} - -export const getAdobeMarketingCloudVisitorIdFromWindow = () => { - if (mcvid) return Promise.resolve(mcvid) - - const config = getGlobalConfig() - - return new Promise(resolve => { - function retry(retries) { - if (retries === 0) return resolve('') - - const demdex = getDemdex() - mcvid = getMarketingCloudVisitorID(demdex) - return mcvid ? resolve(mcvid) : window.setTimeout(() => retry(--retries), config.TIME_BETWEEN_RETRIES) - } - retry(config.TIMES_TO_RETRY) +/** + * @deprecated Adobe Analytics integration has been removed. + * These functions are kept for backwards compatibility but return empty values. + * Please remove any imports of these functions from your code. + */ + +/** + * @deprecated Returns empty Adobe visitor data + * @returns {Promise<{trackingServer: string, version: string}>} + */ +export const getAdobeVisitorData = () => { + return Promise.resolve({ + trackingServer: '', + version: '' }) } -const importVisitorApiAndGetAdobeMCVisitorID = () => - import('../scripts/adobeVisitorApi.js').then(() => { - mcvid = getAdobeMarketingCloudVisitorIdFromWindow() - return mcvid - }) - -const getAdobeMCVisitorID = () => { - const getCustomAdobeVisitorId = getConfig('getCustomAdobeVisitorId') - if (typeof getCustomAdobeVisitorId === 'function') { - return getCustomAdobeVisitorId() - } - - return getConfig('importAdobeVisitorId') === true - ? importVisitorApiAndGetAdobeMCVisitorID() - : getAdobeMarketingCloudVisitorIdFromWindow() +/** + * @deprecated Returns empty Marketing Cloud Visitor ID + * @returns {Promise} + */ +export const getAdobeMCVisitorID = () => { + return Promise.resolve('') } - -export {getAdobeVisitorData, getAdobeMCVisitorID} diff --git a/packages/sui-segment-wrapper/src/repositories/googleRepository.js b/packages/sui-segment-wrapper/src/repositories/googleRepository.js index 343fb2650..8352d9587 100644 --- a/packages/sui-segment-wrapper/src/repositories/googleRepository.js +++ b/packages/sui-segment-wrapper/src/repositories/googleRepository.js @@ -3,6 +3,7 @@ import {dispatchEvent} from '@s-ui/js/lib/events' import {getConfig} from '../config.js' import {EVENTS} from '../events.js' import {utils} from '../middlewares/source/pageReferrer.js' +import * as cookiesUtils from '../utils/cookies.js' const FIELDS = { clientId: 'client_id', @@ -59,6 +60,9 @@ const loadScript = async src => document.head.appendChild(script) }) +// Promise that resolves when GA4 is ready and cookie is available +let ga4ReadyPromise = null + export const loadGoogleAnalytics = async () => { const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId') const dataLayerName = getConfig('googleAnalyticsDataLayer') || DEFAULT_DATA_LAYER_NAME @@ -67,8 +71,41 @@ export const loadGoogleAnalytics = async () => { if (!googleAnalyticsMeasurementId) return Promise.resolve(false) // Create the `gtag` script const gtagScript = `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsMeasurementId}&l=${dataLayerName}` - // Load it and retrieve the `clientId` from Google - return loadScript(gtagScript) + + // Create a promise that resolves when gtag is loaded + cookie is ready + ga4ReadyPromise = loadScript(gtagScript).then(() => { + // Wait a tick for gtag to process config and create cookie + return new Promise(resolve => setTimeout(resolve, 100)) + }) + + return ga4ReadyPromise +} + +/** + * Waits for GA4 to be ready (only on first call). + * Subsequent calls return immediately. + * + * @returns {Promise} + */ +const waitForGA4Ready = async () => { + if (ga4ReadyPromise) { + await ga4ReadyPromise + ga4ReadyPromise = null // Only wait once + } +} + +/** + * Check if the given session ID is new (not in localStorage). + * @param {string} sessionId - The session ID to check + * @returns {{isNewSession: boolean, eventKey: string}} - Whether it's a new session and the storage key + */ +const checkNewSession = sessionId => { + const eventName = getConfig('googleAnalyticsInitEvent') ?? DEFAULT_GA_INIT_EVENT + const eventPrefix = `ga_event_${eventName}_` + const eventKey = `${eventPrefix}${sessionId}` + const isNewSession = !localStorage.getItem(eventKey) + + return {isNewSession, eventKey} } // Trigger GA init event just once per session. @@ -184,12 +221,51 @@ function readFromUtm(searchParams) { } export const getGoogleClientId = async () => getGoogleField(FIELDS.clientId) + +/** + * Gets GA4 session ID from cookie ONLY. + * + * CRITICAL BEHAVIOR: + * - Waits for GA4 to be ready on first call (ensures cookie exists) + * - Returns sessionId ONLY if available in cookie (reliable source) + * - "sui" event is triggered ONLY when sessionId is available and on new sessions + * - Both "sui" event and Segment events use the SAME sessionId from cookie + * + * This ensures: + * 1. No session mismatches between client and server-side tracking + * 2. No events sent to Segment without valid sessionId + * 3. "sui" event only sent on new sessions with correct sessionId + * 4. First track waits ~100ms for GA4, subsequent tracks are instant + * + * @returns {Promise} Session ID from cookie, or null if not ready + */ export const getGoogleSessionId = async () => { - const sessionId = await getGoogleField(FIELDS.sessionId) + const cookiePrefix = getConfig('googleAnalyticsCookiePrefix') || 'segment' + + // Wait for GA4 to be ready (only on first call) + await waitForGA4Ready() + + // ONLY use cookie value - this is the source of truth + const cookieSessionId = cookiesUtils.getGA4SessionIdFromCookie(cookiePrefix) + + // If cookie is available, trigger "sui" event on new sessions + if (cookieSessionId) { + const {isNewSession} = checkNewSession(cookieSessionId) - triggerGoogleAnalyticsInitEvent(sessionId) + if (isNewSession) { + triggerGoogleAnalyticsInitEvent(cookieSessionId, true) + // eslint-disable-next-line no-console + console.log(`New GA4 session started: ${cookieSessionId} (Source: Cookie)`) + } + } else { + // Cookie still not available even after waiting + // eslint-disable-next-line no-console + console.warn('GA4 cookie not available after waiting. SessionId will not be sent to Segment.') + } - return sessionId + // Return cookie sessionId (or null if not ready) + // When null, Segment events will NOT include sessionId + return cookieSessionId } // Unified consent state getter. diff --git a/packages/sui-segment-wrapper/src/scripts/adobeVisitorApi.js b/packages/sui-segment-wrapper/src/scripts/adobeVisitorApi.js deleted file mode 100644 index 219fd6d31..000000000 --- a/packages/sui-segment-wrapper/src/scripts/adobeVisitorApi.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable */ -/* prettier-ignore */ -/* istanbul ignore file */ - -/** - * @license - * Adobe Visitor API for JavaScript version: 4.6.0 - * Copyright 2020 Adobe, Inc. All Rights Reserved - * More info available at https://marketing.adobe.com/resources/help/en_US/mcvid/ - */ -var e=function(){"use strict";function e(t){"@babel/helpers - typeof";return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function n(){return{callbacks:{},add:function(e,t){this.callbacks[e]=this.callbacks[e]||[];var n=this.callbacks[e].push(t)-1,i=this;return function(){i.callbacks[e].splice(n,1)}},execute:function(e,t){if(this.callbacks[e]){t=void 0===t?[]:t,t=t instanceof Array?t:[t];try{for(;this.callbacks[e].length;){var n=this.callbacks[e].shift();"function"==typeof n?n.apply(null,t):n instanceof Array&&n[1].apply(n[0],t)}delete this.callbacks[e]}catch(e){}}},executeAll:function(e,t){(t||e&&!j.isObjectEmpty(e))&&Object.keys(this.callbacks).forEach(function(t){var n=void 0!==e[t]?e[t]:"";this.execute(t,n)},this)},hasCallbacks:function(){return Boolean(Object.keys(this.callbacks).length)}}}function i(e,t,n){var i=null==e?void 0:e[t];return void 0===i?n:i}function r(e){for(var t=/^\d+$/,n=0,i=e.length;nr)return 1;if(r>i)return-1}return 0}function s(e,t){if(e===t)return 0;var n=e.toString().split("."),i=t.toString().split(".");return r(n.concat(i))?(a(n,i),o(n,i)):NaN}function l(e){return e===Object(e)&&0===Object.keys(e).length}function c(e){return"function"==typeof e||e instanceof Array&&e.length}function u(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){return!0};this.log=_e("log",e,t),this.warn=_e("warn",e,t),this.error=_e("error",e,t)}function d(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.isEnabled,n=e.cookieName,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=i.cookies;return t&&n&&r?{remove:function(){r.remove(n)},get:function(){var e=r.get(n),t={};try{t=JSON.parse(e)}catch(e){t={}}return t},set:function(e,t){t=t||{},r.set(n,JSON.stringify(e),{domain:t.optInCookieDomain||"",cookieLifetime:t.optInStorageExpiry||3419e4,expires:!0})}}:{get:Pe,set:Pe,remove:Pe}}function f(e){this.name=this.constructor.name,this.message=e,"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error(e).stack}function p(){function e(e,t){var n=De(e);return n.length?n.every(function(e){return!!t[e]}):Se(t)}function t(){M(b),O(ce.COMPLETE),_(h.status,h.permissions),m.set(h.permissions,{optInCookieDomain:l,optInStorageExpiry:c}),C.execute(xe)}function n(e){return function(n,i){if(!Ae(n))throw new Error("[OptIn] Invalid category(-ies). Please use the `OptIn.Categories` enum.");return O(ce.CHANGED),Object.assign(b,ye(De(n),e)),i||t(),h}}var i=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=i.doesOptInApply,a=i.previousPermissions,o=i.preOptInApprovals,s=i.isOptInStorageEnabled,l=i.optInCookieDomain,c=i.optInStorageExpiry,u=i.isIabContext,f=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},p=f.cookies,g=Le(a);Re(g,"Invalid `previousPermissions`!"),Re(o,"Invalid `preOptInApprovals`!");var m=d({isEnabled:!!s,cookieName:"adobeujs-optin"},{cookies:p}),h=this,_=le(h),C=ge(),I=Me(g),v=Me(o),D=m.get(),S={},A=function(e,t){return ke(e)||t&&ke(t)?ce.COMPLETE:ce.PENDING}(I,D),y=function(e,t,n){var i=ye(pe,!r);return r?Object.assign({},i,e,t,n):i}(v,I,D),b=be(y),O=function(e){return A=e},M=function(e){return y=e};h.deny=n(!1),h.approve=n(!0),h.denyAll=h.deny.bind(h,pe),h.approveAll=h.approve.bind(h,pe),h.isApproved=function(t){return e(t,h.permissions)},h.isPreApproved=function(t){return e(t,v)},h.fetchPermissions=function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=t?h.on(ce.COMPLETE,e):Pe;return!r||r&&h.isComplete||!!o?e(h.permissions):t||C.add(xe,function(){return e(h.permissions)}),n},h.complete=function(){h.status===ce.CHANGED&&t()},h.registerPlugin=function(e){if(!e||!e.name||"function"!=typeof e.onRegister)throw new Error(je);S[e.name]||(S[e.name]=e,e.onRegister.call(e,h))},h.execute=Ne(S),Object.defineProperties(h,{permissions:{get:function(){return y}},status:{get:function(){return A}},Categories:{get:function(){return ue}},doesOptInApply:{get:function(){return!!r}},isPending:{get:function(){return h.status===ce.PENDING}},isComplete:{get:function(){return h.status===ce.COMPLETE}},__plugins:{get:function(){return Object.keys(S)}},isIabContext:{get:function(){return u}}})}function g(e,t){function n(){r=null,e.call(e,new f("The call took longer than you wanted!"))}function i(){r&&(clearTimeout(r),e.apply(e,arguments))}if(void 0===t)return e;var r=setTimeout(n,t);return i}function m(){if(window.__cmp)return window.__cmp;var e=window;if(e===window.top)return void Ie.error("__cmp not found");for(var t;!t;){e=e.parent;try{e.frames.__cmpLocator&&(t=e)}catch(e){}if(e===window.top)break}if(!t)return void Ie.error("__cmp not found");var n={};return window.__cmp=function(e,i,r){var a=Math.random()+"",o={__cmpCall:{command:e,parameter:i,callId:a}};n[a]=r,t.postMessage(o,"*")},window.addEventListener("message",function(e){var t=e.data;if("string"==typeof t)try{t=JSON.parse(e.data)}catch(e){}if(t.__cmpReturn){var i=t.__cmpReturn;n[i.callId]&&(n[i.callId](i.returnValue,i.success),delete n[i.callId])}},!1),window.__cmp}function h(){var e=this;e.name="iabPlugin",e.version="0.0.1";var t=ge(),n={allConsentData:null},i=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return n[e]=t};e.fetchConsentData=function(e){var t=e.callback,n=e.timeout,i=g(t,n);r({callback:i})},e.isApproved=function(e){var t=e.callback,i=e.category,a=e.timeout;if(n.allConsentData)return t(null,s(i,n.allConsentData.vendorConsents,n.allConsentData.purposeConsents));var o=g(function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=n.vendorConsents,a=n.purposeConsents;t(e,s(i,r,a))},a);r({category:i,callback:o})},e.onRegister=function(t){var n=Object.keys(de),i=function(e){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=i.purposeConsents,a=i.gdprApplies,o=i.vendorConsents;!e&&a&&o&&r&&(n.forEach(function(e){var n=s(e,o,r);t[n?"approve":"deny"](e,!0)}),t.complete())};e.fetchConsentData({callback:i})};var r=function(e){var r=e.callback;if(n.allConsentData)return r(null,n.allConsentData);t.add("FETCH_CONSENT_DATA",r);var s={};o(function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=e.purposeConsents,o=e.gdprApplies,l=e.vendorConsents;(arguments.length>1?arguments[1]:void 0)&&(s={purposeConsents:r,gdprApplies:o,vendorConsents:l},i("allConsentData",s)),a(function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(arguments.length>1?arguments[1]:void 0)&&(s.consentString=e.consentData,i("allConsentData",s)),t.execute("FETCH_CONSENT_DATA",[null,n.allConsentData])})})},a=function(e){var t=m();t&&t("getConsentData",null,e)},o=function(e){var t=Fe(de),n=m();n&&n("getVendorConsents",t,e)},s=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=!!t[de[e]];return i&&function(){return fe[e].every(function(e){return n[e]})}()}}var _="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};Object.assign=Object.assign||function(e){for(var t,n,i=1;i=0||t.indexOf("Trident/")>=0&&t.indexOf("Windows NT 6")>=0};n.getIeVersion=function(){return document.documentMode?document.documentMode:i()?7:null},n.encodeAndBuildRequest=function(e,t){return e.map(encodeURIComponent).join(t)},n.isObject=function(t){return null!==t&&"object"===e(t)&&!1===Array.isArray(t)},n.defineGlobalNamespace=function(){return window.adobe=n.isObject(window.adobe)?window.adobe:{},window.adobe},n.pluck=function(e,t){return t.reduce(function(t,n){return e[n]&&(t[n]=e[n]),t},Object.create(null))},n.parseOptOut=function(e,t,n){t||(t=n,e.d_optout&&e.d_optout instanceof Array&&(t=e.d_optout.join(",")));var i=parseInt(e.d_ottl,10);return isNaN(i)&&(i=7200),{optOut:t,d_ottl:i}},n.normalizeBoolean=function(e){var t=e;return"true"===e?t=!0:"false"===e&&(t=!1),t}}),V=(j.isObjectEmpty,j.isValueEmpty,j.getIeVersion,j.encodeAndBuildRequest,j.isObject,j.defineGlobalNamespace,j.pluck,j.parseOptOut,j.normalizeBoolean,n),U=E.MESSAGES,H={0:"prefix",1:"orgID",2:"state"},B=function(e,t){this.parse=function(e){try{var t={};return e.data.split("|").forEach(function(e,n){if(void 0!==e){t[H[n]]=2!==n?e:JSON.parse(e)}}),t}catch(e){}},this.isInvalid=function(n){var i=this.parse(n);if(!i||Object.keys(i).length<2)return!0;var r=e!==i.orgID,a=!t||n.origin!==t,o=-1===Object.keys(U).indexOf(i.prefix);return r||a||o},this.send=function(n,i,r){var a=i+"|"+e;r&&r===Object(r)&&(a+="|"+JSON.stringify(r));try{n.postMessage(a,t)}catch(e){}}},G=E.MESSAGES,Y=function(e,t,n,i){function r(e){Object.assign(p,e)}function a(e){Object.assign(p.state,e),Object.assign(p.state.ALLFIELDS,e),p.callbackRegistry.executeAll(p.state)}function o(e){if(!h.isInvalid(e)){m=!1;var t=h.parse(e);p.setStateAndPublish(t.state)}}function s(e){!m&&g&&(m=!0,h.send(i,e))}function l(){r(new P(n._generateID)),p.getMarketingCloudVisitorID(),p.callbackRegistry.executeAll(p.state,!0),_.removeEventListener("message",c)}function c(e){if(!h.isInvalid(e)){var t=h.parse(e);m=!1,_.clearTimeout(p._handshakeTimeout),_.removeEventListener("message",c),r(new F(p)),_.addEventListener("message",o),p.setStateAndPublish(t.state),p.callbackRegistry.hasCallbacks()&&s(G.GETSTATE)}}function u(){g&&postMessage?(_.addEventListener("message",c),s(G.HANDSHAKE),p._handshakeTimeout=setTimeout(l,250)):l()}function d(){_.s_c_in||(_.s_c_il=[],_.s_c_in=0),p._c="Visitor",p._il=_.s_c_il,p._in=_.s_c_in,p._il[p._in]=p,_.s_c_in++}function f(){function e(e){0!==e.indexOf("_")&&"function"==typeof n[e]&&(p[e]=function(){})}Object.keys(n).forEach(e),p.getSupplementalDataID=n.getSupplementalDataID,p.isAllowed=function(){return!0}}var p=this,g=t.whitelistParentDomain;p.state={ALLFIELDS:{}},p.version=n.version,p.marketingCloudOrgID=e,p.cookieDomain=n.cookieDomain||"",p._instanceType="child";var m=!1,h=new B(e,g);p.callbackRegistry=V(),p.init=function(){d(),f(),r(new x(p)),u()},p.findField=function(e,t){if(void 0!==p.state[e])return t(p.state[e]),p.state[e]},p.messageParent=s,p.setStateAndPublish=a},q=E.MESSAGES,X=E.ALL_APIS,W=E.ASYNC_API_MAP,J=E.FIELDGROUP_TO_FIELD,K=function(e,t){function n(){var t={};return Object.keys(X).forEach(function(n){var i=X[n],r=e[i]();j.isValueEmpty(r)||(t[n]=r)}),t}function i(){var t=[];return e._loading&&Object.keys(e._loading).forEach(function(n){if(e._loading[n]){var i=J[n];t.push(i)}}),t.length?t:null}function r(t){return function n(r){var a=i();if(a){var o=W[a[0]];e[o](n,!0)}else t()}}function a(e,i){var r=n();t.send(e,i,r)}function o(e){l(e),a(e,q.HANDSHAKE)}function s(e){r(function(){a(e,q.PARENTSTATE)})()}function l(n){function i(i){r.call(e,i),t.send(n,q.PARENTSTATE,{CUSTOMERIDS:e.getCustomerIDs()})}var r=e.setCustomerIDs;e.setCustomerIDs=i}return function(e){if(!t.isInvalid(e)){(t.parse(e).prefix===q.HANDSHAKE?o:s)(e.source)}}},z=function(e,t){function n(e){return function(n){i[e]=n,r++,r===a&&t(i)}}var i={},r=0,a=Object.keys(e).length;Object.keys(e).forEach(function(t){var i=e[t];if(i.fn){var r=i.args||[];r.unshift(n(t)),i.fn.apply(i.context||null,r)}})},Q={get:function(e){e=encodeURIComponent(e);var t=(";"+document.cookie).split(" ").join(";"),n=t.indexOf(";"+e+"="),i=n<0?n:t.indexOf(";",n+1);return n<0?"":decodeURIComponent(t.substring(n+2+e.length,i<0?t.length:i))},set:function(e,t,n){var r=i(n,"cookieLifetime"),a=i(n,"expires"),o=i(n,"domain"),s=i(n,"secure"),l=s?"Secure":"";if(a&&"SESSION"!==r&&"NONE"!==r){var c=""!==t?parseInt(r||0,10):-60;if(c)a=new Date,a.setTime(a.getTime()+1e3*c);else if(1===a){a=new Date;var u=a.getYear();a.setYear(u+2+(u<1900?1900:0))}}else a=0;return e&&"NONE"!==r?(document.cookie=encodeURIComponent(e)+"="+encodeURIComponent(t)+"; path=/;"+(a?" expires="+a.toGMTString()+";":"")+(o?" domain="+o+";":"")+l,this.get(e)===t):0},remove:function(e,t){var n=i(t,"domain");n=n?" domain="+n+";":"",document.cookie=encodeURIComponent(e)+"=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"+n}},$=function(e){var t;!e&&_.location&&(e=_.location.hostname),t=e;var n,i=t.split(".");for(n=i.length-2;n>=0;n--)if(t=i.slice(n).join("."),Q.set("test","cookie",{domain:t}))return Q.remove("test",{domain:t}),t;return""},Z={compare:s,isLessThan:function(e,t){return s(e,t)<0},areVersionsDifferent:function(e,t){return 0!==s(e,t)},isGreaterThan:function(e,t){return s(e,t)>0},isEqual:function(e,t){return 0===s(e,t)}},ee=!!_.postMessage,te={postMessage:function(e,t,n){var i=1;t&&(ee?n.postMessage(e,t.replace(/([^:]+:\/\/[^\/]+).*/,"$1")):t&&(n.location=t.replace(/#.*$/,"")+"#"+ +new Date+i+++"&"+e))},receiveMessage:function(e,t){var n;try{ee&&(e&&(n=function(n){if("string"==typeof t&&n.origin!==t||"[object Function]"===Object.prototype.toString.call(t)&&!1===t(n.origin))return!1;e(n)}),_.addEventListener?_[e?"addEventListener":"removeEventListener"]("message",n):_[e?"attachEvent":"detachEvent"]("onmessage",n))}catch(e){}}},ne=function(e){var t,n,i="0123456789",r="",a="",o=8,s=10,l=10;if(1==e){for(i+="ABCDEF",t=0;16>t;t++)n=Math.floor(Math.random()*o),r+=i.substring(n,n+1),n=Math.floor(Math.random()*o),a+=i.substring(n,n+1),o=16;return r+"-"+a}for(t=0;19>t;t++)n=Math.floor(Math.random()*s),r+=i.substring(n,n+1),0===t&&9==n?s=3:(1==t||2==t)&&10!=s&&2>n?s=10:2n?l=10:20&&(t=!1)),{corsType:e,corsCookiesEnabled:t}}(),getCORSInstance:function(){return"none"===this.corsMetadata.corsType?null:new _[this.corsMetadata.corsType]},fireCORS:function(t,n,i){function r(e){var n;try{if((n=JSON.parse(e))!==Object(n))return void a.handleCORSError(t,null,"Response is not JSON")}catch(e){return void a.handleCORSError(t,e,"Error parsing response as JSON")}try{for(var i=t.callback,r=_,o=0;o=a&&(e.splice(r,1),r--);return{dataPresent:o,dataValid:s}},manageSyncsSize:function(e){if(e.join("*").length>this.MAX_SYNCS_LENGTH)for(e.sort(function(e,t){return parseInt(e.split("-")[1],10)-parseInt(t.split("-")[1],10)});e.join("*").length>this.MAX_SYNCS_LENGTH;)e.shift()},fireSync:function(t,n,i,r,a,o){var s=this;if(t){if("img"===n.tag){var l,c,u,d,f=n.url,p=e.loadSSL?"https:":"http:";for(l=0,c=f.length;lre.DAYS_BETWEEN_SYNC_ID_CALLS},attachIframeASAP:function(){function e(){t.startedAttachingIframe||(n.body?t.attachIframe():setTimeout(e,30))}var t=this;e()}}},oe={audienceManagerServer:{},audienceManagerServerSecure:{},cookieDomain:{},cookieLifetime:{},cookieName:{},doesOptInApply:{},disableThirdPartyCalls:{},discardTrackingServerECID:{},idSyncAfterIDCallResult:{},idSyncAttachIframeOnWindowLoad:{},idSyncContainerID:{},idSyncDisable3rdPartySyncing:{},disableThirdPartyCookies:{},idSyncDisableSyncs:{},disableIdSyncs:{},idSyncIDCallResult:{},idSyncSSLUseAkamai:{},isCoopSafe:{},isIabContext:{},isOptInStorageEnabled:{},loadSSL:{},loadTimeout:{},marketingCloudServer:{},marketingCloudServerSecure:{},optInCookieDomain:{},optInStorageExpiry:{},overwriteCrossDomainMCIDAndAID:{},preOptInApprovals:{},previousPermissions:{},resetBeforeVersion:{},sdidParamExpiry:{},serverState:{},sessionCookieName:{},secureCookie:{},takeTimeoutMetrics:{},trackingServer:{},trackingServerSecure:{},whitelistIframeDomains:{},whitelistParentDomain:{}},se={getConfigNames:function(){return Object.keys(oe)},getConfigs:function(){return oe},normalizeConfig:function(e){return"function"!=typeof e?e:e()}},le=function(e){var t={};return e.on=function(e,n,i){if(!n||"function"!=typeof n)throw new Error("[ON] Callback should be a function.");t.hasOwnProperty(e)||(t[e]=[]);var r=t[e].push({callback:n,context:i})-1;return function(){t[e].splice(r,1),t[e].length||delete t[e]}},e.off=function(e,n){t.hasOwnProperty(e)&&(t[e]=t[e].filter(function(e){if(e.callback!==n)return e}))},e.publish=function(e){if(t.hasOwnProperty(e)){var n=[].slice.call(arguments,1);t[e].slice(0).forEach(function(e){e.callback.apply(e.context,n)})}},e.publish},ce={PENDING:"pending",CHANGED:"changed",COMPLETE:"complete"},ue={AAM:"aam",ADCLOUD:"adcloud",ANALYTICS:"aa",CAMPAIGN:"campaign",ECID:"ecid",LIVEFYRE:"livefyre",TARGET:"target",MEDIA_ANALYTICS:"mediaaa"},de=(C={},t(C,ue.AAM,565),t(C,ue.ECID,565),C),fe=(I={},t(I,ue.AAM,[1,2,5]),t(I,ue.ECID,[1,2,5]),I),pe=function(e){return Object.keys(e).map(function(t){return e[t]})}(ue),ge=function(){var e={};return e.callbacks=Object.create(null),e.add=function(t,n){if(!c(n))throw new Error("[callbackRegistryFactory] Make sure callback is a function or an array of functions.");e.callbacks[t]=e.callbacks[t]||[];var i=e.callbacks[t].push(n)-1;return function(){e.callbacks[t].splice(i,1)}},e.execute=function(t,n){if(e.callbacks[t]){n=void 0===n?[]:n,n=n instanceof Array?n:[n];try{for(;e.callbacks[t].length;){var i=e.callbacks[t].shift();"function"==typeof i?i.apply(null,n):i instanceof Array&&i[1].apply(i[0],n)}delete e.callbacks[t]}catch(e){}}},e.executeAll=function(t,n){(n||t&&!l(t))&&Object.keys(e.callbacks).forEach(function(n){var i=void 0!==t[n]?t[n]:"";e.execute(n,i)},e)},e.hasCallbacks=function(){return Boolean(Object.keys(e.callbacks).length)},e},me=function(){},he=function(e){var t=window,n=t.console;return!!n&&"function"==typeof n[e]},_e=function(e,t,n){return n()?function(){if(he(e)){for(var n=arguments.length,i=new Array(n),r=0;r-1})},ye=function(e,t){return e.reduce(function(e,n){return e[n]=t,e},{})},be=function(e){return JSON.parse(JSON.stringify(e))},Oe=function(e){return"[object Array]"===Object.prototype.toString.call(e)&&!e.length},Me=function(e){if(Te(e))return e;try{return JSON.parse(e)}catch(e){return{}}},ke=function(e){return void 0===e||(Te(e)?Ae(Object.keys(e)):Ee(e))},Ee=function(e){try{var t=JSON.parse(e);return!!e&&ve(e,"string")&&Ae(Object.keys(t))}catch(e){return!1}},Te=function(e){return null!==e&&ve(e,"object")&&!1===Array.isArray(e)},Pe=function(){},Le=function(e){return ve(e,"function")?e():e},Re=function(e,t){ke(e)||Ie.error("".concat(t))},we=function(e){return Object.keys(e).map(function(t){return e[t]})},Fe=function(e){return we(e).filter(function(e,t,n){return n.indexOf(e)===t})},Ne=function(e){return function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.command,i=t.params,r=void 0===i?{}:i,a=t.callback,o=void 0===a?Pe:a;if(!n||-1===n.indexOf("."))throw new Error("[OptIn.execute] Please provide a valid command.");try{var s=n.split("."),l=e[s[0]],c=s[1];if(!l||"function"!=typeof l[c])throw new Error("Make sure the plugin and API name exist.");var u=Object.assign(r,{callback:o});l[c].call(l,u)}catch(e){Ie.error("[execute] Something went wrong: "+e.message)}}};f.prototype=Object.create(Error.prototype),f.prototype.constructor=f;var xe="fetchPermissions",je="[OptIn#registerPlugin] Plugin is invalid.";p.Categories=ue,p.TimeoutError=f;var Ve=Object.freeze({OptIn:p,IabPlugin:h}),Ue=function(e,t){e.publishDestinations=function(n){var i=arguments[1],r=arguments[2];try{r="function"==typeof r?r:n.callback}catch(e){r=function(){}}var a=t;if(!a.readyToAttachIframePreliminary())return void r({error:"The destination publishing iframe is disabled in the Visitor library."});if("string"==typeof n){if(!n.length)return void r({error:"subdomain is not a populated string."});if(!(i instanceof Array&&i.length))return void r({error:"messages is not a populated array."});var o=!1 -;if(i.forEach(function(e){"string"==typeof e&&e.length&&(a.addMessage(e),o=!0)}),!o)return void r({error:"None of the messages are populated strings."})}else{if(!j.isObject(n))return void r({error:"Invalid parameters passed."});var s=n;if("string"!=typeof(n=s.subdomain)||!n.length)return void r({error:"config.subdomain is not a populated string."});var l=s.urlDestinations;if(!(l instanceof Array&&l.length))return void r({error:"config.urlDestinations is not a populated array."});var c=[];l.forEach(function(e){j.isObject(e)&&(e.hideReferrer?e.message&&a.addMessage(e.message):c.push(e))});!function e(){c.length&&setTimeout(function(){var t=new Image,n=c.shift();t.src=n.url,a.onPageDestinationsFired.push(n),e()},100)}()}a.iframe?(r({message:"The destination publishing iframe is already attached and loaded."}),a.requestToProcess()):!e.subdomain&&e._getField("MCMID")?(a.subdomain=n,a.doAttachIframe=!0,a.url=a.getUrl(),a.readyToAttachIframe()?(a.iframeLoadedCallbacks.push(function(e){r({message:"Attempted to attach and load the destination publishing iframe through this API call. Result: "+(e.message||"no result")})}),a.attachIframe()):r({error:"Encountered a problem in attempting to attach and load the destination publishing iframe through this API call."})):a.iframeLoadedCallbacks.push(function(e){r({message:"Attempted to attach and load the destination publishing iframe through normal Visitor API processing. Result: "+(e.message||"no result")})})}},He=function e(t){function n(e,t){return e>>>t|e<<32-t}for(var i,r,a=Math.pow,o=a(2,32),s="",l=[],c=8*t.length,u=e.h=e.h||[],d=e.k=e.k||[],f=d.length,p={},g=2;f<64;g++)if(!p[g]){for(i=0;i<313;i+=g)p[i]=g;u[f]=a(g,.5)*o|0,d[f++]=a(g,1/3)*o|0}for(t+="€";t.length%64-56;)t+="\0";for(i=0;i>8)return;l[i>>2]|=r<<(3-i)%4*8}for(l[l.length]=c/o|0,l[l.length]=c,r=0;r>>3)+m[i-7]+(n(C,17)^n(C,19)^C>>>10)|0);u=[D+((n(I,2)^n(I,13)^n(I,22))+(I&u[1]^I&u[2]^u[1]&u[2]))|0].concat(u),u[4]=u[4]+D|0}for(i=0;i<8;i++)u[i]=u[i]+h[i]|0}for(i=0;i<8;i++)for(r=3;r+1;r--){var S=u[i]>>8*r&255;s+=(S<16?0:"")+S.toString(16)}return s},Be=function(e,t){return"SHA-256"!==t&&"SHA256"!==t&&"sha256"!==t&&"sha-256"!==t||(e=He(e)),e},Ge=function(e){return String(e).trim().toLowerCase()},Ye=Ve.OptIn;j.defineGlobalNamespace(),window.adobe.OptInCategories=Ye.Categories;var qe=function(t,n,i){function r(e){var t=e;return function(e){var n=e||D.location.href;try{var i=m._extractParamFromUri(n,t);if(i)return N.parsePipeDelimetedKeyValues(i)}catch(e){}}}function a(e){function t(e,t,n){e&&e.match(re.VALID_VISITOR_ID_REGEX)&&(n===y&&(v=!0),t(e))}t(e[y],m.setMarketingCloudVisitorID,y),m._setFieldExpire(T,-1),t(e[M],m.setAnalyticsVisitorID)}function o(e){e=e||{},m._supplementalDataIDCurrent=e.supplementalDataIDCurrent||"",m._supplementalDataIDCurrentConsumed=e.supplementalDataIDCurrentConsumed||{},m._supplementalDataIDLast=e.supplementalDataIDLast||"",m._supplementalDataIDLastConsumed=e.supplementalDataIDLastConsumed||{}}function s(e){function t(e,t,n){return n=n?n+="|":n,n+=e+"="+encodeURIComponent(t)}function n(e,n){var i=n[0],r=n[1];return null!=r&&r!==P&&(e=t(i,r,e)),e}var i=e.reduce(n,"");return function(e){var t=N.getTimestampInSeconds();return e=e?e+="|":e,e+="TS="+t}(i)}function l(e){var t=e.minutesToLive,n="";return(m.idSyncDisableSyncs||m.disableIdSyncs)&&(n=n||"Error: id syncs have been disabled"),"string"==typeof e.dpid&&e.dpid.length||(n=n||"Error: config.dpid is empty"),"string"==typeof e.url&&e.url.length||(n=n||"Error: config.url is empty"),void 0===t?t=20160:(t=parseInt(t,10),(isNaN(t)||t<=0)&&(n=n||"Error: config.minutesToLive needs to be a positive number")),{error:n,ttl:t}}function c(){return!!m.configs.doesOptInApply&&!(h.optIn.isComplete&&u())}function u(){return m.configs.doesOptInApply&&m.configs.isIabContext?h.optIn.isApproved(h.optIn.Categories.ECID)&&I:h.optIn.isApproved(h.optIn.Categories.ECID)}function d(){[["getMarketingCloudVisitorID"],["setCustomerIDs",void 0],["getAnalyticsVisitorID"],["getAudienceManagerLocationHint"],["getLocationHint"],["getAudienceManagerBlob"]].forEach(function(e){var t=e[0],n=2===e.length?e[1]:"",i=m[t];m[t]=function(e){return u()&&m.isAllowed()?i.apply(m,arguments):("function"==typeof e&&m._callCallback(e,[n]),n)}})}function f(e,t){if(I=!0,e)throw new Error("[IAB plugin] : "+e);t.gdprApplies&&(C=t.consentString),m.init(),g()}function p(){h.optIn.isComplete&&(h.optIn.isApproved(h.optIn.Categories.ECID)?m.configs.isIabContext?h.optIn.execute({command:"iabPlugin.fetchConsentData",callback:f}):(m.init(),g()):(d(),g()))}function g(){h.optIn.off("complete",p)}if(!i||i.split("").reverse().join("")!==t)throw new Error("Please use `Visitor.getInstance` to instantiate Visitor.");var m=this,h=window.adobe,C="",I=!1,v=!1;m.version="4.6.0";var D=_,S=D.Visitor;S.version=m.version,S.AuthState=E.AUTH_STATE,S.OptOut=E.OPT_OUT,D.s_c_in||(D.s_c_il=[],D.s_c_in=0),m._c="Visitor",m._il=D.s_c_il,m._in=D.s_c_in,m._il[m._in]=m,D.s_c_in++,m._instanceType="regular",m._log={requests:[]},m.marketingCloudOrgID=t,m.cookieName="AMCV_"+t,m.sessionCookieName="AMCVS_"+t,m.cookieDomain=$(),m.loadSSL=!0,m.loadTimeout=3e4,m.CORSErrors=[],m.marketingCloudServer=m.audienceManagerServer="dpm.demdex.net",m.sdidParamExpiry=30;var A=null,y="MCMID",b="MCIDTS",O="A",M="MCAID",k="AAM",T="MCAAMB",P="NONE",L=function(e){return!Object.prototype[e]},R=ie(m);m.FIELDS=E.FIELDS,m.cookieRead=function(e){return Q.get(e)},m.cookieWrite=function(e,t,n){var i=m.cookieLifetime?(""+m.cookieLifetime).toUpperCase():"",r=!1;return m.configs&&m.configs.secureCookie&&"https:"===location.protocol&&(r=!0),Q.set(e,""+t,{expires:n,domain:m.cookieDomain,cookieLifetime:i,secure:r})},m.resetState=function(e){e?m._mergeServerState(e):o()},m._isAllowedDone=!1,m._isAllowedFlag=!1,m.isAllowed=function(){return m._isAllowedDone||(m._isAllowedDone=!0,(m.cookieRead(m.cookieName)||m.cookieWrite(m.cookieName,"T",1))&&(m._isAllowedFlag=!0)),"T"===m.cookieRead(m.cookieName)&&m._helpers.removeCookie(m.cookieName),m._isAllowedFlag},m.setMarketingCloudVisitorID=function(e){m._setMarketingCloudFields(e)},m._use1stPartyMarketingCloudServer=!1,m.getMarketingCloudVisitorID=function(e,t){m.marketingCloudServer&&m.marketingCloudServer.indexOf(".demdex.net")<0&&(m._use1stPartyMarketingCloudServer=!0);var n=m._getAudienceManagerURLData("_setMarketingCloudFields"),i=n.url;return m._getRemoteField(y,i,e,t,n)};var w=function(e,t){var n={};m.getMarketingCloudVisitorID(function(){t.forEach(function(e){n[e]=m._getField(e,!0)}),-1!==t.indexOf("MCOPTOUT")?m.isOptedOut(function(t){n.MCOPTOUT=t,e(n)},null,!0):e(n)},!0)};m.getVisitorValues=function(e,t){var n={MCMID:{fn:m.getMarketingCloudVisitorID,args:[!0],context:m},MCOPTOUT:{fn:m.isOptedOut,args:[void 0,!0],context:m},MCAID:{fn:m.getAnalyticsVisitorID,args:[!0],context:m},MCAAMLH:{fn:m.getAudienceManagerLocationHint,args:[!0],context:m},MCAAMB:{fn:m.getAudienceManagerBlob,args:[!0],context:m}},i=t&&t.length?j.pluck(n,t):n;t&&-1===t.indexOf("MCAID")?w(e,t):z(i,e)},m._currentCustomerIDs={},m._customerIDsHashChanged=!1,m._newCustomerIDsHash="",m.setCustomerIDs=function(t,n){function i(){m._customerIDsHashChanged=!1}if(!m.isOptedOut()&&t){if(!j.isObject(t)||j.isObjectEmpty(t))return!1;m._readVisitor();var r,a,o;for(r in t)if(L(r)&&(a=t[r],n=a.hasOwnProperty("hashType")?a.hashType:n,a))if("object"===e(a)){var s={};if(a.id){if(n){if(!(o=Be(Ge(a.id),n)))return;a.id=o,s.hashType=n}s.id=a.id}void 0!=a.authState&&(s.authState=a.authState),m._currentCustomerIDs[r]=s}else if(n){if(!(o=Be(Ge(a),n)))return;m._currentCustomerIDs[r]={id:o,hashType:n}}else m._currentCustomerIDs[r]={id:a};var l=m.getCustomerIDs(),c=m._getField("MCCIDH"),u="";c||(c=0);for(r in l)L(r)&&(a=l[r],u+=(u?"|":"")+r+"|"+(a.id?a.id:"")+(a.authState?a.authState:""));m._newCustomerIDsHash=String(m._hash(u)),m._newCustomerIDsHash!==c&&(m._customerIDsHashChanged=!0,m._mapCustomerIDs(i))}},m.getCustomerIDs=function(){m._readVisitor();var e,t,n={};for(e in m._currentCustomerIDs)L(e)&&(t=m._currentCustomerIDs[e],t.id&&(n[e]||(n[e]={}),n[e].id=t.id,void 0!=t.authState?n[e].authState=t.authState:n[e].authState=S.AuthState.UNKNOWN,t.hashType&&(n[e].hashType=t.hashType)));return n},m.setAnalyticsVisitorID=function(e){m._setAnalyticsFields(e)},m.getAnalyticsVisitorID=function(e,t,n){if(!N.isTrackingServerPopulated()&&!n)return m._callCallback(e,[""]),"";var i="";if(n||(i=m.getMarketingCloudVisitorID(function(t){m.getAnalyticsVisitorID(e,!0)})),i||n){var r=n?m.marketingCloudServer:m.trackingServer,a="";m.loadSSL&&(n?m.marketingCloudServerSecure&&(r=m.marketingCloudServerSecure):m.trackingServerSecure&&(r=m.trackingServerSecure));var o={};if(r){var s="http"+(m.loadSSL?"s":"")+"://"+r+"/id",l="d_visid_ver="+m.version+"&mcorgid="+encodeURIComponent(m.marketingCloudOrgID)+(i?"&mid="+encodeURIComponent(i):"")+(m.idSyncDisable3rdPartySyncing||m.disableThirdPartyCookies?"&d_coppa=true":""),c=["s_c_il",m._in,"_set"+(n?"MarketingCloud":"Analytics")+"Fields"];a=s+"?"+l+"&callback=s_c_il%5B"+m._in+"%5D._set"+(n?"MarketingCloud":"Analytics")+"Fields",o.corsUrl=s+"?"+l,o.callback=c}return o.url=a,m._getRemoteField(n?y:M,a,e,t,o)}return""},m.getAudienceManagerLocationHint=function(e,t){if(m.getMarketingCloudVisitorID(function(t){m.getAudienceManagerLocationHint(e,!0)})){var n=m._getField(M);if(!n&&N.isTrackingServerPopulated()&&(n=m.getAnalyticsVisitorID(function(t){m.getAudienceManagerLocationHint(e,!0)})),n||!N.isTrackingServerPopulated()){var i=m._getAudienceManagerURLData(),r=i.url;return m._getRemoteField("MCAAMLH",r,e,t,i)}}return""},m.getLocationHint=m.getAudienceManagerLocationHint,m.getAudienceManagerBlob=function(e,t){if(m.getMarketingCloudVisitorID(function(t){m.getAudienceManagerBlob(e,!0)})){var n=m._getField(M);if(!n&&N.isTrackingServerPopulated()&&(n=m.getAnalyticsVisitorID(function(t){m.getAudienceManagerBlob(e,!0)})),n||!N.isTrackingServerPopulated()){var i=m._getAudienceManagerURLData(),r=i.url;return m._customerIDsHashChanged&&m._setFieldExpire(T,-1),m._getRemoteField(T,r,e,t,i)}}return""},m._supplementalDataIDCurrent="",m._supplementalDataIDCurrentConsumed={},m._supplementalDataIDLast="",m._supplementalDataIDLastConsumed={},m.getSupplementalDataID=function(e,t){m._supplementalDataIDCurrent||t||(m._supplementalDataIDCurrent=m._generateID(1));var n=m._supplementalDataIDCurrent;return m._supplementalDataIDLast&&!m._supplementalDataIDLastConsumed[e]?(n=m._supplementalDataIDLast,m._supplementalDataIDLastConsumed[e]=!0):n&&(m._supplementalDataIDCurrentConsumed[e]&&(m._supplementalDataIDLast=m._supplementalDataIDCurrent,m._supplementalDataIDLastConsumed=m._supplementalDataIDCurrentConsumed,m._supplementalDataIDCurrent=n=t?"":m._generateID(1),m._supplementalDataIDCurrentConsumed={}),n&&(m._supplementalDataIDCurrentConsumed[e]=!0)),n};var F=!1;m._liberatedOptOut=null,m.getOptOut=function(e,t){var n=m._getAudienceManagerURLData("_setMarketingCloudFields"),i=n.url;if(u())return m._getRemoteField("MCOPTOUT",i,e,t,n);if(m._registerCallback("liberatedOptOut",e),null!==m._liberatedOptOut)return m._callAllCallbacks("liberatedOptOut",[m._liberatedOptOut]),F=!1,m._liberatedOptOut;if(F)return null;F=!0;var r="liberatedGetOptOut";return n.corsUrl=n.corsUrl.replace(/dpm\.demdex\.net\/id\?/,"dpm.demdex.net/optOutStatus?"),n.callback=[r],_[r]=function(e){if(e===Object(e)){var t,n,i=j.parseOptOut(e,t,P);t=i.optOut,n=1e3*i.d_ottl,m._liberatedOptOut=t,setTimeout(function(){m._liberatedOptOut=null},n)}m._callAllCallbacks("liberatedOptOut",[t]),F=!1},R.fireCORS(n),null},m.isOptedOut=function(e,t,n){t||(t=S.OptOut.GLOBAL);var i=m.getOptOut(function(n){var i=n===S.OptOut.GLOBAL||n.indexOf(t)>=0;m._callCallback(e,[i])},n);return i?i===S.OptOut.GLOBAL||i.indexOf(t)>=0:null},m._fields=null,m._fieldsExpired=null,m._hash=function(e){var t,n,i=0;if(e)for(t=0;t0;)m._callCallback(n.shift(),t)}},m._addQuerystringParam=function(e,t,n,i){var r=encodeURIComponent(t)+"="+encodeURIComponent(n),a=N.parseHash(e),o=N.hashlessUrl(e);if(-1===o.indexOf("?"))return o+"?"+r+a;var s=o.split("?"),l=s[0]+"?",c=s[1];return l+N.addQueryParamAtLocation(c,r,i)+a},m._extractParamFromUri=function(e,t){var n=new RegExp("[\\?&#]"+t+"=([^&#]*)"),i=n.exec(e);if(i&&i.length)return decodeURIComponent(i[1])},m._parseAdobeMcFromUrl=r(re.ADOBE_MC),m._parseAdobeMcSdidFromUrl=r(re.ADOBE_MC_SDID),m._attemptToPopulateSdidFromUrl=function(e){var n=m._parseAdobeMcSdidFromUrl(e),i=1e9;n&&n.TS&&(i=N.getTimestampInSeconds()-n.TS),n&&n.SDID&&n.MCORGID===t&&ire.ADOBE_MC_TTL_IN_MIN||e.MCORGID!==t)return;a(e)}},m._mergeServerState=function(e){if(e)try{if(e=function(e){return N.isObject(e)?e:JSON.parse(e)}(e),e[m.marketingCloudOrgID]){var t=e[m.marketingCloudOrgID];!function(e){N.isObject(e)&&m.setCustomerIDs(e)}(t.customerIDs),o(t.sdid)}}catch(e){throw new Error("`serverState` has an invalid format.")}},m._timeout=null,m._loadData=function(e,t,n,i){t=m._addQuerystringParam(t,"d_fieldgroup",e,1),i.url=m._addQuerystringParam(i.url,"d_fieldgroup",e,1),i.corsUrl=m._addQuerystringParam(i.corsUrl,"d_fieldgroup",e,1),V.fieldGroupObj[e]=!0,i===Object(i)&&i.corsUrl&&"XMLHttpRequest"===R.corsMetadata.corsType&&R.fireCORS(i,n,e)},m._clearTimeout=function(e){null!=m._timeout&&m._timeout[e]&&(clearTimeout(m._timeout[e]),m._timeout[e]=0)},m._settingsDigest=0,m._getSettingsDigest=function(){if(!m._settingsDigest){var e=m.version;m.audienceManagerServer&&(e+="|"+m.audienceManagerServer),m.audienceManagerServerSecure&&(e+="|"+m.audienceManagerServerSecure),m._settingsDigest=m._hash(e)}return m._settingsDigest},m._readVisitorDone=!1,m._readVisitor=function(){if(!m._readVisitorDone){m._readVisitorDone=!0;var e,t,n,i,r,a,o=m._getSettingsDigest(),s=!1,l=m.cookieRead(m.cookieName),c=new Date;if(l||v||m.discardTrackingServerECID||(l=m.cookieRead(re.FIRST_PARTY_SERVER_COOKIE)),null==m._fields&&(m._fields={}),l&&"T"!==l)for(l=l.split("|"),l[0].match(/^[\-0-9]+$/)&&(parseInt(l[0],10)!==o&&(s=!0),l.shift()),l.length%2==1&&l.pop(),e=0;e1?(r=parseInt(t[1],10),a=t[1].indexOf("s")>0):(r=0,a=!1),s&&("MCCIDH"===n&&(i=""),r>0&&(r=c.getTime()/1e3-60)),n&&i&&(m._setField(n,i,1),r>0&&(m._fields["expire"+n]=r+(a?"s":""),(c.getTime()>=1e3*r||a&&!m.cookieRead(m.sessionCookieName))&&(m._fieldsExpired||(m._fieldsExpired={}),m._fieldsExpired[n]=!0)));!m._getField(M)&&N.isTrackingServerPopulated()&&(l=m.cookieRead("s_vi"))&&(l=l.split("|"),l.length>1&&l[0].indexOf("v1")>=0&&(i=l[1],e=i.indexOf("["),e>=0&&(i=i.substring(0,e)),i&&i.match(re.VALID_VISITOR_ID_REGEX)&&m._setField(M,i)))}},m._appendVersionTo=function(e){var t="vVersion|"+m.version,n=e?m._getCookieVersion(e):null;return n?Z.areVersionsDifferent(n,m.version)&&(e=e.replace(re.VERSION_REGEX,t)):e+=(e?"|":"")+t,e},m._writeVisitor=function(){var e,t,n=m._getSettingsDigest();for(e in m._fields)L(e)&&m._fields[e]&&"expire"!==e.substring(0,6)&&(t=m._fields[e],n+=(n?"|":"")+e+(m._fields["expire"+e]?"-"+m._fields["expire"+e]:"")+"|"+t);n=m._appendVersionTo(n),m.cookieWrite(m.cookieName,n,1)},m._getField=function(e,t){return null==m._fields||!t&&m._fieldsExpired&&m._fieldsExpired[e]?null:m._fields[e]},m._setField=function(e,t,n){null==m._fields&&(m._fields={}),m._fields[e]=t,n||m._writeVisitor()},m._getFieldList=function(e,t){var n=m._getField(e,t);return n?n.split("*"):null},m._setFieldList=function(e,t,n){m._setField(e,t?t.join("*"):"",n)},m._getFieldMap=function(e,t){var n=m._getFieldList(e,t);if(n){var i,r={};for(i=0;i0?e.substr(t):""},hashlessUrl:function(e){var t=e.indexOf("#");return t>0?e.substr(0,t):e},addQueryParamAtLocation:function(e,t,n){var i=e.split("&");return n=null!=n?n:i.length,i.splice(n,0,t),i.join("&")},isFirstPartyAnalyticsVisitorIDCall:function(e,t,n){if(e!==M)return!1;var i;return t||(t=m.trackingServer),n||(n=m.trackingServerSecure),!("string"!=typeof(i=m.loadSSL?n:t)||!i.length)&&(i.indexOf("2o7.net")<0&&i.indexOf("omtrdc.net")<0)},isObject:function(e){return Boolean(e&&e===Object(e))},removeCookie:function(e){Q.remove(e,{domain:m.cookieDomain})},isTrackingServerPopulated:function(){return!!m.trackingServer||!!m.trackingServerSecure},getTimestampInSeconds:function(){return Math.round((new Date).getTime()/1e3)},parsePipeDelimetedKeyValues:function(e){return e.split("|").reduce(function(e,t){var n=t.split("=");return e[n[0]]=decodeURIComponent(n[1]),e},{})},generateRandomString:function(e){e=e||5;for(var t="",n="abcdefghijklmnopqrstuvwxyz0123456789";e--;)t+=n[Math.floor(Math.random()*n.length)];return t},normalizeBoolean:function(e){return"true"===e||"false"!==e&&e},parseBoolean:function(e){return"true"===e||"false"!==e&&null},replaceMethodsWithFunction:function(e,t){for(var n in e)e.hasOwnProperty(n)&&"function"==typeof e[n]&&(e[n]=t);return e}};m._helpers=N;var x=ae(m,S);m._destinationPublishing=x,m.timeoutMetricsLog=[];var V={isClientSideMarketingCloudVisitorID:null,MCIDCallTimedOut:null,AnalyticsIDCallTimedOut:null,AAMIDCallTimedOut:null,fieldGroupObj:{},setState:function(e,t){switch(e){case"MC":!1===t?!0!==this.MCIDCallTimedOut&&(this.MCIDCallTimedOut=!1):this.MCIDCallTimedOut=t;break;case O:!1===t?!0!==this.AnalyticsIDCallTimedOut&&(this.AnalyticsIDCallTimedOut=!1):this.AnalyticsIDCallTimedOut=t;break;case k:!1===t?!0!==this.AAMIDCallTimedOut&&(this.AAMIDCallTimedOut=!1):this.AAMIDCallTimedOut=t}}};m.isClientSideMarketingCloudVisitorID=function(){return V.isClientSideMarketingCloudVisitorID},m.MCIDCallTimedOut=function(){return V.MCIDCallTimedOut},m.AnalyticsIDCallTimedOut=function(){return V.AnalyticsIDCallTimedOut},m.AAMIDCallTimedOut=function(){return V.AAMIDCallTimedOut},m.idSyncGetOnPageSyncInfo=function(){return m._readVisitor(),m._getField("MCSYNCSOP")},m.idSyncByURL=function(e){if(!m.isOptedOut()){var t=l(e||{});if(t.error)return t.error;var n,i,r=e.url,a=encodeURIComponent,o=x;return r=r.replace(/^https:/,"").replace(/^http:/,""),n=j.encodeAndBuildRequest(["",e.dpid,e.dpuuid||""],","),i=["ibs",a(e.dpid),"img",a(r),t.ttl,"",n],o.addMessage(i.join("|")),o.requestToProcess(),"Successfully queued"}},m.idSyncByDataSource=function(e){if(!m.isOptedOut())return e===Object(e)&&"string"==typeof e.dpuuid&&e.dpuuid.length?(e.url="//dpm.demdex.net/ibs:dpid="+e.dpid+"&dpuuid="+e.dpuuid,m.idSyncByURL(e)):"Error: config or config.dpuuid is empty"},Ue(m,x),m._getCookieVersion=function(e){e=e||m.cookieRead(m.cookieName);var t=re.VERSION_REGEX.exec(e);return t&&t.length>1?t[1]:null},m._resetAmcvCookie=function(e){var t=m._getCookieVersion();t&&!Z.isLessThan(t,e)||N.removeCookie(m.cookieName)},m.setAsCoopSafe=function(){A=!0},m.setAsCoopUnsafe=function(){A=!1},function(){if(m.configs=Object.create(null),N.isObject(n))for(var e in n)L(e)&&(m[e]=n[e],m.configs[e]=n[e])}(),d();var U;m.init=function(){c()&&(h.optIn.fetchPermissions(p,!0),!h.optIn.isApproved(h.optIn.Categories.ECID))||U||(U=!0,function(){if(N.isObject(n)){m.idSyncContainerID=m.idSyncContainerID||0,A="boolean"==typeof m.isCoopSafe?m.isCoopSafe:N.parseBoolean(m.isCoopSafe),m.resetBeforeVersion&&m._resetAmcvCookie(m.resetBeforeVersion),m._attemptToPopulateIdsFromUrl(),m._attemptToPopulateSdidFromUrl(),m._readVisitor();var e=m._getField(b),t=Math.ceil((new Date).getTime()/re.MILLIS_PER_DAY);m.idSyncDisableSyncs||m.disableIdSyncs||!x.canMakeSyncIDCall(e,t)||(m._setFieldExpire(T,-1),m._setField(b,t)),m.getMarketingCloudVisitorID(),m.getAudienceManagerLocationHint(),m.getAudienceManagerBlob(),m._mergeServerState(m.serverState)}else m._attemptToPopulateIdsFromUrl(),m._attemptToPopulateSdidFromUrl()}(),function(){if(!m.idSyncDisableSyncs&&!m.disableIdSyncs){x.checkDPIframeSrc();var e=function(){var e=x;e.readyToAttachIframe()&&e.attachIframe()};D.addEventListener("load",function(){S.windowLoaded=!0,e()});try{te.receiveMessage(function(e){x.receiveMessage(e.data)},x.iframeHost)}catch(e){}}}(),function(){m.whitelistIframeDomains&&re.POST_MESSAGE_ENABLED&&(m.whitelistIframeDomains=m.whitelistIframeDomains instanceof Array?m.whitelistIframeDomains:[m.whitelistIframeDomains],m.whitelistIframeDomains.forEach(function(e){var n=new B(t,e),i=K(m,n);te.receiveMessage(i,e)}))}())}};qe.config=se,_.Visitor=qe;var Xe=qe,We=function(e){if(j.isObject(e))return Object.keys(e).filter(function(t){return""!==e[t]}).reduce(function(t,n){var i=se.normalizeConfig(e[n]),r=j.normalizeBoolean(i);return t[n]=r,t},Object.create(null))},Je=Ve.OptIn,Ke=Ve.IabPlugin;return Xe.getInstance=function(e,t){if(!e)throw new Error("Visitor requires Adobe Marketing Cloud Org ID.");e.indexOf("@")<0&&(e+="@AdobeOrg");var n=function(){var t=_.s_c_il;if(t)for(var n=0;n ({ /** * Get all needed integrations depending on the gdprPrivacy value. - * One of them is the AdobeMarketingCloudVisitorId for Adobe Analytics integration. * @param {object} param - Object with the gdprPrivacyValue and if it's a CMP Submitted event */ const getTrackIntegrations = async ({gdprPrivacyValue, event}) => { const isGdprAccepted = checkAnalyticsGdprIsAccepted(gdprPrivacyValue) - let marketingCloudVisitorId let sessionId let clientId try { - if (isGdprAccepted) { - marketingCloudVisitorId = await getAdobeMCVisitorID() - } sessionId = await getGoogleSessionId() clientId = await getGoogleClientId() } catch (error) { @@ -72,17 +66,19 @@ const getTrackIntegrations = async ({gdprPrivacyValue, event}) => { const restOfIntegrations = getRestOfIntegrations({isGdprAccepted, event}) - // If we don't have the user consents we remove all the integrations but Adobe Analytics nor GA4 + // If we don't have the user consents we remove all the integrations + // CRITICAL: Only enable GA4 destination if we have BOTH clientId AND sessionId from cookie + // This prevents session mismatches and "Others" in GA4 reports + // When sessionId is not ready (null), we disable GA4 destination entirely return { ...restOfIntegrations, - 'Adobe Analytics': marketingCloudVisitorId ? {marketingCloudVisitorId} : true, 'Google Analytics 4': clientId && sessionId ? { clientId, sessionId } - : true + : false // Disable GA4 if no sessionId available } } diff --git a/packages/sui-segment-wrapper/src/utils/cookies.js b/packages/sui-segment-wrapper/src/utils/cookies.js index 0f731ad0b..8037bd11b 100644 --- a/packages/sui-segment-wrapper/src/utils/cookies.js +++ b/packages/sui-segment-wrapper/src/utils/cookies.js @@ -4,6 +4,31 @@ export function readCookie(cookieName) { return value !== null ? unescape(value[1]) : null } +/** + * Reads the GA4 session ID directly from the cookie. + * The cookie format is: _ga_=GS1.1..... + * + * @param {string} cookiePrefix - Cookie prefix configured in GA4 (e.g., 'segment') + * @returns {string|null} The session ID or null if not found + */ +export function getGA4SessionIdFromCookie(cookiePrefix = 'segment') { + const cookies = document.cookie.split(';') + const sessionRegex = /\.s(\d+)/ + const searchStr = cookiePrefix ? `${cookiePrefix}_ga_` : '_ga_' + + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim() + if (cookie.indexOf(searchStr) === 0) { + const match = cookie.match(sessionRegex) + if (match && match[1]) { + return match[1] + } + } + } + + return null +} + const ONE_YEAR = 31_536_000 const DEFAULT_PATH = '/' const DEFAULT_SAME_SITE = 'Lax' diff --git a/packages/sui-segment-wrapper/test/middlewares/optimizelyMiddlewaresIntegrationSpec.js b/packages/sui-segment-wrapper/test/middlewares/optimizelyMiddlewaresIntegrationSpec.js index 806baff5d..7c1e1016b 100644 --- a/packages/sui-segment-wrapper/test/middlewares/optimizelyMiddlewaresIntegrationSpec.js +++ b/packages/sui-segment-wrapper/test/middlewares/optimizelyMiddlewaresIntegrationSpec.js @@ -20,11 +20,7 @@ describe('optimizely middlewares integration', () => { context: { site: 'fakesite.fake' }, - integrations: { - 'Adobe Analytics': { - mcvid: 'fakeMcvid' - } - } + integrations: {} } } @@ -39,9 +35,6 @@ describe('optimizely middlewares integration', () => { attributes: { site: 'fakesite.fake' } - }, - 'Adobe Analytics': { - mcvid: 'fakeMcvid' } } } @@ -88,9 +81,6 @@ describe('optimizely middlewares integration', () => { site: 'fakesite.fake' }, integrations: { - 'Adobe Analytics': { - mcvid: 'fakeMcvid' - }, Optimizely: { attributes: { myAttribute: 'attributeValue' @@ -106,9 +96,6 @@ describe('optimizely middlewares integration', () => { site: 'fakesite.fake' }, integrations: { - 'Adobe Analytics': { - mcvid: 'fakeMcvid' - }, Optimizely: { attributes: { site: 'fakesite.fake', diff --git a/packages/sui-segment-wrapper/test/repositories/googleRepositorySpec.js b/packages/sui-segment-wrapper/test/repositories/googleRepositorySpec.js index 552ba77f6..930e7c885 100644 --- a/packages/sui-segment-wrapper/test/repositories/googleRepositorySpec.js +++ b/packages/sui-segment-wrapper/test/repositories/googleRepositorySpec.js @@ -1,7 +1,8 @@ import {expect} from 'chai' import sinon from 'sinon' -import {getCampaignDetails, getGoogleConsentValue} from '../../src/repositories/googleRepository.js' +import {getCampaignDetails, getGoogleConsentValue, getGoogleSessionId} from '../../src/repositories/googleRepository.js' + describe('GoogleRepository', () => { let initialTrackingTagsType @@ -233,4 +234,107 @@ describe('GoogleRepository', () => { expect(consentValue).to.be.undefined }) }) + + describe('getGoogleSessionId', () => { + let localStorageMock + + beforeEach(() => { + // Setup localStorage mock + localStorageMock = { + store: {}, + getItem: function (key) { + return this.store[key] || null + }, + setItem: function (key, value) { + this.store[key] = value.toString() + }, + removeItem: function (key) { + delete this.store[key] + } + } + Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true, + configurable: true + }) + + // Setup window.gtag mock + window.gtag = sinon.stub() + window.__mpi = window.__mpi || {} + window.__mpi.segmentWrapper = window.__mpi.segmentWrapper || {} + window.__mpi.segmentWrapper.googleAnalyticsMeasurementId = 'G-TEST123' + + // Mock gtag to return session ID + window.gtag.callsFake((command, target, field, callback) => { + if (command === 'get' && field === 'session_id') { + const sessionId = '9999999999' + // eslint-disable-next-line n/no-callback-literal + callback(sessionId) + } + }) + + // Clear cookies between tests + document.cookie.split(';').forEach(cookie => { + const name = cookie.split('=')[0].trim() + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;` + }) + }) + + afterEach(() => { + delete window.gtag + delete window.__mpi.segmentWrapper.googleAnalyticsMeasurementId + delete window.__mpi.segmentWrapper.googleAnalyticsCookiePrefix + // Clear localStorage between tests + if (localStorageMock) { + localStorageMock.store = {} + } + }) + + it('should return cookie sessionId when available', async () => { + // Given - set actual cookie instead of stubbing + const cookieSessionId = '1234567890' + document.cookie = `segment_ga_G-TEST123=GS1.1.s${cookieSessionId}.1.0.${Date.now()}.0.0.0; path=/` + + // When + const result = await getGoogleSessionId() + + // Then + expect(result).to.equal(cookieSessionId) + }) + + it('should return null when cookie not available yet', async () => { + // Given - no cookie set + + // When + const result = await getGoogleSessionId() + + // Then + expect(result).to.be.null + }) + + it('should call cookie function with configured prefix', async () => { + // Given + const cookieSessionId = '5555555555' + window.__mpi.segmentWrapper.googleAnalyticsCookiePrefix = 'custom' + document.cookie = `custom_ga_G-TEST123=GS1.1.s${cookieSessionId}.1.0.${Date.now()}.0.0.0; path=/` + + // When + const result = await getGoogleSessionId() + + // Then + expect(result).to.equal(cookieSessionId) + }) + + it('should use default prefix when not configured', async () => { + // Given + const cookieSessionId = '7777777777' + document.cookie = `segment_ga_G-TEST123=GS1.1.s${cookieSessionId}.1.0.${Date.now()}.0.0.0; path=/` + + // When + const result = await getGoogleSessionId() + + // Then + expect(result).to.equal(cookieSessionId) + }) + }) }) diff --git a/packages/sui-segment-wrapper/test/segmentWrapperSpec.js b/packages/sui-segment-wrapper/test/segmentWrapperSpec.js index 30e019a5a..d440cd8d8 100644 --- a/packages/sui-segment-wrapper/test/segmentWrapperSpec.js +++ b/packages/sui-segment-wrapper/test/segmentWrapperSpec.js @@ -8,7 +8,6 @@ import {defaultContextProperties} from '../src/middlewares/source/defaultContext import {pageReferrer} from '../src/middlewares/source/pageReferrer.js' import {userScreenInfo} from '../src/middlewares/source/userScreenInfo.js' import {userTraits} from '../src/middlewares/source/userTraits.js' -import {getAdobeVisitorData} from '../src/repositories/adobeRepository.js' import {INTEGRATIONS_WHEN_NO_CONSENTS} from '../src/segmentWrapper.js' import initTcfTracking, {getGdprPrivacyValue, USER_GDPR} from '../src/tcf.js' import {assertCampaignDetails} from './assertions.js' @@ -43,10 +42,6 @@ describe('Segment Wrapper', function () { stubWindowObjects() stubGoogleAnalytics() - window.__SEGMENT_WRAPPER = window.__SEGMENT_WRAPPER || {} - window.__SEGMENT_WRAPPER.ADOBE_ORG_ID = '012345678@AdobeOrg' - window.__SEGMENT_WRAPPER.TRACKING_SERVER = 'mycompany.test.net' - window.analytics.addSourceMiddleware(userTraits) window.analytics.addSourceMiddleware(defaultContextProperties) window.analytics.addSourceMiddleware(campaignContext) @@ -194,32 +189,6 @@ describe('Segment Wrapper', function () { expect(context.traits.anonymousId).to.deep.equal('fakeAnonymousId') }) - it('should send MarketingCloudId on Adobe Analytics integration', async () => { - await simulateUserAcceptAnalyticsConsents() - - window.Visitor = {} - window.Visitor.getInstance = sinon.stub().returns({ - getMarketingCloudVisitorID: sinon.stub().returns('fakeCloudId') - }) - - await suiAnalytics.track( - 'fakeEvent', - {}, - { - integrations: {fakeIntegrationKey: 'fakeIntegrationValue'} - } - ) - - const {context} = getDataFromLastTrack() - - expect(context.integrations).to.deep.includes({ - fakeIntegrationKey: 'fakeIntegrationValue', - 'Adobe Analytics': { - marketingCloudVisitorId: 'fakeCloudId' - } - }) - }) - describe('and gtag has been configured properly', () => { it('should send Google Analytics integration with true if user declined consents', async () => { // Add the needed config to enable Google Analytics @@ -239,7 +208,7 @@ describe('Segment Wrapper', function () { expect(context.integrations).to.deep.includes({ fakeIntegrationKey: 'fakeIntegrationValue', - 'Google Analytics 4': {clientId: 'fakeClientId', sessionId: 'fakeSessionId'} + 'Google Analytics 4': {clientId: 'fakeClientId', sessionId: '1234567890'} }) }) @@ -263,7 +232,7 @@ describe('Segment Wrapper', function () { fakeIntegrationKey: 'fakeIntegrationValue', 'Google Analytics 4': { clientId: 'fakeClientId', - sessionId: 'fakeSessionId' + sessionId: '1234567890' } }) }) @@ -721,53 +690,6 @@ describe('Segment Wrapper', function () { }) }) - describe('when the MarketingCloudVisitorId is loaded via callback', () => { - before(() => { - stubWindowObjects() - - window.__mpi = { - segmentWrapper: {} - } - window.__mpi.segmentWrapper.getCustomAdobeVisitorId = () => Promise.resolve('myCustomCloudVisitorId') - }) - - it('should use the visitor id resolved by the defined async callback function', async () => { - await simulateUserAcceptAnalyticsConsents() // simulate already fire an analytics.track - - const {context} = getDataFromLastTrack() - - expect(context.integrations).to.deep.include({ - 'Adobe Analytics': { - marketingCloudVisitorId: 'myCustomCloudVisitorId' - } - }) - }) - }) - - describe('when the importAdobeVisitorId config is set', () => { - before(() => { - setConfig('importAdobeVisitorId', true) - }) - - it('should import local Visitor Api version and create a MarketingCloudVisitorId on consents accepted', async () => { - await simulateUserAcceptAnalyticsConsents() // simulate already fire an analytics.track - - const {version} = await getAdobeVisitorData() - const {context} = getDataFromLastTrack() - - expect(version).to.equal('4.6.0') - expect(context.integrations['Adobe Analytics'].marketingCloudVisitorId).to.be.a('string') - }) - - it('should define Adobe Analytics as true in integrations', async () => { - await simulateUserDeclinedConsents() // simulate already fire an analytics.track - - const {context} = getDataFromLastTrack() - - expect(context.integrations['Adobe Analytics']).to.be.true - }) - }) - describe('when tcfTrackDefaultProperties config is set', () => { beforeEach(() => { stubWindowObjects() @@ -806,7 +728,6 @@ describe('Segment Wrapper', function () { window.__mpi = { segmentWrapper: {} } - window.__mpi.segmentWrapper.getCustomAdobeVisitorId = () => Promise.resolve('myCustomCloudVisitorId') }) it('sends an event with the actual context and traits when the consents are declined', async () => { @@ -834,8 +755,7 @@ describe('Segment Wrapper', function () { const {context} = getDataFromLastTrack() const integrations = { All: false, - 'Adobe Analytics': true, - 'Google Analytics 4': true, + 'Google Analytics 4': false, Personas: false, Webhooks: true, Webhook: true diff --git a/packages/sui-segment-wrapper/test/stubs.js b/packages/sui-segment-wrapper/test/stubs.js index c387f2256..6448d063a 100644 --- a/packages/sui-segment-wrapper/test/stubs.js +++ b/packages/sui-segment-wrapper/test/stubs.js @@ -43,11 +43,28 @@ export const stubFetch = ({responses = [{urlRe: /^http/, fetchResponse: {}}]} = } export const stubGoogleAnalytics = () => { + // Use numeric session ID to match real GA4 behavior (timestamps) + const fakeSessionId = '1234567890' + const fakeFields = { client_id: 'fakeClientId', - session_id: 'fakeSessionId' + session_id: fakeSessionId } + // Mock GA4 cookie with matching session ID to simulate real scenario + // Cookie format: segment_ga_=GS1.1.s.... + // Clear ALL existing GA4 cookies first (from any previous test) + document.cookie.split(';').forEach(cookie => { + const name = cookie.split('=')[0].trim() + if (name.includes('_ga_')) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;` + } + }) + + // Set the test cookie + const cookieName = 'segment_ga_G123456' + document.cookie = `${cookieName}=GS1.1.s${fakeSessionId}.1.0.${Date.now()}.0.0.0; path=/` + window.gtag = (key, id, field, done) => { if (key === 'get') { return done(fakeFields?.[field]) diff --git a/packages/sui-segment-wrapper/test/utils/cookiesSpec.js b/packages/sui-segment-wrapper/test/utils/cookiesSpec.js new file mode 100644 index 000000000..caa56401e --- /dev/null +++ b/packages/sui-segment-wrapper/test/utils/cookiesSpec.js @@ -0,0 +1,73 @@ +import {expect} from 'chai' + +import {getGA4SessionIdFromCookie} from '../../src/utils/cookies.js' + +describe('Cookies Utils', () => { + describe('getGA4SessionIdFromCookie', () => { + let originalCookie + + beforeEach(() => { + originalCookie = document.cookie + // Clear all cookies + document.cookie.split(';').forEach(cookie => { + const [name] = cookie.split('=') + document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/` + }) + }) + + afterEach(() => { + // Restore original cookies (best effort) + document.cookie = originalCookie + }) + + it('should return null when GA4 cookie does not exist', () => { + const sessionId = getGA4SessionIdFromCookie('segment') + expect(sessionId).to.be.null + }) + + it('should extract session ID from GA4 cookie with default prefix', () => { + // Simulate GA4 cookie: segment_ga_CONTAINERID=GS1.1.1234567890.1.0.1234567890.0.0.0 + document.cookie = 'segment_ga_G123456=GS1.1.s1234567890.1.0.1234567890.0.0.0' + + const sessionId = getGA4SessionIdFromCookie('segment') + expect(sessionId).to.equal('1234567890') + }) + + it('should extract session ID from GA4 cookie with custom prefix', () => { + document.cookie = 'custom_ga_G123456=GS1.1.s9876543210.1.0.1234567890.0.0.0' + + const sessionId = getGA4SessionIdFromCookie('custom') + expect(sessionId).to.equal('9876543210') + }) + + it('should extract session ID from GA4 cookie with no prefix', () => { + document.cookie = '_ga_G123456=GS1.1.s5555555555.1.0.1234567890.0.0.0' + + const sessionId = getGA4SessionIdFromCookie('') + expect(sessionId).to.equal('5555555555') + }) + + it('should return null when cookie format is invalid', () => { + document.cookie = 'segment_ga_G123456=invalid_format' + + const sessionId = getGA4SessionIdFromCookie('segment') + expect(sessionId).to.be.null + }) + + it('should return null when cookie has no session marker', () => { + document.cookie = 'segment_ga_G123456=GS1.1.1234567890.1.0.1234567890.0.0.0' + + const sessionId = getGA4SessionIdFromCookie('segment') + expect(sessionId).to.be.null + }) + + it('should handle multiple cookies and find the right one', () => { + document.cookie = 'other_cookie=value123' + document.cookie = 'segment_ga_G123456=GS1.1.s7777777777.1.0.1234567890.0.0.0' + document.cookie = 'another_cookie=value456' + + const sessionId = getGA4SessionIdFromCookie('segment') + expect(sessionId).to.equal('7777777777') + }) + }) +}) From 7df2c255c5c158aa5f0d83de74a47872ea1eb7d6 Mon Sep 17 00:00:00 2001 From: Kiko Ruiz Date: Mon, 30 Mar 2026 12:02:17 +0200 Subject: [PATCH 2/2] test(packages/sui-segment-wrapper): update docs and tests --- packages/sui-segment-wrapper/src/utils/cookies.js | 3 ++- packages/sui-segment-wrapper/test/stubs.js | 3 ++- packages/sui-segment-wrapper/test/utils/cookiesSpec.js | 8 ++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/sui-segment-wrapper/src/utils/cookies.js b/packages/sui-segment-wrapper/src/utils/cookies.js index 8037bd11b..745afdaf4 100644 --- a/packages/sui-segment-wrapper/src/utils/cookies.js +++ b/packages/sui-segment-wrapper/src/utils/cookies.js @@ -6,7 +6,8 @@ export function readCookie(cookieName) { /** * Reads the GA4 session ID directly from the cookie. - * The cookie format is: _ga_=GS1.1..... + * The cookie format is: _ga_=GS1.1.s$... + * Example: segment_ga_6NE7MBSF9K=GS2.1.s1774864422$o1$g0$t1774864422$j60$l0$h0 * * @param {string} cookiePrefix - Cookie prefix configured in GA4 (e.g., 'segment') * @returns {string|null} The session ID or null if not found diff --git a/packages/sui-segment-wrapper/test/stubs.js b/packages/sui-segment-wrapper/test/stubs.js index 6448d063a..d17c6ab04 100644 --- a/packages/sui-segment-wrapper/test/stubs.js +++ b/packages/sui-segment-wrapper/test/stubs.js @@ -52,7 +52,8 @@ export const stubGoogleAnalytics = () => { } // Mock GA4 cookie with matching session ID to simulate real scenario - // Cookie format: segment_ga_=GS1.1.s.... + // Production cookie format: segment_ga_=GS2.1.s$o1$g0$t$j60$l0$h0 + // Test uses simplified format: segment_ga_=GS1.1.s.... // Clear ALL existing GA4 cookies first (from any previous test) document.cookie.split(';').forEach(cookie => { const name = cookie.split('=')[0].trim() diff --git a/packages/sui-segment-wrapper/test/utils/cookiesSpec.js b/packages/sui-segment-wrapper/test/utils/cookiesSpec.js index caa56401e..0037d06a1 100644 --- a/packages/sui-segment-wrapper/test/utils/cookiesSpec.js +++ b/packages/sui-segment-wrapper/test/utils/cookiesSpec.js @@ -69,5 +69,13 @@ describe('Cookies Utils', () => { const sessionId = getGA4SessionIdFromCookie('segment') expect(sessionId).to.equal('7777777777') }) + + it('should work with production cookie format using $ separator', () => { + // Real production format: GS2.1.s$o1$g0$t$j60$l0$h0 + document.cookie = 'segment_ga_6NE7MBSF9K=GS2.1.s1774864422$o1$g0$t1774864422$j60$l0$h0' + + const sessionId = getGA4SessionIdFromCookie('segment') + expect(sessionId).to.equal('1774864422') + }) }) })