1// object to hold values of one trust flag variables on the basis of user's cookie selection 2window.isConsentCookieEnabled = { 3 targeting : getTargetingCookieValue(), 4 functional : getFunctionalCookieValue(), 5 performance : getPerformanceCookieValue() 6}; 7 8window.isWinePage = window.location.pathname?.toLowerCase().indexOf('aisles/wine-beer-spirits') != -1; 9var ltdEnabled = !window.isConsentCookieEnabled['targeting']; 10var isPDPPage = window.location.pathname.includes("product-details.") 11 || /\/shop\/[^/]+\/\d+$/.test(window.location.pathname) 12 || window.location.pathname.includes("/shop/pd/"); 13const isSearchPage = window.location.pathname?.toLowerCase().indexOf('shop/search-results') != -1; 14const delayDuration = 3000; // Delay duration in milliseconds 15let timerId; 16const slotToRenderMap = new Set(); 17const slotsToRefreshWithEnableSRA = new Set(); 18/** Start GPT Tag **/ 19window.googletag = window.googletag || { 20 cmd: [], 21}; 22 23function isPpidFlagDisabled() { 24 return window?.SWY?.CONFIGSERVICE?.featureToggleConfig?.isGamPPIDDisabled === true; 25} 26 27function sha256HexAsync(str) { 28 if (!window.crypto?.subtle) return Promise.resolve(null); 29 const bytes = new TextEncoder().encode(str); 30 return crypto.subtle.digest('SHA-256', bytes) 31 .then(buf => Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('')) 32 .catch(() => null); 33} 34 35function getPreferredRawIdForPpid() { 36 const ccn = getClubCardNumber?.(); 37 if (ccn) return String(ccn); 38 const avid = getAdobeVisitorId?.(); 39 if (avid) return String(avid); 40 return null; 41} 42 43function shouldSendPpid() { 44 if (isPpidFlagDisabled()) return false; 45 const restricted = 46 ltdEnabled || 47 window.isWinePage || 48 window.isRestrictedPage === true || 49 isGamTrackingDisabledOnPage() || 50 (isSearchPage && window.isSearchGamTrackingEnabled === false); 51 return ( 52 !restricted && 53 window.isConsentCookieEnabled?.targeting === true && 54 window.isConsentCookieEnabled?.performance === true 55 ); 56} 57 58googletag.cmd.push(function() { 59 googletag.pubads().enableSingleRequest(); 60 googletag.pubads().setPrivacySettings({ 61 limitedAds: ltdEnabled 62 }); 63 //enable disable initial load based on feature flag and if search page 64 isSearchPage && googletag.pubads().disableInitialLoad(); 65 66 var finish = function() { googletag.enableServices(); }; 67 if (!shouldSendPpid()) return finish(); 68 var raw = getPreferredRawIdForPpid(); 69 if (!raw) return finish(); 70 sha256HexAsync(raw.trim()) 71 .then(function(ppid) { if (ppid) googletag.pubads().setPublisherProvidedId(ppid); }) 72 .finally(finish); 73}); 74if (window.addEventListener) { 75 window.addEventListener("resize", browserResize); 76} else if (window.attachEvent) { 77 window.attachEvent("onresize", browserResize); 78} 79// Function to refresh certain ad slots 80let initialResize = window.innerWidth; 81 82function browserResize() { 83 let afterResize = window.innerWidth; 84 if ( 85 (initialResize < 1440 && afterResize >= 1440) || (initialResize >= 1440 && afterResize < 1440) || (initialResize < 1024 && afterResize >= 1024) || (initialResize >= 1024 && afterResize < 1024) || (initialResize < 768 && afterResize >= 768) || (initialResize >= 768 && afterResize < 768) || (initialResize < 360 && afterResize >= 360) || (initialResize >= 360 && afterResize < 360) 86 ) { 87 initialResize = afterResize; 88 const filteredSlots = googletag.pubads() 89 .getSlots() 90 .filter((item) => item.getSizes().length > 1 && item.getTargeting('criteoData').length === 0); 91 for (let i = 0; i < filteredSlots.length; i++) { 92 googletag.cmd.push(function() { 93 googletag.pubads().refresh([filteredSlots[i]], { 94 changeCorrelator: false, 95 }); 96 }); 97 } 98 } 99} 100 101function validateEnv(adUnitPath, sizes) { 102 const regex = /(nonprod)/; 103 const str = adUnitPath.match(regex); 104 if (str && str.length > 0 && sizes.length > 1 || str && str.length > 0 && sizes[0] != "fluid") { 105 return true; 106 } else { 107 return false;
108 } 109} 110// Function to add content to page 111function slotGenerator(data, elem) { 112 googletag.cmd.push(function() { 113 var slot = validateEnv(data.adUnitPath, data.sizes) ? 114 googletag.defineSlot(data.adUnitPath, data.sizes, elem) 115 .set("adsense_test_mode", "on") 116 .addService(googletag.pubads()) : 117 googletag.defineSlot(data.adUnitPath, data.sizes, elem) 118 .addService(googletag.pubads()); 119 if(data.isRefreshEnabled){ 120 slot.sectionId = elem; 121 } 122 if (data.sizeMapping) { 123 slot.defineSizeMapping(data.sizeMapping); 124 } 125 if (data.targeting) { 126 data.targeting.forEach((obj) => { 127 const key = Object.keys(obj)[0]; //gets key value 128 slot.setTargeting(key, obj[key]); 129 }); 130 } 131 if (isSearchPage || !data.isInsideCarousel || data.enableSRA){ 132 googletag.display(slot); 133 } 134 if(isSearchPage && data.isInsideCarousel){ 135 slotsToRefreshWithEnableSRA.add(slot); 136 } 137 if (!data.isInsideCarousel || data.enableSRA) { 138 if (isSearchPage) { // if search page 139 // Check feature flag for changeCorrelator behavior 140 const searchDuplicateGamAdsDisabled = window['SWY'] && window['SWY']['CONFIGSERVICE'] && window['SWY']['CONFIGSERVICE']['featureToggleConfig'] && window['SWY']['CONFIGSERVICE']['featureToggleConfig']['searchDuplicateGamAdsDisabled'] === true; 141 const refreshOptions = searchDuplicateGamAdsDisabled ? {} : { changeCorrelator: false }; 142 143 if (window.isSearchGamTrackingEnabled !== undefined) { 144 if (data.enableSRA) { 145 googletag.pubads().refresh(Array.from(slotsToRefreshWithEnableSRA.values()), refreshOptions); 146 slotsToRefreshWithEnableSRA.clear(); 147 } else { 148 googletag.pubads().refresh([slot], refreshOptions); 149 } 150 return; 151 } 152 if (timerId === undefined) { // check if no timer is running 153 setSearchPageTrackingTimer(); // start timer 154 } 155 if (timerId === null) { // timer is completed or cleared //#todo : bottom banner doesn't have PII added yet as it was not in slot list 156 if (data.enableSRA) { 157 googletag.pubads().refresh(Array.from(slotsToRefreshWithEnableSRA.values()), refreshOptions); 158 slotsToRefreshWithEnableSRA.clear(); 159 } else { 160 googletag.pubads().refresh([slot], refreshOptions); 161 } 162 return; 163 } 164 !data.enableSRA && slotToRenderMap.add(slot); 165 } 166 167 } 168 }); 169} 170/** End GPT Tag **/ 171 172//handler for GPT limited ads script loaded 173function gamScriptLoaded(isloaded) { 174 window.checkAdBlocker = true; 175 if (!isloaded) { //adblocker is enabled 176 window.googletag._loaded_ = false; 177 deleteGAMCookies(true); 178 }else{ 179 checkCookieConsentAndSetGAMoptions(); 180 } 181 window.dispatchEvent(new CustomEvent('adBlockerLoadChecked', { detail: { adblockerEnabled: isloaded } })); 182} 183//start the timer for search page tracking to refresh slots 184setSearchPageTrackingTimer = function () { 185 timerId = setTimeout(() => { 186 slotGeneratorRefresh(); 187 timerId = null; 188 }, delayDuration); 189} 190//refresh slots when timer is completed or event from pgm search is received 191slotGeneratorRefresh = function () { 192 if (slotToRenderMap.size > 0 || slotsToRefreshWithEnableSRA.size > 0) { 193 const slots = [...slotToRenderMap.values(), ...slotsToRefreshWithEnableSRA.values()]; 194 // set ado_vid to restricted for all slots having criteoData key when tracking is disabled on search page 195 if(isSearchPage && window.isSearchGamTrackingEnabled === false) { 196 slots?.forEach(slot => { 197 if (slot.getTargeting('criteoData')?.length > 0) { 198 slot.setTargeting('ado_vid', 'restricted'); 199 } 200 }); 201 } 202 203 // Check feature flag for changeCorrelator behavior when on
203search page 204 let refreshOptions = {}; 205 if (isSearchPage) { 206 const searchDuplicateGamAdsDisabled = window['SWY'] && window['SWY']['CONFIGSERVICE'] && window['SWY']['CONFIGSERVICE']['featureToggleConfig'] && window['SWY']['CONFIGSERVICE']['featureToggleConfig']['searchDuplicateGamAdsDisabled'] === true; 207 refreshOptions = searchDuplicateGamAdsDisabled ? {} : { changeCorrelator: false }; 208 } 209 210 googletag.pubads().refresh(slots, refreshOptions); 211 slotToRenderMap.clear(); 212 slotsToRefreshWithEnableSRA.clear(); 213 timerId = null; 214 } 215} 216 217//check for cookie consent feature flag and events from Onetrust 218checkCookieConsentAndSetGAMoptions = function () { 219 const cookieConsentCheckEnable = window['SWY']['CONFIGSERVICE']['featureToggleConfig']['cookieConsentCheckDisable'] !== true ? true : false; 220 if (cookieConsentCheckEnable) { 221 const oneTrustLoadedEvent = window['dataLayer'] && window['dataLayer'].find(x => x.event === 'OneTrustGroupsUpdated'); 222 if (oneTrustLoadedEvent) { 223 // setting the values of one trust flag variables on the basis of user's cookie selection 224 if(oneTrustLoadedEvent['OnetrustActiveGroups']){ 225 setFlagsForOneTrustCookies(oneTrustLoadedEvent['OnetrustActiveGroups']); 226 } 227 // check if targetting cookies are enabled and page is not restricted page 228 if (window.isConsentCookieEnabled['targeting'] && !isGamTrackingDisabledOnPage()) { 229 setLimitedAdsOption(false); 230 } 231 // in case targetting cookies are disabled or its a restricted page set the limitedads option to true and delete GAM related cookies 232 else { 233 setLimitedAdsOption(true); 234 deleteGAMCookies(); 235 } 236 if (!window.isConsentCookieEnabled['functional'] || window.isWinePage) { 237 // deleting gam related cookies in case functional cookies are disabled or it is a wine page 238 deleteGAMCookies(); 239 } 240 241 } 242 else { 243 // OneTrustLoaded event not fired yet - listen & then check whether cookies types are enabled or disabled 244 window.addEventListener("OneTrustGroupsUpdated", (eventResponse) => { 245 const oneTrustLoadedEventResponse = eventResponse && eventResponse.detail; 246 // setting the values of one trust flag variables on the basis of user's cookie selection 247 if(oneTrustLoadedEventResponse){ 248 setFlagsForOneTrustCookies(oneTrustLoadedEventResponse); 249 } 250 // check if targetting cookies are enabled and page is not restricted page 251 if (window.isConsentCookieEnabled['targeting'] && !isGamTrackingDisabledOnPage()) { 252 setLimitedAdsOption(false); 253 254 } 255 // in case targetting cookies are disabled or its a restricted page set the limitedads option to true and delete GAM related cookies 256 else { 257 setLimitedAdsOption(true); 258 deleteGAMCookies(); 259 } 260 // deleting gam related cookies in case functional cookies are disabled or it is a wine page 261 if (!window.isConsentCookieEnabled['functional'] || window.isWinePage) { 262 deleteGAMCookies(); 263 } 264 }, { once: true }); 265 } 266 } 267 else { 268 // Cookie consent feature is not enabled 269 setLimitedAdsOption(false); 270 } 271} 272//update limitedAds option value 273setLimitedAdsOption = function (value) { 274 ltdEnabled = value; 275 googletag.cmd.push(function () { 276 googletag.pubads().setPrivacySettings({ 277 limitedAds: ltdEnabled 278 }); 279 }); 280} 281 282//checking whether tracking is enabled/disabled on the PDP or other page 283isGamTrackingDisabledOnPage = function (value) { 284 285 if (isPDPPage){ 286 return document.querySelector(`[data-isProduct-tracking-disabled]`)?.getAttribute('data-isProduct-tracking-disabled') === 'true' ? true : false;
287 } else if (window.isRestrictedPage === true) { 288 return true; 289 } else { 290 return false; 291 } 292} 293// delete all GAM related cookies 294deleteGAMCookies = function (deleteAllCookies = false) { 295 const bannerName = window?.SWY?.CONFIGSERVICE?.getResolvedBanner(); 296 //targeting cookies 297 const GAMcookies = ["__gads", "__gpi", "__gpi_optout", "GED_PLAYLIST_ACTIVITY"]; 298 //functional cookies 299 if (!window.isConsentCookieEnabled['functional'] || deleteAllCookies) { 300 GAMcookies.push("__eoi"); 301 } 302 // "NID", "AEC", "1P_JAR", "IDE", "APC", "test_cookie", "DSID", "id" - these are 3rd party GAM related cookies, set on different domain (google, googlesyndication, doubleclick.net) and will not be populated once limitedAds is set to true. 303 for (let cookieName of GAMcookies) { 304 document.cookie = cookieName + `=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.${bannerName}.com`; 305 } 306} 307 308// check for performance cookie value in OptanonConsent cookie 309function getPerformanceCookieValue(){ 310 return getCookieValue('C0002'); 311} 312 313// check for functional cookie value in OptanonConsent cookie 314function getFunctionalCookieValue(){ 315 return getCookieValue('C0003'); 316} 317 318// check for targeting cookie value in OptanonConsent cookie 319function getTargetingCookieValue(){ 320 return getCookieValue('C0004'); 321} 322 323// method to get the values from the optanonConsent cookie related to user consent 324function getCookieValue(inputValue){ 325 const cookieValueOptanonConsent = document.cookie.split("; ").find((row) => row.startsWith("OptanonConsent="))?.split("="); 326 let cookieValueOptanonConsentValue = 0; 327 let isCookieUnavailable = false; 328 if (cookieValueOptanonConsent) { 329 const index = cookieValueOptanonConsent.findIndex(value => value.includes(inputValue)); 330 if (index !== -1) { 331 let keyIndex = inputValue === 'C0003' ? 3 : inputValue === 'C0004' ? 2 : inputValue === 'C0002' ? 1 : null 332 keyIndex != null && (cookieValueOptanonConsentValue = decodeURIComponent(cookieValueOptanonConsent[index]).split(',')[keyIndex][6]); 333 } else { 334 isCookieUnavailable = true; 335 } 336 } 337 return !cookieValueOptanonConsent || isCookieUnavailable || cookieValueOptanonConsentValue == 1 338} 339 340// function to set the values of one trust flag variables on the basis of user's cookie selection 341function setFlagsForOneTrustCookies(oneTrustEventVariable) { 342 // check if C0002 part is available which represents Performance cookie acceptance 343 window.isConsentCookieEnabled['performance'] = oneTrustEventVariable?.includes('C0002'); 344 // check if C0003 part is available which represents functional cookie acceptance 345 window.isConsentCookieEnabled['functional'] = oneTrustEventVariable?.includes('C0003'); 346 // check if C0004 part is available which represents Targeting cookie acceptance 347 window.isConsentCookieEnabled['targeting'] = oneTrustEventVariable?.includes('C0004'); 348} 349 350//Inject the GPT script dynamically if functional cookies are enabled and the script is not already loaded 351if (isGPTScriptNotLoaded() && window.isConsentCookieEnabled) { 352 if (window.isConsentCookieEnabled.functional === true) { 353 //Inject the gpt.js script dynamically 354 //<script src="https://pagead2.googlesyndication.com/tag/js/gpt.js" onerror="gamScriptLoaded(false)" onload="gamScriptLoaded(true)" async></script> 355 const script = document.createElement('script'); 356 script.src = 'https://pagead2.googlesyndication.com/tag/js/gpt.js'; 357 script.async = true; 358 script.onload = () => gamScriptLoaded(true); 359 script.onerror = () => gamScriptLoaded(false); 360 document.head.appendChild(script); 361 } 362 if (window.isConsentCookieEnabled.functional === false) { 363 gamScriptLoaded(false); 364 } 365} 366 367function isGPTScriptNotLoaded() { 368 return document.querySelectorAll('[src="https://pagead2.googlesyndication.com/tag/js/gpt.js"]').length < 1; 369} 370// Fix for ads not refreshing on back navigation on mobile devices 371const mobileCTAFixDisabled = window['SWY']?.CONFIGSERVICE?.featureToggleConfig?.mobileCTAFixDisabled === true; 372//Add homepage check to avoid running this code on all pages 373const isHomePage = !!document.querySelector('[content="nextgen-home-page"]') || 374 (window['AB']?.DATALAYER?.isHomePage && typeof window['AB'].DATALAYER.isHomePage === 'function' && window['AB'].DATALAYER.isHomePage()); 375 376if (!mobileCTAFixDisabled && isMobileDevice() && typeof googletag !== 'undefined' && isHomePage) { 377 // Initialize backAction state 378 window.backAction = false;
379 let pageHidden = false; 380 let refreshTriggered = false; 381 382 // Track when page becomes hidden 383 function handlePageHide() { 384 pageHidden = true; 385 refreshTriggered = false; 386 window.backAction = false; 387 console.warn('GAM Fix - Page hidden - ' + window.location.href); 388 } 389 390 // Handle page visibility restoration 391 function handlePageShow() { 392 if (pageHidden && !refreshTriggered) { 393 gamSlotReloadBackNavigation(); 394 refreshTriggered = true; 395 } 396 pageHidden = false; 397 } 398 399 // Enhanced slot validation and refresh 400 function gamSlotReloadBackNavigation() { 401 const slotsToRefresh = ['topc2', 'topc3', 'topc5', 'toprightb1', 'toprightb2']; 402 googletag.cmd.push(function() { 403 //topc2,topc3,topc5,toprightb1,toprightb2 404 const activeSlots = googletag.pubads().getSlots().filter(slot => { 405 const element = document.getElementById(slot.getSlotElementId()); 406 const adUnitPath = slot.getAdUnitPath(); 407 return element && slotsToRefresh.some(slotName => adUnitPath.includes(slotName)); 408 }); 409 if (activeSlots.length > 0) { 410 //set some tragetting if required before refresh 411 activeSlots.forEach(slot => { 412 slot.setTargeting('gam_backnav', 'true'); 413 }); 414 googletag.pubads().refresh(activeSlots); 415 console.warn('GAM Fix - Page visible - refreshed ads after back navigation: ' + window.location.href + " - Slots refreshed: " + activeSlots.map(slot => slot.getSlotElementId()).join(', ')); 416 } 417 }); 418 window.backAction = true; 419 } 420 421 // Multiple event listeners for comprehensive mobile support 422 423 // 1. Page visibility API (most reliable) 424 document.addEventListener('visibilitychange', function() { 425 if (document.visibilityState === 'hidden') { 426 handlePageHide(); 427 } else if (document.visibilityState === 'visible') { 428 // Small delay to ensure DOM is ready after back navigation 429 setTimeout(handlePageShow, 100); 430 } 431 }); 432 433 // 2. Page lifecycle events (iOS Safari specific) 434 window.addEventListener('pagehide', handlePageHide); 435 436 window.addEventListener('pageshow', function(e) { 437 // Check if coming from back/forward cache 438 const cameFromBFCache = e.persisted || 439 (performance.getEntriesByType && 440 performance.getEntriesByType('navigation')[0]?.type === 'back_forward'); 441 442 if (cameFromBFCache || pageHidden) { 443 // Delay to ensure proper execution on mobile 444 setTimeout(handlePageShow, 150); 445 } 446 }); 447 448 // 3. Focus events (additional mobile support) 449 window.addEventListener('focus', function() { 450 if (pageHidden) { 451 setTimeout(handlePageShow, 100); 452 } 453 }); 454 455 // 4. Beforeunload cleanup 456 window.addEventListener('beforeunload', function() { 457 console.warn('GAM Fix - Leaving window - ' + window.location.href); 458 handlePageHide(); 459 }); 460 461 // 5. Mobile-specific: popstate event for back button 462 window.addEventListener('popstate', function() { 463 // Small delay to allow page to settle 464 setTimeout(() => { 465 if (!refreshTriggered) { 466 gamSlotReloadBackNavigation(); 467 refreshTriggered = true; 468 } 469 }, 200); 470 }); 471} 472 473window.addEventListener('message', (event) => { 474 adjustIframeHeight(event); 475 processCriteoEvent(event); 476 processOneTagEvent(event); 477}); 478 479//Adjust height of iframe 480adjustIframeHeight = function(event) { 481 const eventData = event?.data?.data; 482 // Process the data based on its type or content 483 if (event?.data?.type !== 'adjust-banner-height' || !eventData || !eventData?.id) { 484 return; 485 } 486 const iframeElement = document.querySelector('[id*="' + eventData?.id + '"] iframe'); 487 console?.warn('GAM Height data:', event.data); 488 if(eventData?.height && iframeElement) { 489 iframeElement.style.height = eventData?.height; 490 } 491} 492 493// Process Criteo backfill events 494processCriteoEvent = function(event) { 495 const eventData = event?.data?.data; 496 // Process the data based on its type or content 497 if (event?.data?.type !== 'criteo-backfill' || !eventData || !eventData?.id) { 498 return; 499 } 500 const iframeElement = document.querySelector('[id*="' + eventData?.id + '"] iframe'); 501 const adParent = iframeElement?.closest(".google-adManager"); 502 const criteoProcessed = adParent?.getAttribute('data-criteo-processed'); 503 504 if (criteoProcessed === 'true') { 505 //Already processed the event for this iframe, return 506 console.warn('Criteo backfill event already processed for iframe id:', eventData?.id || 'unknown'); 507 //Adjust the height if provided in event data 508 if (eventData?.height && iframeElement) { 509 iframeElement.style.height = eventData?.height; 510 } 511 return; 512 } 513 514 console?.warn('Criteo backfill data:', event.data); 515 516 if (event?.data?.status === 'success') { 517 console?.warn('Criteo backfill success:', event?.data?.message || 'Criteo Render successful'); 518 519 if (eventData?.height && iframeElement) { 520 iframeElement.style.height = eventData?.height; 521 } else { 522 //find all iframes matching event source and set the height 523 const iframes = document.querySelectorAll('iframe');
524 iframes.forEach(iframe => { 525 if (iframe.contentWindow === event.source) { 526 iframe.style.height = 'auto'; 527 } 528 }); 529 } 530 //Remove loading animation class once the criteo event is processed successfully. For error scenario we wait for refresh 531 adParent?.classList.remove('banner-unit__injection--loading-animation'); 532 } else if (event?.data?.status === 'error' || event?.data?.status === 'failure') { 533 console.warn('Criteo backfill error:', event?.data?.message || 'Unknown error'); 534 //refresh the ad 535 if (eventData?.id && googletag?.pubads) { 536 //find the slot and refresh using id 537 const slotToRefresh = googletag.pubads().getSlots().find(slot => slot.getAdUnitPath().includes(eventData?.id)); 538 slotToRefresh.setTargeting('type', 'backfill'); 539 googletag.pubads().refresh([slotToRefresh]); 540 //This setTimeout is to ensure that iframe has correct height after refresh 541 setTimeout(() => { 542 //Get the id element again after refresh 543 const idElement = document.querySelector('[id*="' + eventData?.id + '"]'); 544 //Adjust the iframe height if idElement and iframe exist 545 if (idElement && idElement.querySelector('iframe')) { 546 const iframe = idElement.querySelector('iframe'); 547 // Check if idElement has a valid height set 548 const elementHeight = idElement.style.height; 549 // If height is in 'px', set the iframe height accordingly - this is to fix the height issue for Ad Exchange ads 550 if (elementHeight && elementHeight.includes('px')) { 551 iframe.style.height = elementHeight; 552 const adParent = idElement.closest('.nextgen-banner-unit__injection'); 553 if (adParent) { 554 adParent.style.height = elementHeight; 555 adParent.style.textAlign = 'center'; 556 } 557 } 558 } 559 }, 2000); 560 } 561 } 562 //Set processed flag to true to avoid multiple event listener addition 563 adParent?.setAttribute('data-criteo-processed', 'true'); 564} 565 566// Process OneTag events 567processOneTagEvent = function(event) { 568 if (event.data && event.data.eventType === 'onetag-onload-gam') { 569 const { bannerTitle, bannerType, adUnitPath, recommendationType } = event.data; 570 if (adUnitPath) { 571 // Check if OneTag is enabled for this specific GAM slot 572 const gamSectionId = adUnitPath?.substring(adUnitPath.lastIndexOf('/') + 1); 573 const gamElement = document.querySelector(`[data-bannerposition="${gamSectionId}"][data-onetag-events-enabled="true"]`); 574 575 if (!gamElement) { 576 // OneTag events not enabled for this slot 577 return; 578 } 579 580 const iframeElement = document.querySelector(`[id*="${adUnitPath}"] iframe`); 581 const adParent = iframeElement?.closest(".google-adManager"); 582 583 if (adParent) { 584 // Create the data object for the one-tag-event attribute 585 const oneTagEventData = { 586 bannerTitle, 587 bannerType, 588 bannerPlacement: AB.DATALAYER.getBannerPlacementForGAM(iframeElement), 589 recommendationType 590 }; 591 // Set the data-one-tag-event attribute 592 adParent.setAttribute('data-one-tag-event', JSON.stringify(oneTagEventData)); 593 } else { 594 console.warn('OneTag onload GAM: Could not find parent element for adUnitPath:', adUnitPath); 595 } 596 } 597 } 598} 599 600// Helper function to wait for data attributes using MutationObserver 601function waitForDataAttribute(element, attributeName, callback, timeout = 5000) { 602 // Check if attribute already exists 603 const existingValue = element ? element.getAttribute(attributeName) : null;
604 if (existingValue) { 605 callback(existingValue); 606 return; 607 } 608 609 if (!element) { 610 console.warn(`Element not found for ${attributeName} attribute`); 611 return; 612 } 613 614 const observer = new MutationObserver((mutations) => { 615 mutations.forEach((mutation) => { 616 if (mutation.type === 'attributes' && mutation.attributeName === attributeName) { 617 const value = element.getAttribute(attributeName); 618 if (value) { 619 observer.disconnect(); 620 callback(value); 621 } 622 } 623 }); 624 }); 625 626 // Start observing 627 observer.observe(element, { 628 attributes: true, 629 attributeFilter: [attributeName] 630 }); 631 632 // Cleanup after timeout 633 setTimeout(() => { 634 observer.disconnect(); 635 }, timeout); 636} 637 638// Updated isBackfillEligbleSlot function 639function isBackfillEligbleSlot(aisleId, pageId, bannerVariation, bannerPosition) { 640 const isCriteoBackfillEnabled = window?.SWY?.CONFIGSERVICE?.featureToggleConfig?.criteoBackfillEnabled; 641 642 if (!isCriteoBackfillEnabled || !window?.CRITEO_CONFIG || !pageId || !bannerVariation || !bannerPosition) { 643 return false; 644 } 645 646 // Get page-specific configuration 647 const pageConfig = window?.CRITEO_CONFIG?.pageConfigs?.[pageId]; 648 if (!pageConfig) { 649 return false; 650 } 651 652 // Check position and variation eligibility first (faster checks) 653 const isValidPosition = pageConfig.eligiblePositions?.includes(bannerPosition); 654 const isValidVariation = pageConfig.eligibleVariations?.includes(bannerVariation); 655 656 if (!isValidPosition || !isValidVariation) { 657 return false; 658 } 659 660 // Only check aisle eligibility for aisle pages 661 if (pageId.startsWith('aisles')) { 662 const eligibleAisles = pageConfig.eligibleAisles || window?.CRITEO_CONFIG?.eligibleAisles; 663 if (!eligibleAisles || !aisleId) return false; 664 665 return pageConfig.eligibleAisles 666 ? eligibleAisles.some(aisle => aisleId === aisle) 667 : eligibleAisles.some(aisle => aisleId === aisle || aisleId.startsWith(aisle + '_')); 668 } 669 670 // For non-aisle pages, return true if position and variation are valid 671 return true; 672} 673 674// Updated getBackfillSizeData function 675function getBackfillSizeData(type, bannerVariation) { 676 const variationConfig = window?.CRITEO_CONFIG?.bannerVariations?.[bannerVariation]; 677 return variationConfig ? variationConfig[type] : null; 678} 679 680//Function to get parameters for GAM call. 681function gamParamCall(gamId) { 682 const gamCmpData = document.getElementById(gamId); 683 var getGamId = gamCmpData.getAttribute('data-gamId'); 684 var isInsideCarousel = document.getElementById("gamcontainer_" + getGamId).closest(".abs-carousel") ? true : false; 685 var isGamTrackingDisabled = document.querySelector(`[data-isProduct-tracking-disabled]`)?.getAttribute('data-isProduct-tracking-disabled') === 'true' ? true : false; 686 const aisleL4Element = document.querySelector(`[data-l4-id]`); 687 var getFluid = gamCmpData.getAttribute('data-fluid'); 688 var isOneTagEnabled = gamCmpData.getAttribute('data-onetag-events-enabled'); 689 var isRestrictedPage = gamCmpData.getAttribute('data-isRestrictedPage') === 'true' ? true : false; 690 var getRuntimeContext = gamCmpData.getAttribute('data-runtimeContext'); 691 var getBannerVariation = gamCmpData.getAttribute('data-bannerVariation'); 692 var getPageId = gamCmpData.getAttribute('data-pageId'); 693 var categoryName = gamCmpData.getAttribute('data-categoryName'); 694 var aisleId = gamCmpData.getAttribute('data-aisleId'); 695 var bannerPosition = gamCmpData.getAttribute('data-bannerPosition'); 696 var isBackfill = isBackfillEligbleSlot(aisleId, getPageId, getBannerVariation, bannerPosition); 697 var getEnv = SWY.CONFIGSERVICE.env === 'prod' || SWY.CONFIGSERVICE.env === 'beta' ? '' : 'nonprod.'; 698 var currEnv = SWY.CONFIGSERVICE.env === 'prod' || SWY.CONFIGSERVICE.env === 'beta' ? 'prod' : 'nonprod';; 699 var codeArray=gamCmpData.getAttribute('data-gamNetworkCode')?.split(","); 700 var networkCode; 701 const isReloadEnabled = ['search-results', 'aisles-l3', 'aisles-l4', 'coupons-deals'].includes(getPageId); 702 var isRefreshBanner = false;
703 if(!!window?.SWY?.CONFIGSERVICE?.featureToggleConfig?.allDealsGAMBannersDisabled !== true) { 704 //Refresh the banner if it is used in any SPA 705 isRefreshBanner = gamCmpData.getAttribute('data-spaPage') === 'true'; 706 } 707 708 //enable ultra skinny one tag event tracking - this is to avoid authoring and dependency of other teams 709 if(window?.SWY?.CONFIGSERVICE?.featureToggleConfig?.ultraSkinnyOnetagTrackingEnabled === true && getBannerVariation === 'UltraSkinnyBanner') { 710 isOneTagEnabled = 'true'; 711 gamCmpData.setAttribute('data-onetag-events-enabled', 'true'); 712 } 713 714 // Generate and set uniqueid for OneTag enabled banners 715 var uniqueId = gamCmpData.getAttribute('data-uniqueid'); 716 if (isOneTagEnabled === 'true' && !uniqueId) { 717 uniqueId = 'web-' + AB.COMMON.generateUUID() || ''; 718 gamCmpData.setAttribute('data-uniqueid', uniqueId); 719 } 720 721 // Function to update the limited ads to true incase of myhealth data 722 if (isGamTrackingDisabled || isRestrictedPage) 723 { 724 setLimitedAdsOption(true); 725 deleteGAMCookies(); 726 } 727 728 for(var i=0;i<codeArray.length;i++){ 729 if(codeArray[i].startsWith(currEnv)){ 730 networkCode=codeArray[i].substring(codeArray[i].indexOf(":")+1); 731 break; 732 } 733 } 734 735 // Function to get filter selection from URL 736 getFilterParameter = function (filter) { 737 var filterParam = decodeURIComponent(window.location.search).split('&').find(e => e.indexOf(filter) > -1); 738 var getFilter = filterParam ? filterParam.split(filter + '=')[1].split('~').reduce((prev, curr, idx, arr) => { 739 if (prev.indexOf(curr) === -1) { 740 prev.push(filter + ":" + curr); 741 } 742 return prev; 743 }, []) : ''; 744 return getFilter; 745 } 746 decodeURIComponent(window.location.search).split('&').find(e => e.indexOf("nutrition") > -1) 747 748 749 750 var getNutrition = window.location.search.indexOf("nutritionContent") > -1 ? getFilterParameter("nutritionContent") : []; 751 var getBrand = window.location.search.indexOf("brand=") > -1 ? getFilterParameter("brand") : []; 752 //var getPrice = window.location.search.indexOf("price") > -1 ? getFilterParameter("price") : []; 753 var getFilterSelection = getNutrition.concat(getBrand); 754 755 var sizesData = getFluid == "true" ? "fluid" : JSON.parse(gamCmpData.getAttribute('data-bannerSizes')?.replace(/fluid/g, "\"fluid\"")); 756 var sizeMappingsData = getFluid == "true" ? null : JSON.parse(gamCmpData.getAttribute('data-sizeMappings')?.replace(/fluid/g, "\"fluid\"")); 757 const gamData_ = { 758 detail: { 759 sizes: isBackfill ? getBackfillSizeData('sizes', getBannerVariation) : sizesData, 760 sizeMapping: isBackfill ? getBackfillSizeData('sizeMappings', getBannerVariation) : sizeMappingsData, 761 targeting: [{ 762 txid: gamCmpData.getAttribute('data-taxonomyId'), 763 }, { 764 store: getStoreId(), 765 }], 766 isRefreshEnabled: (getBannerVariation == 'MediumBanner' && isReloadEnabled) || isRefreshBanner 767 }, 768 }; 769 770 if (window.location.href.indexOf("weeklyad") > -1) { 771 gamData_.detail['adUnitPath']= "/"+networkCode+"/" + getEnv + SWY.CONFIGSERVICE.resolvedBanner + "/weekly-ad/*/*.*/" + gamCmpData.getAttribute('data-bannerPosition'); 772 }else if (window.location.pathname.includes("product-details.") || window.location.pathname.includes("product/details/") || window.location.pathname.includes("/shop/pd/")) { 773 gamData_.detail['adUnitPath']="/"+networkCode+"/" + getEnv + getUpdatedPdpAdUnitPath(gamCmpData.getAttribute('data-adUnitPath'), aisleL4Element) + "/" + gamCmpData.getAttribute('data-bannerPosition'); 774 }else { 775 gamData_.detail['adUnitPath']="/"+networkCode+"/" + getEnv + gamCmpData.getAttribute('data-adUnitPath') + "/" + gamCmpData.getAttribute('data-bannerPosition'); 776 } 777 778 if(window.location.pathname.split("/").pop().split("-")[0] == 'search'){ 779 gamData_.detail.targeting.push({q :getQueryParameter("q")}); 780 } 781 782 if(window.location.pathname.includes('/loyalty/offer-details') && categoryName){ 783 gamData_.detail.targeting.push({q : categoryName }); 784 } 785 786 if (window.location.href.indexOf("aisle") > -1) { 787 788 var loc = window.location; 789 var pathArray = loc.pathname.split("/"); 790 if (pathArray.length == 3) { // adding if condition to check whether its L1 page and txpath should be "1" 791 gamData_.detail.targeting.push({ txpath: "1" }); 792 } else { 793 gamData_.detail.targeting.push({ txpath: gamCmpData.getAttribute('data-aisleId') }); 794 } 795 796 //Adding Shelf parameter for level 4 797 if (pathArray.length >= 6) { 798 var shelfName = pathArray[5].substring(0, pathArray[5].indexOf(".")); 799 gamData_.detail.targeting.push({ shelf: shelfName }); 800 } 801 802 803 if (getFilterSelection != '') { 804 gamData_.detail.targeting.push({ f: getFilterSelection }); 805 } 806 }else if(window.location.pathname.split("/").pop().includes("product-details.") || window.location.pathname.includes("product/details/") || window.location.pathname.includes("/shop/pd/")){ 807 //Adding Aisle L4 ID when on PDP 808 if(aisleL4Element) { 809 gamData_.detail.targeting.push({txpath :aisleL4Element.getAttribute(`data-l4-id`)}); 810 } 811 var bpn = window.location.pathname.split("/").pop(); 812 if(window.location.pathname.includes("product-details.")){ 813 bpn = bpn.substring(bpn.indexOf(".")+1,bpn.lastIndexOf(".")); 814 } 815 gamData_.detail.targeting.push({pid:bpn}); 816 }else{ 817 gamData_.detail.targeting.push({txpath :"1"}); 818 }
819 const fulfillmentChannelValue = getFulfillmentChannel(); 820 if(fulfillmentChannelValue){ 821 gamData_.detail.targeting.push({fc :fulfillmentChannelValue}); 822 } 823 if(getRuntimeContext!= null){ 824 gamData_.detail.targeting.push({rc :getRuntimeContext}); 825 } 826 if (!isGamTrackingDisabled && !isRestrictedPage && window.isConsentCookieEnabled['targeting'] && window.isConsentCookieEnabled['performance'] && 827 (getPageId !== 'search-results' || (getPageId == 'search-results' && window.isSearchGamTrackingEnabled))) { 828 if (getAdobeVisitorId() != null) { 829 gamData_.detail.targeting.push({ ado_vid: getAdobeVisitorId() }); 830 } 831 if (getClubCardNumber() != null) { 832 gamData_.detail.targeting.push({ ccn_sid: getClubCardNumber() }); 833 } 834 } 835 // One tag event handling 836 if(isOneTagEnabled == "true"){ 837 // Remove ado_vid or ccn_sid keys when one tag is enabled 838 gamData_.detail.targeting = gamData_.detail.targeting.filter(item => { 839 return !item.hasOwnProperty('ado_vid') && !item.hasOwnProperty('ccn_sid'); 840 }); 841 // Add uniqueid to targeting info for OneTag enabled banners 842 if(uniqueId) { 843 gamData_.detail.targeting.push({ uniqueid: uniqueId }); 844 } 845 } 846 // Criteo backfill handling for eligible pages 847 if(isBackfill){ 848 let view = window.innerWidth > 1023 ? 'desktop' : 'mobile'; 849 let pageId = "GAM_Test"; 850 let context = getQueryParameter("q"); 851 let isCategoryPage = window.location.href.indexOf("aisle") > -1; 852 let isSearchPage = getPageId == 'search-results'; 853 let isProductDetailsPage = window.location.pathname.split("/").pop().includes("product-details.") || window.location.pathname.includes("product/details/") || window.location.pathname.includes("/shop/pd/"); 854 if(isCategoryPage) { 855 pageId = "viewCategory_GAM_" + view; 856 const catId = $('[data-aisle-id]').attr('data-aisle-id'); 857 context = transformCatId(catId); 858 } else if(isSearchPage) { 859 pageId = "viewSearchResult_GAM_" + view; 860 } else if(isProductDetailsPage) { 861 pageId = "viewProductDetails_GAM_" + view; 862 let bpn = window.location.pathname.split("/").pop(); 863 if(window.location.pathname.includes("product-details.")){ 864 context = bpn.substring(bpn.indexOf(".")+1,bpn.lastIndexOf(".")); 865 } 866 } 867 //Set the render data & pass back tag 868 const criteoObj = { 869 "iframeId": [ gamData_.detail['adUnitPath'] ], 870 "parentUrl": [ window.location.origin] 871 }; 872 const encodedCriteoObj = encodeURIComponent(JSON.stringify(criteoObj).replace(/'/g, '"')).replace(/\*/g, '%2A'); 873 874 gamData_.detail.targeting.push({ context : encodeURI(getQueryParameter("context") ? getQueryParameter("context") : context) }); 875 gamData_.detail.targeting.push({ pa : getQueryParameter("pa") ? getQueryParameter("pa") : pageId }); 876 gamData_.detail.targeting.push({ pl : getQueryParameter("pl") ? getQueryParameter("pl") : "Banner" }); 877 gamData_.detail.targeting.push({ kwd : getQueryParameter("kwd") ? getQueryParameter("kwd") : getQueryParameter("q") }); 878 gamData_.detail.targeting.push({ regionId : getQueryParameter("regionId") ? getQueryParameter("regionId") : getRegionId() }); 879 gamData_.detail.targeting.push({ gdpr : getQueryParameter("gdpr") ? getQueryParameter("gdpr") : 2 }); 880 gamData_.detail.targeting.push({ criteoData : encodedCriteoObj }); 881 if(isGamTrackingDisabled || isRestrictedPage || (isSearchPage && window.isSearchGamTrackingEnabled === false)) { 882 // Remove ado_vid when tracking is disabled or restricted page and set its value to 'restricted' 883 gamData_.detail.targeting = gamData_.detail.targeting.filter(item => { 884 return !item.hasOwnProperty('ado_vid'); 885 }); 886 gamData_.detail.targeting.push({ ado_vid: 'restricted' }); 887 } 888 } 889 if (!isInsideCarousel) { 890 //Adblocker check to remove the banner element when GAM is not inside carousel 891 if (window.googletag._loaded_ !== undefined && window.googletag._loaded_ === false) { 892 var bannerUnitDiv = document.getElementById("gamcontainer_" + getGamId).closest(".banner-unit.banner-unit-rules"); 893 if (bannerUnitDiv){ 894 $(bannerUnitDiv).remove(); 895 } 896 } 897 //Check to collapse the empty slots when GAM is not inside carousel
898 else { 899 googletag.cmd.push(function () { 900 googletag.pubads().addEventListener("slotResponseReceived", function (event) { 901 if (!event.slot.getResponseInformation()) { 902 const slotEl = document.getElementById(event.slot.getSlotElementId()); 903 const adParent = slotEl?.closest(".banner-unit.banner-unit-rules") 904 || slotEl?.closest(".google-ad-manager") 905 || slotEl?.closest(".collection") 906 || slotEl?.closest(".google-adManager.flex-gam_injection.flex-gam-banner"); 907 if (adParent) { 908 adParent.classList.add('collapse'); 909 } 910 }else{ 911 const adParent = document.getElementById(event.slot.getSlotElementId())?.closest(".google-adManager"); 912 //Additional check to not remove the loading animation class for criteo ads as they take longer time to load 913 const isCriteoSlot = event.slot?.getTargeting('criteoData')?.length > 0 && event.slot?.getTargeting('type')?.[0] !== 'backfill'; 914 if (adParent) { 915 if (isCriteoSlot) { 916 setTimeout(() => { 917 // Original logic for removing loading animation 918 const dataAttr = adParent.getAttribute('data-criteo-processed'); 919 if (!dataAttr) { 920 adParent.classList.remove('banner-unit__injection--loading-animation'); 921 } 922 }, 3000); // Changed to 3 seconds as required 923 } else { 924 adParent.classList.remove('banner-unit__injection--loading-animation'); 925 } 926 } 927 } 928 }); 929 }); 930 } 931 } 932 933 //Add eventlistener only when currentPage is search or aisle and variation is medium to refresh 934 if (gamData_?.detail?.isRefreshEnabled) { 935 document.addEventListener("reloadGAM_" + getGamId, (event) => { 936 const sectionId = 'gamcontainer_' + event.detail.bannerGAMId; 937 googletag.pubads().refresh(googletag.pubads().getSlots().filter((item) => item.sectionId == sectionId), { 938 changeCorrelator: false, 939 }); 940 }); 941 942 } 943 //ADA - fix iOS device defect which skips announcing the iframe title, aria-label 944 if(isInsideCarousel || !isIOSDevice()){ 945 $(gamCmpData)?.siblings('.gam-adContentLabel')?.remove(); 946 } 947 948 return gamData_; 949} 950 951// Function to get Club Card Number for Authenticated User 952getClubCardNumber = function () { 953 const clubCardNumber = window['AB']['COMMON'].authorize() ? AB.userInfo.clubCard : null; 954 return clubCardNumber; 955}; 956 957// Function to get Adobe Visitor Id 958getAdobeVisitorId = function () { 959 const adobeVisitorId = AB.COMMON.getCookie('AMCV_A7BF3BC75245ADF20A490D4D@AdobeOrg') ? AB.COMMON.getCookie('AMCV_A7BF3BC75245ADF20A490D4D@AdobeOrg').match(/MCMID\|\d+(?=\|)/)[0].split('|')[1] : null; 960 return adobeVisitorId; 961}; 962 963// Function to get cat id 964function transformCatId(catId) { 965 if(!catId) { 966 return catId; 967 } 968 let transformedCatId = catId; 969 //Remove 1_ from the beginning of the catId if it exists. Then replace all underscores with >. 970 if (catId.startsWith('1_')) { 971 transformedCatId = catId.substring(2); 972 } 973 return transformedCatId.replace(/_/g, '>'); 974} 975//function to get the regionId 976function getRegionId() { 977 let fulfillmentChannel = getFulfillmentChannel(); 978 return getStoreId() + (fulfillmentChannel == 'DEL' ? 'DLY' : fulfillmentChannel); 979} 980// Function to get query parameter from URL 981getQueryParameter = function (query) { 982 const match = RegExp('[?&]' + query + '=([^&]*)', 'i').exec(window.location.search); 983 return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); 984}; 985 986// Helper function to create GAM data object from event data 987function createGamDataForOneTag(dataAttribute, event, uniqueId) { 988 const dataOneTagEvent = JSON.parse(dataAttribute); 989 990 return { 991 bannerTitle: dataOneTagEvent?.bannerTitle || '', 992 bannerType: dataOneTagEvent?.bannerType || '', 993 bannerPlacement: dataOneTagEvent?.bannerPlacement || '', 994 recommendationType: dataOneTagEvent?.recommendationType || '', 995 adUnitPath: event.slot.getAdUnitPath() || '', 996 lineItemId: event.lineItemId ? String(event.lineItemId) : '', 997 creativeId: event.creativeId ? String(event.creativeId) : '', 998 creativeTemplateId: event.creativeTemplateId ? String(event.creativeTemplateId) : '', 999 responseStatus: event.slot && event.slot.isEmpty ? 'failure' : 'success', 1000 uniqueID: uniqueId, 1001 }; 1002} 1003 1004googletag.cmd.push(function () { 1005 // Function for ADA handler in mobile browser 1006 googletag.pubads().addEventListener("slotOnload", function (event) { 1007 if (event.slot.getResponseInformation()) { 1008 document.getElementById("google_ads_iframe_" + event.slot.getSlotId().getId()).role = "none"; 1009 } 1010 }); 1011 // send banner placement value to iframe 1012 googletag.pubads().addEventListener("slotOnload", (function (event) { 1013 if (event.slot.getResponseInformation()) { 1014 let iframe = document.getElementById("google_ads_iframe_" + event.slot.getSlotId()); 1015 if(/topc\d+_\d+$/.test("google_ads_iframe_" + event.slot.getSlotId())) { 1016 iframe.style.touchAction = 'pan-y'; 1017 } 1018 if (iframe.src != '') { 1019 let obj = { bannerPlacement: AB.DATALAYER.getBannerPlacementForGAM(iframe), windowSize: window.innerWidth}; 1020 if (window.googletag._iframeOrigin_ == undefined && iframe.src) 1021 window.googletag._iframeOrigin_ = new URL(iframe.src)?.origin 1022 iframe.contentWindow?.postMessage(obj, iframe.src); 1023 } 1024 } 1025 } 1026 )); 1027 // Add slotRenderEnded event listener 1028 googletag.pubads().addEventListener("slotRenderEnded", function(event) { 1029 // Find the iframe and its parent div to capture data-one-tag-event 1030 const slotElementId = event.slot.getSlotElementId(); 1031 const slotElement = document.getElementById(slotElementId); 1032 const parentDiv = slotElement?.closest(".google-adManager"); 1033 1034 if (parentDiv) { 1035 // Get uniqueid from the banner's data attribute 1036 const gamSection = parentDiv?.querySelector("gam-section"); 1037 const uniqueId = gamSection?.getAttribute('data-uniqueid') || ''; 1038 1039 // Wait for data-one-tag-event attribute to be available using MutationObserver 1040 waitForDataAttribute(parentDiv, 'data-one-tag-event', (dataAttribute) => { 1041 const sendGamData = createGamDataForOneTag(dataAttribute, event, uniqueId); 1042 AB.DATALAYER.setExperimentExposureGAM(sendGamData); 1043 }); 1044 } else { 1045 console.warn('slotRenderEnded - parentDiv not found'); 1046 } 1047 }); 1048 // Add impressionViewable event listener 1049 googletag.pubads().addEventListener("impressionViewable", function(event) { 1050 // Find the iframe and its parent div to capture data-one-tag-event 1051 const slotElementId = event.slot.getSlotElementId(); 1052 const slotElement = document.getElementById(slotElementId); 1053 const parentDiv = slotElement?.closest(".google-adManager"); 1054 1055 if (parentDiv) { 1056 // Get uniqueid from the banner's data attribute 1057 const gamSection = parentDiv?.querySelector("gam-section"); 1058 const uniqueId = gamSection?.getAttribute('data-uniqueid') || ''; 1059 1060 // Wait for data-one-tag-event attribute to be available using MutationObserver 1061 waitForDataAttribute(parentDiv, 'data-one-tag-event', (dataAttribute) => { 1062 const sendGamData = createGamDataForOneTag(dataAttribute, event, uniqueId); 1063 AB.DATALAYER.setBannerImpressionsGAM(sendGamData); 1064 }); 1065 } else { 1066 console.warn('impressionViewable - parentDiv not found'); 1067 } 1068 }); 1069 1070}); 1071// GAM banner unit tracking invoked by iframe event 1072window.addEventListener("message", (event) => { 1073 if (event.origin !== window.googletag._iframeOrigin_) return; 1074 if (event.data && event.data.bannerPlacement && event.data.isClicked) { 1075 AB.DATALAYER.setBannerUnitTrackingGAM(event); 1076 } 1077 //capture swipe in iframe and inform the hero carousel component 1078 if(event.data && event.data.isHeroBannerCarousel && event.data.isSwiped){ 1079 updateHeroCarouselSwipe(event.data.direction); 1080 } 1081 //capture swipe in iframe and inform the hero carousel component 1082 if(event.data && event.data.isHeroCanvasCarousel && event.data.isSwiped){ 1083 updateHeroCanvasCarouselSwipe(event.data.direction); 1084 } 1085 //capture swipe in iframe and inform the flex banner carousel component 1086 if(event.data && event.data.isFlexBannerCarousel && event.data.isSwiped){ 1087 updateFlexBannerCarouselSwipe(event.data.direction); 1088 } 1089 }, 1090 false 1091); 1092isOneTagEnabledAdSlot = function(slot) { 1093 //get the adunit path and check if gam-section contains data-onetag-events-enabled=true
1094 const adUnitPath = slot?.getAdUnitPath(); 1095 const gamSectionId = adUnitPath?.substring(adUnitPath.lastIndexOf('/') + 1); 1096 return document.querySelector('[data-bannerposition="' + gamSectionId + '"][data-onetag-events-enabled="true"]'); 1097}; 1098// Add event listener to check if tracking is enabled on search page and refresh the slots accordingly with PII data 1099document.addEventListener('search-tracking-enabled', function (e) { 1100 if (isSearchPage && window.isConsentCookieEnabled['targeting'] && window.isConsentCookieEnabled['performance']) { 1101 window.isSearchGamTrackingEnabled = e?.detail?.isTrackingEnabled 1102 setLimitedAdsOption(!e?.detail?.isTrackingEnabled); // set limited ads option to true if tracking is disabled 1103 !e?.detail?.isTrackingEnabled && deleteGAMCookies(); 1104 if (e?.detail?.isTrackingEnabled && googletag?.pubads !== undefined) { 1105 if (getAdobeVisitorId() != null) { 1106 [...slotToRenderMap, ...slotsToRefreshWithEnableSRA].map(x => { 1107 if(!isOneTagEnabledAdSlot(x)) { 1108 x.setTargeting('ado_vid', getAdobeVisitorId()); 1109 } 1110 }); 1111 } 1112 if (getClubCardNumber() != null) { 1113 [...slotToRenderMap, ...slotsToRefreshWithEnableSRA].map(x => { 1114 if(!isOneTagEnabledAdSlot(x)) { 1115 x.setTargeting('ccn_sid', getClubCardNumber()); 1116 } 1117 }); 1118 } 1119 } 1120 //when tracking is disabled, set ado_vid to 'restricted' for Criteo ads to set correct retail_visitor_id in Criteo rmgam script 1121 if (!e?.detail?.isTrackingEnabled && googletag?.pubads !== undefined && (slotToRenderMap?.size > 0 || slotsToRefreshWithEnableSRA?.size > 0)) { 1122 [...slotToRenderMap, ...slotsToRefreshWithEnableSRA].forEach(slot => { 1123 if (slot.getTargeting('criteoData')?.length > 0) { 1124 slot.setTargeting('ado_vid', 'restricted'); 1125 } 1126 }); 1127 } 1128 slotGeneratorRefresh(); 1129 } 1130}, { once: true }); 1131 1132 1133// Function to check feature flag, indicator of wine page or contains restricted search terms on search page 1134// for enabling or disabling standalone GAM component 1135checkGamFeatureFlag = function (featureFlagName, featureFlagValue, gamSectionId) { 1136 const gamElement = document.getElementById(gamSectionId); 1137 if(!gamElement) { 1138 return false; 1139 } 1140 let restrictedSearchTermPage = false; 1141 // checking whether its a search page and if so, suppressing the GAM calls and removing the cookies 1142 // related to GAM on the basis of comparison wih search term value. 1143 const isSearchPage = window.location.pathname?.toLowerCase().indexOf('shop/search-results') != -1; 1144 if (isSearchPage) { 1145 let restrictedSearchTermList = gamElement.getAttribute('data-restrictedSearchTermList'); 1146 restrictedSearchTermPage = isRestrictedSearchTermPage(restrictedSearchTermList); 1147 if (restrictedSearchTermPage) { 1148 handleRestrictedPage(); 1149 } 1150 } 1151 let gamFeatureFlagName = (gamElement != null && gamElement.hasAttribute('data-gamFeatureFlag')) ? `${featureFlagName}`.trim() : false; 1152 let gamFeatureFlagValue = (gamElement != null && gamElement.hasAttribute('data-gamFeatureFlagValue')) ? `${featureFlagValue}` : `true`; 1153 if ((!gamFeatureFlagName || (gamFeatureFlagName && String(SWY.CONFIGSERVICE.featureToggleConfig[gamFeatureFlagName]) !== gamFeatureFlagValue)) && window.isConsentCookieEnabled['functional'] && !window.isWinePage && !restrictedSearchTermPage) { 1154 return true; 1155 } else { 1156 gamElement.parentElement.parentElement.remove(); 1157 return false; 1158 } 1159}; 1160 1161// Function to check whether query pararmeter value comes under restricted search terms to enable or disable GAM component on the pages. 1162isRestrictedSearchTermPage = function (restrictedSearchTermList) { 1163 let restrictedSearchTermListArray = JSON.parse(restrictedSearchTermList.toLowerCase()); 1164 //Remove all spaces from the search terms and convert them to lowercase 1165 restrictedSearchTermListArray = restrictedSearchTermListArray?.map((term) => term?.replace(/\s/g, '')?.toLowerCase()); 1166 //Remove all spaces from the query parameter and convert it to lowercase 1167 const queryParam = getQueryParameter("q")?.replace(/\s/g, '').toLowerCase(); 1168 return restrictedSearchTermListArray?.includes(queryParam); 1169}; 1170 1171// Function to update adUnitPath for PDP pages
1172getUpdatedPdpAdUnitPath = function (adUnitCurrentPath, aisleL4Element) { 1173 if(aisleL4Element) { 1174 const pdpHierarchy = aisleL4Element.querySelector('a')?.getAttribute('href'); 1175 if (!pdpHierarchy || !adUnitCurrentPath) return adUnitCurrentPath; 1176 let arrayContainingHierarchyInfo = pdpHierarchy.split('/'); 1177 let hierarchyToBereplaced = arrayContainingHierarchyInfo[3].concat('/',arrayContainingHierarchyInfo[4]); 1178 let adUnitFinalPath = adUnitCurrentPath.replace('*/*',hierarchyToBereplaced); 1179 return adUnitFinalPath; 1180 } else { 1181 return adUnitCurrentPath; 1182 } 1183}; 1184 1185// handle swipe on hero carousel GAM iframe 1186updateHeroCarouselSwipe = function (direction) { 1187 let carousel = document.getElementsByTagName(NextgenAbsCarouselStaticConfig.SELECTORS.NEXTGEN_ABS_CAROUSEL_WC); 1188 if (carousel && carousel.length > 0) { 1189 carousel[0].setAttribute(NextgenAbsCarouselStaticConfig.ATTRIBUTES.SWIPE_CAPTURE, direction); 1190 } 1191}; 1192 1193// handle swipe on hero canvas carousel GAM iframe 1194updateHeroCanvasCarouselSwipe = function (direction) { 1195 let carousel = document.getElementsByTagName('webgen-hero-canvas-carousel'); 1196 if (carousel && carousel.length > 0) { 1197 carousel[0].setAttribute('data-swipe-direction-updated', direction); 1198 } 1199}; 1200 1201// handle swipe on flex banner carousel GAM iframe 1202updateFlexBannerCarouselSwipe = function (direction) { 1203 const carousel = document.querySelector('.banner-carousel abs-carousel-wc'); 1204 if (carousel) { 1205 carousel.setAttribute('data-swipe-direction-updated', direction); 1206 } 1207}; 1208// check for preference of fulfillment channel in SWY_SYND_USER_INFO cookie to set it in GAM params 1209function getFulfillmentChannel() { 1210 var syndUserInfoCookie = SWY.CONFIGSERVICE.getCookie('SWY_SYND_USER_INFO'); 1211 if (syndUserInfoCookie) { 1212 syndUserInfoCookie = JSON.parse(syndUserInfoCookie); 1213 let preference = syndUserInfoCookie.preference; 1214 if (preference) { 1215 return setFulfillmentChannel(preference); 1216 } 1217 } 1218 // Fallback to AB.userInfo.preferenceMode if cookie is not present 1219 if(AB?.userInfo?.preferenceMode) { 1220 return setFulfillmentChannel(AB.userInfo.preferenceMode); 1221 } 1222}; 1223 1224// Setting the fulfillment channel on the basis of the value of preference from the SWY_SYND_USER_INFO cookie 1225function setFulfillmentChannel(preference) { 1226 let fulfillmentChannelType = null; 1227 preference = preference.toLowerCase(); 1228 switch (preference) { 1229 case 'delivery': 1230 fulfillmentChannelType = 'DEL'; 1231 break; 1232 case 'j4u': 1233 fulfillmentChannelType = 'INS'; 1234 break; 1235 case 'dug': 1236 fulfillmentChannelType = 'PUP'; 1237 break; 1238 default: 1239 break; 1240 } 1241 return fulfillmentChannelType; 1242}; 1243 1244/** 1245 * Checks if the current device is an iOS device, ipad, ipad pro. 1246 * @returns {boolean} True if the device is running on iOS, false otherwise. 1247 */ 1248function isIOSDevice() { 1249 return (/iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.userAgent.match(/Mac/) && navigator.maxTouchPoints && navigator.maxTouchPoints > 2)) && !window.MSStream; 1250} 1251 1252/** 1253* check if the current device is Android and other non iOS mobile devices 1254*/ 1255function isMobileDevice() { 1256 // Touch capability check for hybrid devices 1257 const hasTouch = 'ontouchstart' in window || navigator?.maxTouchPoints > 0; 1258 // Device Type Check 1259 const isMobileAgent = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(navigator?.userAgent); 1260 return isIOSDevice() || isMobileAgent || (window.innerWidth <= 1024 && hasTouch && navigator?.userAgent?.includes('Mobile')); 1261} 1262 1263// getting the store ID value either from the cookie or default value from the configs 1264function getStoreId() { 1265 return getStoreIdFromCookie() || SWY?.CONFIGSERVICE?.storeResolutionConfig?.bannerDefaultStoreId; 1266}; 1267 1268// check for storeiid in SWY_SYND_USER_INFO cookie to set it in GAM params 1269function getStoreIdFromCookie() { 1270 let storeId; 1271 let syndUserInfoCookie = SWY?.CONFIGSERVICE?.getCookie('SWY_SYND_USER_INFO'); 1272 if (syndUserInfoCookie) { 1273 syndUserInfoCookie = JSON.parse(syndUserInfoCookie); 1274 storeId = syndUserInfoCookie.storeId; 1275 } 1276 return storeId 1277}; 1278 1279// function to delete GAM cookies in case a restricted term encountered. 1280function handleRestrictedPage(){ 1281 if(window.isSearchRestrictedPage === undefined){ 1282 window.isSearchRestrictedPage = true; 1283 deleteGAMCookies(true); 1284 } 1285}; 1286 1287// Google Ad Manager Tagless Integration 1288// Refined for clarity, maintainability, and production use 1289 1290// --------------------------------------------------------------------------- 1291// Constants â var (not const) so they are always hoisted and never in TDZ. 1292// Declared at the top so they are initialized before any function executes. 1293// --------------------------------------------------------------------------- 1294 1295var TAGLESS_HOST = 'https://securepubads.g.doubleclick.net/gampad/adx'; 1296 1297/** 1298 * Maps data-bannerVariation to GAM tagless custom sizes (1x1â1x6). 1299 * 1x7 is Criteo-only and handled separately in resolveTaglessSz. 1300 * 1301 * Two sources set data-bannerVariation in the DOM: 1302 * banner-unit v3 â properties.variation (PascalCase, e.g. "NewMediumBanner") 1303 * flex-module â properties.designType (lowercase, e.g. "medium") 1304 * 1305 * Tagless dialog values use a "Tagless" suffix (e.g. MediumBannerTagless, MediumBannerIngridTagless) 1306 * to avoid confusion with the legacy GPT line items (MediumBanner, MediumInGridBanner). 1307 * Both old and new keys are kept so existing authored pages continue to resolve correctly. 1308 * 1309 * Confirm size assignments with the GAM team before adding new entries â 1310 * the sz value must match an active tagless line-item in the GAM network. 1311 */ 1312var TAGLESS_SIZE_MAP = { 1313 // --- banner-unit v3 (properties.variation) --- 1314 // Tagless dialog values use a "Tagless" suffix to distinguish from legacy GPT line items. 1315 // Legacy keys are kept as fallbacks so existing authored pages resolve without re-authoring. 1316 HeroBannerTagless: '1x1', // AlbertsonsNewHomePageHeroCanvasTagless (tagless dialog value) 1317 HeroBanner: '1x1', // AlbertsonsNewHomePageHeroCanvasTagless (legacy fallback) 1318 HeroCollectionsBannerTagless: '1x2', // AlbertsonsHomePageHeroCardTagless (tagless dialog value) 1319 HeroCollectionsBanner: '1x2', // AlbertsonsHomePageHeroCardTagless (legacy fallback) 1320 MediumBannerTagless: '1x3', // AlbertsonsNewMediumBannerTagless (tagless dialog value) 1321 MediumBanner: '1x3', // AlbertsonsNewMediumBannerTagless (legacy fallback) 1322 MediumBannerIngridTagless: '1x4', // AlbertsonsNewMediumInGridBannerTagless (tagless dialog value) 1323 MediumInGridBanner: '1x4', // AlbertsonsNewMediumInGridBannerTagless (legacy fallback) 1324 UltraSkinnyBannerTagless: '1x5', // AlbertsonsNewUltraSkinnyTagless (tagless dialog value) 1325 UltraSkinnyBanner: '1x5', // AlbertsonsNewUltraSkinnyTagless (legacy fallback) 1326 SidebarBannerTagless: '1x6', // AlbertsonsSidebarBannerTagless (tagless dialog value) 1327 SidebarBanner: '1x6', // AlbertsonsSidebarBannerTagless (legacy fallback) 1328 1329 // --- flex-module (properties.designType) --- 1330 large: '1x1', 1331 default: '1x2', 1332 medium: '1x3', 1333 tile: '1x4', 1334}; 1335 1336/** 1337 * Main entry point for tagless ad slot rendering. 1338 * Determines if slot is Criteo backfill eligible and triggers the tagless flow. 1339 */ 1340function tagLessSlotGenerator(gamSectionDivId, gamContainerDivId) { 1341 const gamSectionElement = document.getElementById(gamSectionDivId); 1342 if (!gamSectionElement) { 1343 console.error(`Element with ID ${gamSectionDivId} not found`); 1344 return; 1345 } 1346 // Extract attributes for backfill eligibility 1347 const aisleId = gamSectionElement.getAttribute('data-aisleid'); 1348 const pageId = gamSectionElement.getAttribute('data-pageid'); 1349 const bannerVariation = gamSectionElement.getAttribute('data-bannervariation'); 1350 const bannerPosition = gamSectionElement.getAttribute('data-bannerposition'); 1351 // Use only the eligibility function (no hardcoded true)
1352 const isCriteoBackfill = isBackfillEligbleSlot(aisleId, pageId, bannerVariation, bannerPosition); //true || 1353 // Build tagless request URL 1354 const additionalTargeting = isCriteoBackfill ? 'criteoData%3Dtest' : null; 1355 const requestUrl = getGAMRequestUrl(gamSectionElement, additionalTargeting, isCriteoBackfill, false); 1356 if (!requestUrl) { collapseBanner(gamContainerDivId); return; } 1357 makeTaglessRequest(requestUrl, gamSectionDivId, gamContainerDivId, isCriteoBackfill, gamSectionElement, false); 1358} 1359 1360/** 1361 * Makes a tagless GAM request and handles the response. 1362 * Handles Criteo and non-Criteo flows, including retries and fallback. 1363 */ 1364function makeTaglessRequest(requestUrl, gamSectionDivId, gamContainerDivId, isCriteoBackfill, gamSectionElement, isBackfillRetry) { 1365 fetch(requestUrl, { 1366 method: 'GET', 1367 headers: { 'Accept': 'text/plain' } 1368 }) 1369 .then(function(response) { 1370 if (!response.ok) throw new Error('HTTP error! status: ' + response.status); 1371 const contentType = response.headers.get('content-type') || ''; 1372 const contentLength = response.headers.get('content-length'); 1373 if (contentLength === '0' || !/json/i.test(contentType)) return null; 1374 return response.text().then(function(text) { 1375 if (!text) return null; 1376 try { return JSON.parse(text); } catch (e) { return null; } 1377 }); 1378 }) 1379 .then(function(data) { 1380 var containerElement = document.getElementById(gamContainerDivId); 1381 var isEmptyResponse = !data || typeof data !== 'object' || Array.isArray(data) || Object.keys(data).length === 0; 1382 // Non-backfill: render or collapse 1383 if (!isCriteoBackfill) { 1384 if (containerElement && !isEmptyResponse) { 1385 renderAd(containerElement, JSON.stringify(data)); 1386 fireImpressionBeacon(containerElement, data.viewedImpression); 1387 wireClickTracking(containerElement, data.clickTracking); 1388 } else { 1389 collapseBanner(gamContainerDivId); 1390 } 1391 return; 1392 } 1393 // Criteo backfill: first call 1394 if (isCriteoBackfill && !isBackfillRetry) { 1395 if (!isEmptyResponse && data?.criteoScript1 && data?.criteoScript2) { 1396 injectCriteoScripts(containerElement, data.criteoScript1, data.criteoScript2, gamSectionDivId, gamContainerDivId, gamSectionElement); 1397 return; 1398 } 1399 if (isEmptyResponse) { 1400 handleCriteoEmptyResponse(gamSectionDivId, gamContainerDivId, gamSectionElement); 1401 return; 1402 } 1403 } 1404 // Criteo backfill: retry call 1405 if (isBackfillRetry) { 1406 if (containerElement && !isEmptyResponse) { 1407 renderAd(containerElement, JSON.stringify(data)); 1408 fireImpressionBeacon(containerElement, data.viewedImpression); 1409 wireClickTracking(containerElement, data.clickTracking); 1410 } else { 1411 collapseBanner(gamContainerDivId); 1412 } 1413 return; 1414 } 1415 }) 1416 .catch(function() { 1417 if (!isCriteoBackfill) { 1418 collapseBanner(gamContainerDivId); 1419 } else if (isCriteoBackfill && !isBackfillRetry) { 1420 handleCriteoEmptyResponse(gamSectionDivId, gamContainerDivId, gamSectionElement); 1421 } else { 1422 collapseBanner(gamContainerDivId); 1423 } 1424 }); 1425} 1426 1427/** 1428 * Handles empty response from the first Criteo tagless call. 1429 * Branches based on whether iu_parts contains 'topc'. 1430 */ 1431function handleCriteoEmptyResponse(gamSectionDivId, gamContainerDivId, gamSectionElement) { 1432 var adUnitPath = gamSectionElement.getAttribute('data-adunitpath') || ''; 1433 var bannerPosition = gamSectionElement.getAttribute('data-bannerposition') || ''; 1434 var iuParts = adUnitPath + '/' + bannerPosition; 1435 if (iuParts.indexOf('topc') !== -1) { 1436 triggerTaglessBackfillRetry(gamSectionDivId, gamContainerDivId, gamSectionElement); 1437 } else { 1438 fallbackToGPTSlotGenerator(gamSectionDivId, gamContainerDivId, gamSectionElement); 1439 } 1440} 1441 1442/** 1443 * Fallback to GPT slot generator with type=backfill targeting. 1444 * If GPT returns empty, triggers a tagless retry call. 1445 */ 1446function fallbackToGPTSlotGenerator(gamSectionDivId, gamContainerDivId, gamSectionElement) { 1447 if (typeof gamParamCall !== 'function' || typeof slotGenerator !== 'function') { 1448 collapseBanner(gamContainerDivId); 1449 return; 1450 } 1451 var gamData = gamParamCall(gamSectionDivId); 1452 if (!gamData) { 1453 triggerTaglessBackfillRetry(gamSectionDivId, gamContainerDivId, gamSectionElement); 1454 return; 1455 } 1456 gamData.detail.targeting.push({ type: 'backfill' }); 1457 googletag.cmd.push(function() { 1458 var slotResponseHandler = function(event) { 1459 if (event.slot.getSlotElementId() === gamContainerDivId) { 1460 googletag.pubads().removeEventListener('slotResponseReceived', slotResponseHandler); 1461 if (!event.slot.getResponseInformation()) { 1462 triggerTaglessBackfillRetry(gamSectionDivId, gamContainerDivId, gamSectionElement); 1463 } 1464 } 1465 }; 1466 googletag.pubads().addEventListener('slotResponseReceived', slotResponseHandler); 1467 });
1468 //gamData.detail.adUnitPath = '/22825627079/nonprod.safeway/home/*/*.*/midbx'; 1469 slotGenerator(gamData.detail, gamContainerDivId); 1470} 1471 1472/** 1473 * Makes a final tagless call with type=backfill targeting. 1474 * If this also returns empty â collapse the banner. 1475 */ 1476function triggerTaglessBackfillRetry(gamSectionDivId, gamContainerDivId, gamSectionElement) { 1477 var backfillTargeting = 'type%3Dbackfill'; 1478 var backfillUrl = getGAMRequestUrl(gamSectionElement, backfillTargeting, true, true); 1479 if (!backfillUrl) { collapseBanner(gamContainerDivId); return; } 1480 makeTaglessRequest(backfillUrl, gamSectionDivId, gamContainerDivId, true, gamSectionElement, true); 1481} 1482 1483/** 1484 * Collapses the banner by adding the 'collapse' class to the closest banner parent. 1485 */ 1486function collapseBanner(gamContainerDivId) { 1487 var containerElement = document.getElementById(gamContainerDivId); 1488 if (containerElement) { 1489 var bannerParent = containerElement.closest('.banner-unit.banner-unit-rules') 1490 || containerElement.closest('.google-adManager') 1491 || containerElement.closest('.collection'); 1492 if (bannerParent) { 1493 bannerParent.classList.add('collapse'); 1494 } 1495 } 1496} 1497 1498/** 1499 * Renders the ad and ensures the banner is visible. 1500 */ 1501function renderAd(containerElement, adData) { 1502 containerElement.innerHTML = '<div><webgen-flex-module class="" data-flex-module-content=\'' + adData + '\'></webgen-flex-module></div>'; 1503 var bannerParent = containerElement.closest('.banner-unit.banner-unit-rules') 1504 || containerElement.closest('.google-adManager') 1505 || containerElement.closest('.collection'); 1506 if (bannerParent && bannerParent.classList.contains('collapse')) { 1507 bannerParent.classList.remove('collapse'); 1508 } 1509} 1510 1511/** 1512 * Fires a GAM tracking URL via navigator.sendBeacon. 1513 * Silent no-op if sendBeacon is unavailable or url is not a valid string â 1514 * never throws so callers need no error handling. 1515 */ 1516function safeFireBeacon(url) { 1517 if (!url || typeof url !== 'string') return; 1518 if (typeof navigator === 'undefined' || typeof navigator.sendBeacon !== 'function') { 1519 console.warn('Tagless: navigator.sendBeacon unavailable, beacon not fired:', url); 1520 return; 1521 } 1522 try { 1523 navigator.sendBeacon(url, new Blob([], { type: 'text/plain' })); 1524 } catch (e) { 1525 console.warn('Tagless: sendBeacon threw unexpectedly:', e); 1526 } 1527} 1528 1529/** 1530 * Fires the GAM viewedImpression URL exactly once, when the ad container 1531 * crosses the 50% visibility threshold (IntersectionObserver). 1532 * Observer disconnects immediately after firing â no double-counting. 1533 * Silent no-op when IntersectionObserver is unavailable (older browsers). 1534 */ 1535function fireImpressionBeacon(containerElement, viewedImpressionUrl) { 1536 if (!containerElement || !viewedImpressionUrl) return; 1537 if (typeof IntersectionObserver === 'undefined') { 1538 console.warn('Tagless: IntersectionObserver unavailable, viewedImpression not fired'); 1539 return; 1540 } 1541 try { 1542 var observer = new IntersectionObserver(function(entries) { 1543 entries.forEach(function(entry) { 1544 if (entry.isIntersecting) { 1545 observer.disconnect(); 1546 safeFireBeacon(viewedImpressionUrl); 1547 } 1548 }); 1549 }, { threshold: 0.5 }); 1550 observer.observe(containerElement); 1551 } catch (e) { 1552 console.warn('Tagless: fireImpressionBeacon failed:', e); 1553 } 1554} 1555 1556/** 1557 * Fires the GAM clickTracking URL on the first click on the ad container. 1558 * { once: true } ensures the listener self-removes after firing â no duplicate 1559 * beacons even if the user clicks multiple times before navigating away. 1560 */ 1561function wireClickTracking(containerElement, clickTrackingUrl) { 1562 if (!containerElement || !clickTrackingUrl) return; 1563 try { 1564 containerElement.addEventListener('click', function() { 1565 safeFireBeacon(clickTrackingUrl); 1566 }, { once: true }); 1567 } catch (e) { 1568 console.warn('Tagless: wireClickTracking failed:', e); 1569 } 1570} 1571 1572/** 1573 * Checks if GamCriteoSlot is present and non-empty. 1574 */ 1575function isGamCriteoSlotNonEmpty(containerElement) { 1576 var slot = containerElement.querySelector('#GamCriteoSlot'); 1577 if (!slot) return false; 1578 if (slot.childNodes.length > 0) return true; 1579 if (slot.innerHTML && slot.innerHTML.replace(/\s+/g, '').length > 0) return true; 1580 return false;
1581} 1582 1583/** 1584 * Injects Criteo scripts and observes for successful render. 1585 */ 1586function injectCriteoScripts(containerElement, criteoScript1Src, criteoScript2Src, gamSectionDivId, gamContainerDivId, gamSectionElement) { 1587 var CRITEO_RENDER_TIMEOUT = 3000; 1588 var criteoResolved = false; 1589 function onCriteoResult(success, reason) { 1590 if (criteoResolved) return; 1591 criteoResolved = true; 1592 if (observer) observer.disconnect(); 1593 if (timeoutId) clearTimeout(timeoutId); 1594 if (success) { 1595 // Successfully rendered, do nothing 1596 return; 1597 } else { 1598 // Only clear and retry if scripts failed or slot is empty after timeout 1599 containerElement.innerHTML = ''; 1600 handleCriteoEmptyResponse(gamSectionDivId, gamContainerDivId, gamSectionElement); 1601 } 1602 } 1603 var observer = new MutationObserver(function() { 1604 var slot = containerElement.querySelector('#GamCriteoSlot'); 1605 if (slot) { 1606 if (isGamCriteoSlotNonEmpty(containerElement)) { 1607 onCriteoResult(true); 1608 } 1609 } 1610 }); 1611 observer.observe(containerElement, { childList: true, subtree: true }); 1612 var timeoutId = setTimeout(function() { 1613 var slot = containerElement.querySelector('#GamCriteoSlot'); 1614 if (slot && isGamCriteoSlotNonEmpty(containerElement)) { 1615 onCriteoResult(true); 1616 } else { 1617 onCriteoResult(false, 'GamCriteoSlot is missing or empty after timeout'); 1618 } 1619 }, CRITEO_RENDER_TIMEOUT); 1620 containerElement.innerHTML = ''; 1621 var script1 = document.createElement('script'); 1622 script1.async = true; 1623 script1.src = criteoScript1Src; 1624 script1.onerror = function() { onCriteoResult(false, 'criteoScript1 failed to load'); }; 1625 containerElement.appendChild(script1); 1626 var script2 = document.createElement('script'); 1627 script2.async = true; 1628 script2.src = criteoScript2Src + 1629 '&retailer-visitor-id=' + (getAdobeVisitorId() || '') + 1630 '&page-id=' + (getCriteoPageId() || '') + 1631 '&placement-id=Banner' + 1632 '&context=' + (getCriteoContext() || '1') + 1633 '®ionId=' + (getRegionId() || '') + 1634 '&gdpr=2' + 1635 '&cb=' + Date.now(); 1636 script2.onerror = function() { onCriteoResult(false, 'criteoScript2 failed to load'); }; 1637 containerElement.appendChild(script2); 1638} 1639 1640/** 1641 * Returns the Criteo page-id based on the current page type and viewport. 1642 */ 1643function getCriteoPageId() { 1644 var view = window.innerWidth > 1023 ? 'desktop' : 'mobile'; 1645 var isCategoryPage = window.location.href.indexOf('aisle') > -1; 1646 var isSearchPage = window.location.pathname.toLowerCase().indexOf('shop/search-results') !== -1; 1647 var isSeoUrlPdp = /\/shop\/[^/]+\/\d+$/.test(window.location.pathname); 1648 var isProductDetailsPage = window.location.pathname.split('/').pop().includes('product-details.') 1649 || window.location.pathname.includes('product/details/') 1650 || window.location.pathname.includes('/shop/pd/') 1651 || isSeoUrlPdp; 1652 if (isCategoryPage) return 'viewCategory_GAM_' + view; 1653 if (isSearchPage) return 'viewSearchResult_GAM_' + view; 1654 if (isProductDetailsPage) return 'viewProductDetails_GAM_' + view; 1655 return 'viewCategory_GAM_desktop'; 1656} 1657 1658/** 1659 * Returns the Criteo context value based on the current page type. 1660 */ 1661function getCriteoContext() { 1662 var isCategoryPage = window.location.href.indexOf('aisle') > -1; 1663 var isSeoUrlPdp = /\/shop\/[^/]+\/\d+$/.test(window.location.pathname); 1664 var isProductDetailsPage = window.location.pathname.split('/').pop().includes('product-details.') 1665 || window.location.pathname.includes('product/details/') 1666 || window.location.pathname.includes('/shop/pd/') 1667 || isSeoUrlPdp; 1668 if (isCategoryPage) { 1669 var catId = document.querySelector('[data-aisle-id]')?.getAttribute('data-aisle-id'); 1670 return catId ? transformCatId(catId) : ''; 1671 } else if (isProductDetailsPage) { 1672 var bpn = window.location.pathname.split('/').pop(); 1673 if (window.location.pathname.includes('product-details.')) { 1674 return bpn.substring(bpn.indexOf('.') + 1, bpn.lastIndexOf('.')); 1675 } 1676 // SEO URL: /shop/<slug>/<bpn> â last segment is the bpn 1677 return bpn; 1678 } 1679 return getQueryParameter('q') || ''; 1680} 1681 1682// --------------------------------------------------------------------------- 1683// Helpers 1684// --------------------------------------------------------------------------- 1685 1686/** 1687 * Parses data-gamNetworkCode (format: "prod:XXXXX,nonprod:YYYYY") and returns 1688 * the resolved network code and environment URL prefix for the current env. 1689 * Returns null when the attribute is absent or the env entry is not found. 1690 */ 1691function resolveNetworkCode(element) { 1692 const attr = element.getAttribute('data-gamNetworkCode') || ''; 1693 const currEnv = (SWY.CONFIGSERVICE.env === 'prod' || SWY.CONFIGSERVICE.env === 'beta') 1694 ? 'prod' : 'nonprod'; 1695 const envPrefix = currEnv === 'prod' ? '' : 'nonprod.'; 1696 const entries = attr.split(','); 1697 for (let i = 0; i < entries.length; i++) { 1698 const entry = entries[i].trim(); 1699 if (entry.startsWith(currEnv + ':')) { 1700 return { networkCode: entry.substring(currEnv.length + 1), envPrefix: envPrefix }; 1701 } 1702 } 1703 console.warn('Tagless: could not resolve network code from data-gamNetworkCode:', attr); 1704 return null; 1705} 1706 1707/** 1708 * Resolves the tagless custom sz parameter for a slot. 1709 * Criteo first-call â 1x7. All other cases (non-Criteo or backfill retry) â 1710 * variation-mapped size from TAGLESS_SIZE_MAP. 1711 * Returns null for unknown banner variations so the caller can collapse. 1712 */ 1713function resolveTaglessSz(element, isCriteoBackfill, isBackfillRetry) { 1714 if (isCriteoBackfill && !isBackfillRetry) return '1x7'; 1715 const variation = element.getAttribute('data-bannervariation'); 1716 const sz = TAGLESS_SIZE_MAP[variation] || null; 1717 if (!sz) { 1718 console.warn('Tagless: unknown bannerVariation "' + variation + '", cannot resolve sz'); 1719 } 1720 return sz; 1721} 1722 1723// --------------------------------------------------------------------------- 1724// URL builder 1725// --------------------------------------------------------------------------- 1726 1727/**
1728 * Builds the tagless GAM request URL. 1729 * Returns null when required parameters (network code or sz) cannot be resolved; 1730 * callers must collapse the banner slot in that case. 1731 */ 1732function getGAMRequestUrl(gamSectionElement, targetingParam, isCriteoBackfill, isBackfillRetry) { 1733 if (!window['correlator-id']) window['correlator-id'] = Date.now(); 1734 1735 const net = resolveNetworkCode(gamSectionElement); 1736 if (!net) return null; 1737 1738 const sz = resolveTaglessSz(gamSectionElement, isCriteoBackfill, isBackfillRetry); 1739 if (!sz) return null; 1740 1741 var adUnitPath = gamSectionElement.getAttribute('data-adunitpath'); 1742 const bannerPosition = gamSectionElement.getAttribute('data-bannerposition'); 1743 var isTaglessPdpPage = window.location.pathname.includes('product-details.') 1744 || window.location.pathname.includes('product/details/') 1745 || window.location.pathname.includes('/shop/pd/'); 1746 if (isTaglessPdpPage && typeof getUpdatedPdpAdUnitPath === 'function') { 1747 var aisleL4El = document.querySelector('[data-l4-id]'); 1748 adUnitPath = getUpdatedPdpAdUnitPath(adUnitPath, aisleL4El); 1749 } 1750 const iu = '/' + net.networkCode + '/' + net.envPrefix + adUnitPath + '/' + bannerPosition; 1751 1752 const params = [ 1753 'c=' + window['correlator-id'], 1754 'iu=' + iu, 1755 'sz=' + sz, 1756 'url=' + encodeURIComponent(window.location.href), 1757 'd_imp_hdr=1', 1758 'd_imp=1', 1759 'tile=1', 1760 'm=text/json', 1761 ]; 1762 if (targetingParam) params.push('t=' + targetingParam); 1763 1764 return TAGLESS_HOST + '?' + params.join('&'); 1765}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.