PageSourceSearch

https://www.carrsqc.com/etc.clientlibs/wcax-core/clientlibs/clientlib-gam-header.min.js

js carrsqc.com collected 2026-09-24 11:17:53 UTC 79,874 bytes, 1,765 lines download raw bytes

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    '&regionId=' + (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.