PageSourceSearch

https://www.cafepress.co.uk/scripts/search-tracking.js?30ee96d925107cd1f2b86141b0ad71ffab79167c

js cafepress.co.uk collected 2026-09-24 23:46:39 UTC 29,539 bytes, 831 lines download raw bytes

1(function () {
2    const SCRIPT_LOAD_TS = Date.now();
3    const endpoint = webUrl('/rest/web/search_tracking');
4    const SS_KEY = 'pa_search:guids';
5    const SS_RESULTS_KEY = 'pa_search:results_pushed';
6    const SS_SEEN_KEY = 'pa_search:seen';
7    const SS_LAST_CLICK_KEY = 'pa_search:last_click';
8    // CW (CanvasWorld) mobile renders thumbs directly into .shutterstock_list
9    // (no #category_thumb_wrapper), so include it to keep impressions working there.
10    const searchResultWrapperSelector = '#category_thumb_wrapper, #filter_design_list, .shutterstock_list';
11    // Covers all brand variants:
12    //   - PA/STI headers:        form.site-search[name="siteSearch"]
13    //   - PA/STI search pages:   form.site-search[name="searchPage"], form.search-page[name="searchPage"]
14    //   - MicroSite (PCRUS etc.): form[name="menu_search"]  (customer/shared/templates/components/common/search.php)
15    //   - CW (CanvasWorld):      form.search_form with input[name="search_keywords"]
16    //                            (customer/cw/templates/parts/search_bar.php)
17    const searchFormSelector = 'form.site-search, form.search-page, form[name="siteSearch"], form[name="searchPage"], form[name="menu_search"], form.search_form';
18    const thumbSelector = '.category_thumb[data-design-id], .design-item-wrapper[data-id]';
19    const CPC_SITE_IDS = [170, 171, 172, 173];
20
21    const INIT_TS = Math.floor(Date.now() / 1000);
22
23    function nowTs() {
24        return Math.floor(Date.now() / 1000);
25    }
26
27    function uuid() {
28        if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
29            return crypto.randomUUID();
30        }
31        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
32            const r = (Math.random() * 16) | 0;
33            const v = c === 'x' ? r : (r & 0x3) | 0x8;
34            return v.toString(16);
35        });
36    }
37
38    function getQueryParam(name) {
39        try {
40            return new URLSearchParams(location.search).get(name) || '';
41        } catch (e) {
42            return '';
43        }
44    }
45
46    function isCpcSite() {
47        if (typeof SITE_ID === 'undefined') {
48            return false;
49        }
50        if (typeof SITE_CAFUS !== 'undefined') {
51            return SITE_ID === SITE_CAFUS || SITE_ID === SITE_CAFCA || SITE_ID === SITE_CAFAU || SITE_ID === SITE_CAFUK;
52        }
53        return CPC_SITE_IDS.indexOf(SITE_ID) !== -1;
54    }
55
56    // PA/STI thumbs expose design id via data-design-id; CPB (B2BDesigns) uses data-id.
57    function getThumbDesignId(node) {
58        return node.getAttribute('data-design-id') || node.getAttribute('data-id') || null;
59    }
60
61    // Mirrors PHP ThirdPartyImageController::getSearchKeywordsFromUri:
62    // decode the path segment, replace '-' with spaces, collapse whitespace, lowercase.
63    function normalizeCwTerm(raw) {
64        let term = raw;
65        try {
66            term = decodeURIComponent(term);
67        } catch (e) {
68        }
69        term = term.replace(/-/g, ' ');
70        term = term.replace(/\s+/g, ' ').trim();
71        return term.toLowerCase();
72    }
73
74    function getCwSearchTermFromPath() {
75        let m;
76        try {
77            m = location.pathname.match(/^\/search\/([^/]+)\/?/);
78        } catch (e) {
79            return '';
80        }
81        if (!m) {
82            return '';
83        }
84        return normalizeCwTerm(m[1]);
85    }
86
87    // CPC search-result PLP: /+term-slug (mirrors CpRoutingService::isSearchUri term extraction).
88    function getCpSearchTermFromPath() {
89        let m;
90        try {
91            m = location.pathname.match(/^\/(\+[\+\-\w]{1,100})$/);
92        } catch (e) {
93            return '';
94        }
95        if (!m) {
96            return '';
97        }
98        let raw = m[1];
99        try {
100            raw = decodeURIComponent(raw);
101        } catch (e) {
102        }
103        const parts = raw.replace(/^\+/, '').split('+');
104        let term = parts[0] || '';
105        if (term.endsWith(' gifts')) {
106            term = term.slice(0, -6);
107        }
108        term = term.replace(/-/g, ' ').replace(/\s+/g, ' ').trim();
109        return term;
110    }
111
112    function getSearchTermFromMenuForm() {
113        const input = document.querySelector('form[name="menu_search"] input[name="q"]');
114        if (input && input.value) {
115            return input.value.trim();
116        }
117        return '';
118    }
119
120    function getSearchTermFromUrl() {
121        const q = getQueryParam('q');
122        if (q) {
123            return q;
124        }
125        const menuTerm = getSearchTermFromMenuForm();
126        if (menuTerm) {
127            return menuTerm;
128        }
129        if (getQueryParam('from_search') === '1') {
130            const cpTerm = getCpSearchTermFromPath();
131            if (cpTerm) {
132                return cpTerm;
133            }
134        }
135        return getCwSearchTermFromPath();
136    }
137
138    function getSearchResultsPageNumber() {
139        const pageParam = getQueryParam('page');
140        if (pageParam) {
141            const p = parseInt(pageParam, 10);
142            if (!isNaN(p) && p > 0) {
143                return p;
144            }
145        }
146        try {
147            if (window.QUERY_PARAMS && window.QUERY_PARAMS.page) {
148                const p = parseInt(window.QUERY_PARAMS.page, 10);
149                if (!isNaN(p) && p > 0) {
150                    return p;
151                }
152            }
153        } catch (e) {
154        }
155        return 1;
156    }
157
158    function isSearchPage() {
159        if (getQueryParam('from_search') === '1' && getSearchTermFromUrl() !== '') {
160            return true;
161        }
162        // CW (CanvasWorld): /search/{keyword}/  (optional ?nr=t)
163        return getCwSearchTermFromPath() !== '';
164    }
165
166    function readGuidMap() {
167        return readMap(SS_KEY);
168    }
169
170    function writeGuidMap(map) {
171        writeMap(SS_KEY, map);
172    }
173
174    function getGuidForTerm(term) {
175        const map = readGuidMap();
176        return map[term] ? map[term].guid : null;
177    }
178
179    function setGuidForTerm(term, guid) {
180        const map = readGuidMap();
181        map[term] = {guid: guid, ts: nowTs()};
182        writeGuidMap(map);
183    }
184
185    function readMap(key) {
186        try {
187            const raw = sessionStorage.getItem(key);
188            return raw ? JSON.parse(raw) : {};
189        } catch (e) {
190            return {};
191        }
192    }
193
194    function writeMap(key, map) {
195        try {
196            sessionStorage.setItem(key, JSON.stringify(map));
197        } catch (e) {
198        }
199    }
200
201    function isResultsPushed(guid) {
202        return Boolean(readMap(SS_RESULTS_KEY)[guid]);
203    }
204
205    function markResultsPushed(guid) {
206        const map = readMap(SS_RESULTS_KEY);
207        map[guid] = true;
208        writeMap(SS_RESULTS_KEY, map);
209    }
210
211    function isSeen(guid, designId) {
212        const map = readMap(SS_SEEN_KEY);
213        return Boolean(map[guid] && map[guid][designId]);
214    }
215
216    function markSeen(guid, designId) {
217        const map = readMap(SS_SEEN_KEY);
218        if (!map[guid]) {
219            map[guid] = {};
220        }
221        map[guid][designId] = true;
222        writeMap(SS_SEEN_KEY, map);
223    }
224
225    function writeLastClick(guid, designId, searchTerm, clickGuid) {
226        try {
227            const payload = {
228                guid: guid,
229                design_id: String(designId),
230                search_term: searchTerm,
231                ts: nowTs()
232            };
233            if (clickGuid) {
234                payload.click_guid = clickGuid;
235            }
236            sessionStorage.setItem(SS_LAST_CLICK_KEY, JSON.stringify(payload));
237        } catch (e) {
238        }
239    }
240
241    function readLastClick() {
242        try {
243            const raw = sessionStorage.getItem(SS_LAST_CLICK_KEY);
244            return raw ? JSON.parse(raw) : null;
245        } catch (e) {
246            return null;
247        }
248    }
249
250    // Clear any per-design tracking recorded under a previous search interaction
251    // (impressions via SS_SEEN_KEY, results-set push via SS_RESULTS_KEY) so a new
252    // search guid starts from a clean slate.
253    function clearGuidTracking(guid) {
254        if (!guid) {
255            return;
256        }
257        const seen = readMap(SS_SEEN_KEY);
258        if (seen[guid]) {
259            delete seen[guid];
260            writeMap(SS_SEEN_KEY, seen);
261        }
262        const results = readMap(SS_RESULTS_KEY);
263        if (results[guid]) {
264            delete results[guid];
265            writeMap(SS_RESULTS_KEY, results);
266        }
267    }
268
269    // --- batched sendBeacon sender ---
270    const queue = [];
271    let flushScheduled = false;
272
273    function flush() {
274        if (!queue.length) {
275            return;
276        }
277        const payload = JSON.stringify(queue.splice(0, queue.length));
278        try {
279            if (navigator.sendBeacon) {
280                navigator.sendBeacon(endpoint, payload);
281                return;
282            }
283        } catch (e) {
284        }
285        try {
286            fetch(endpoint, {
287                method: 'POST',
288                body: payload,
289                keepalive: true,
290                headers: {'Content-Type': 'application/json'}
291            }).catch(function () {});
292        } catch (e) {
293        }
294    }
295
296    function scheduleFlush() {
297        if (flushScheduled) {
298            return;
299        }
300        flushScheduled = true;
301        Promise.resolve().then(function () {
302            flushScheduled = false;
303            flush();
304        });
305    }
306
307    window.addEventListener('pagehide', flush);
308
309    // --- the real _pa_search object ---
310    const api = {
311        push: function (event, data, timestamp) {
312            const ts = timestamp != null ? timestamp : nowTs();
313            queue.push({event: event, timestamp: ts, data: data});
314            scheduleFlush();
315        }
316    };
317
318    // --- replay pre-registered queue ---
319    const queued = window._pa_search;
320    window._pa_search = api;
321    if (Array.isArray(queued)) {
322        for (let i = 0; i < queued.length; i++) {
323            const item = queued[i];
324            if (Array.isArray(item) && item.length >= 1) {
325                api.push(item[0], item[1], item[2] != null ? item[2] : INIT_TS);
326            }
327        }
328    }
329
330    // --- Feature 1: search event ---
331    // Dedup window: the click fallback and the submit handler both fire on a
332    // normal submit-button click (click precedes submit). Treat repeated calls
333    // for the same term within this window as one search so we don't push two
334    // `search` events with two different GUIDs for a single interaction.
335    const SEARCH_DEDUP_WINDOW_SEC = 2;
336
337    function recordSearch(term) {
338        const map = readGuidMap();
339        const existing = map[term];
340        const now = nowTs();
341        if (existing && existing.guid && (now - existing.ts) <= SEARCH_DEDUP_WINDOW_SEC) {
342            // Reuse the GUID just created for this term; don't push again.
343            return existing.guid;
344        }
345        const previousGuid = existing ? existing.guid : null;
346        const guid = uuid();
347        map[term] = {guid: guid, ts: now};
348        writeGuidMap(map);
349        if (previousGuid && previousGuid !== guid) {
350            clearGuidTracking(previousGuid);
351        }
352        api.push('search', {
353            guid: guid,
354            search_term: term,
355            user_agent: navigator.userAgent
356        });
357        return guid;
358    }
359
360    function getSearchTermFromForm(form) {
361        const q = form.querySelector('input[name="q"]');
362        if (q && q.value) {
363            return q.value.trim();
364        }
365        // CW uses input[name="search_keywords"]; normalize to the same form the
366        // results page will expose in the URL (see getSearchTermFromUrl), so the
367        // GUID recorded on submit matches the one looked up on the results page.
368        const cw = form.querySelector('input[name="search_keywords"]');
369        if (cw && cw.value) {
370            return normalizeCwTerm(cw.value);
371        }
372        return '';
373    }
374
375    document.addEventListener('submit', function (e) {
376        const form = e.target;
377        if (!form || !form.matches || !form.matches(searchFormSelector)) {
378            return;
379        }
380        const term = getSearchTermFromForm(form);
381        if (!term) {
382            return;
383        }
384        recordSearch(term);
385    }, true);
386
387    // Fallback: generate the GUID on submit-button click too, in case submit is prevented
388    // or the form navigates before the submit handler runs.
389    document.addEventListener('click', function (e) {
390        const form = e.target.closest && e.target.closest(searchFormSelector);
391        if (!form) {
392            return;
393        }
394        if (e.target.matches && (e.target.matches('input[type="submit"]') || e.target.matches('button[type="submit"]'))) {
395            const term = getSearchTermFromForm(form);
396            if (!term) {
397                return;
398            }
399            recordSearch(term);
400        }
401    }, true);
402
403    // Enter in the search input: MicroSite (PCRUS etc.) calls preventDefault() on
404    // Enter keydown and re-triggers submit via jQuery `.trigger('submit')`, which
405    // does not reach our native addEventListener submit handler. Catch Enter at
406    // capture phase (before that preventDefault) so the search event still fires.
407    // On brands where native submit also fires, the dedup window in recordSearch
408    // collapses the two calls into one event.
409    document.addEventListener('keydown', function (e) {
410        if ((e.key !== 'Enter' && e.keyCode !== 13) || !e.target) {
411            return;
412        }
413        const form = e.target.closest && e.target.closest(searchFormSelector);
414        if (!form) {
415            return;
416        }
417        const term = getSearchTermFromForm(form);
418        if (!term) {
419            return;
420        }
421        recordSearch(term);
422    }, true);
423
424    // --- Feature 2: search results (only on search page) ---
425    if (isSearchPage()) {
426        const term = getSearchTermFromUrl();
427        let guid = getGuidForTerm(term);
428        if (!guid) {
429            // No matching GUID for this term (e.g. direct link to results, or
430            // term mismatch). Create a new one and push a 'search' event for it.
431            guid = uuid();
432            setGuidForTerm(term, guid);
433            api.push('search', {
434                guid: guid,
435                search_term: term,
436                user_agent: navigator.userAgent
437            });
438        }
439
440        // search_results = the searched result set, sourced from qFilterObj.items.
441        // Pushed at most once per guid per tab (deduped across back/forward via
442        // sessionStorage). qFilterObj is built on document ready, so this may be
443        // retried until items are available.
444        function pushSearchResultsOnce() {
445            if (isResultsPushed(guid)) {
446                return;
447            }
448            let designIds = null;
449            let pageSize = 0;
450
451            const qf = window.qFilterObj || window.qfDataObj;
452            if (qf && qf.items && qf.items.length) {
453                // PA/STI/MicroSite use window.qFilterObj; CW (CanvasWorld) uses
454                // window.qfDataObj. Both are QuickFilter instances with the same
455                // items / fullData.structure_indexes / itemsPerPage shape.
456                let idx = 0;
457                try {
458                    idx = (qf.fullData && qf.fullData.structure_indexes && qf.fullData.structure_indexes.ID) || 0;
459                } catch (e) {
460                }
461                designIds = [];
462                for (let i = 0; i < qf.items.length; i++) {
463                    const id = qf.items[i] ? qf.items[i][idx] : null;
464                    if (id) {
465                        designIds.push(id);
466                    }
467                }
468                // page_size = configured items per page (default 36 / mobile 18 / `ipp` override),
469                // NOT pageCounting which is the count of items rendered on the current page.
470                pageSize = Number(qf.itemsPerPage) || 0;
471            } else if (window.SimpleFilterFullData && Array.isArray(window.SimpleFilterFullData.items) && window.SimpleFilterFullData.items.length) {
472                // CPB (cpbus) search page: designs come from SimpleFilterFullData (server returns the
473                // full set; B2BDesigns paginates client-side). page_size is the configured per-page
474                // size from the B2BDesigns instance (parseInt(ipp) || 24).
475                designIds = window.SimpleFilterFullData.items.map(function (v) {
476                    return v && v.id;
477                }).filter(Boolean);
478                try {
479                    pageSize = Number(window.b2bDesigns && window.b2bDesigns.pagination && window.b2bDesigns.pagination.page_size) || 0;
480                } catch (e) {
481                }
482            } else if (isCpcSite() && window.PRODUCT_ITEMS) {
483                // CPC (CAFUS/CAFCA/CAFAU/CAFUK): server-rendered first page only for search_results.
484                if (getSearchResultsPageNumber() > 1) {
485                    return;
486                }
487                let items = window.PRODUCT_ITEMS;
488                if (!Array.isArray(items)) {
489                    items = Object.values(items);
490                }
491                designIds = [];
492                for (let i = 0; i < items.length; i++) {
493                    const item = items[i];
494                    const id = item && (item.design_id != null ? item.design_id : item.id);
495                    if (id) {
496                        designIds.push(id);
497                    }
498                }
499                try {
500                    pageSize = Number(window.QUERY_PARAMS && window.QUERY_PARAMS.n) || 60;
501                } catch (e) {
502                    pageSize = 60;
503                }
504            }
505
506            if (!designIds || !designIds.length) {
507                return;
508            }
509            markResultsPushed(guid);
510            api.push('search_results', {
511                guid: guid,
512                search_term: term,
513                design_ids: designIds,
514                page_size: pageSize
515            });
516        }
517
518        // impressions (design_view) — handled only by the IntersectionObserver.
519        // Deduped per (guid, design) via sessionStorage so back/forward does not
520        // re-count the same design under the same guid. All newly-seen designs in
521        // one observer tick are pushed together as a single event with an array.
522        //
523        // PA/STI QuickFilter stores each card's thumb block inside a
524        // <script type="text/html"> template that its own IO expands on demand.
525        // Until hydration the card only contains the description (~65px) instead
526        // of its final ~49vw+description height, so many short cards temporarily
527        // fit the first viewport and inflate design_view impressions.  To prevent
528        // this, a .category_thumb is only observed once its .thumb_wrapper is in
529        // the live DOM.  Other card types (e.g. CPB .design-item-wrapper) don't
530        // use this lazy pattern and are observed immediately.
531        function observeCardIfReady(card) {
532            if (!card || card._paSearchObserved) {
533                return false;
534            }
535            if (card.matches('.category_thumb') && !card.querySelector('.thumb_wrapper')) {
536                return false;
537            }
538            card._paSearchObserved = true;
539            io.observe(card);
540            return true;
541        }
542
543        const io = new IntersectionObserver(function (entries) {
544            const newIds = [];
545            entries.forEach(function (entry) {
546                if (!entry.isIntersecting) {
547                    return;
548                }
549                const id = getThumbDesignId(entry.target);
550                if (id && !isSeen(guid, id)) {
551                    markSeen(guid, id);
552                    newIds.push(id);
553                }
554            });
555            if (newIds.length) {
556                api.push('design_view', {guid: guid, search_term: term, design_ids: newIds});
557            }
558        }, {threshold: 0.5});
559
560        function observeThumbs(root) {
561            (root || document).querySelectorAll(thumbSelector).forEach(function (node) {
562                observeCardIfReady(node);
563            });
564        }
565
566        function watchWrapper(w) {
567            observeThumbs(w);
568            pushSearchResultsOnce();
569
570            const mo = new MutationObserver(function (mutations) {
571                let addedAny = false;
572                mutations.forEach(function (m) {
573                    m.addedNodes.forEach(function (node) {
574                        if (node.nodeType !== 1) {
575                            return;
576                        }
577                        // Collect candidate cards from this addition: both
578                        // new .category_thumb nodes and existing cards whose
579                        // .thumb_wrapper was just injected by lazy hydration.
580                        const candidates = [];
581                        if (node.matches && node.matches(thumbSelector)) {
582                            candidates.push(node);
583                        }
584                        if (node.matches && node.matches('.thumb_wrapper')) {
585                            const card = node.closest(thumbSelector);
586                            if (card) candidates.push(card);
587                        }
588                        if (node.querySelectorAll) {
589                            node.querySelectorAll(thumbSelector).forEach(function (t) {
590                                candidates.push(t);
591                            });
592                            node.querySelectorAll('.thumb_wrapper').forEach(function (wrapper) {
593                                const card = wrapper.closest(thumbSelector);
594                                if (card) candidates.push(card);
595                            });
596                        }
597                        candidates.forEach(function (card) {
598                            observeCardIfReady(card);
599                            addedAny = true;
600                        });
601                    });
602                });
603                if (addedAny) {
604                    pushSearchResultsOnce();
605                }
606            });
607            mo.observe(w, {childList: true, subtree: true});
608        }
609
610        const wrapper = document.querySelector(searchResultWrapperSelector);
611        if (wrapper) {
612            watchWrapper(wrapper);
613        } else {
614            // wrapper not present yet; watch the whole document until it appears
615            const bodyMo = new MutationObserver(function () {
616                const w = document.querySelector(searchResultWrapperSelector);
617                if (w) {
618                    bodyMo.disconnect();
619                    watchWrapper(w);
620                }
621            });
622            bodyMo.observe(document.documentElement, {childList: true, subtree: true});
623        }
624
625        // On mobile, jQuery Mobile turns a touch into a `tap` event and some
626        // QuickFilter templates re-trigger a synthetic `click` from `tap`
627        // (e.g. `return $(this).trigger('click')`). Both the native click and
628        // that synthetic click reach this capture-phase listener and would
629        // otherwise emit two identical `design_click` beacons. Dedup by
630        // (guid, designId) within this window; the window is short enough that
631        // a deliberate second click on the same card still records.
632        const CLICK_DEDUP_WINDOW_SEC = 1;
633        const lastClickByKey = new Map();
634
635        function shouldRecordClick(guid, designId) {
636            const key = guid + ':' + designId;
637            const now = nowTs();
638            const last = lastClickByKey.get(key);
639            if (last != null && (now - last) <= CLICK_DEDUP_WINDOW_SEC) {
640                return false;
641            }
642            lastClickByKey.set(key, now);
643            return true;
644        }
645
646        document.addEventListener('click', function (e) {
647            const card = e.target.closest && e.target.closest(thumbSelector);
648            if (!card) {
649                return;
650            }
651            const id = getThumbDesignId(card);
652            if (id && shouldRecordClick(guid, id)) {
653                const clickGuid = uuid();
654                writeLastClick(guid, id, term, clickGuid);
655                api.push('design_click', {
656                    guid: guid,
657                    search_term: term,
658                    design_ids: [id],
659                    click_guid: clickGuid
660                });
661            }
662        }, true);
663    }
664
665    // --- Feature 3: PDP funnel events (gated by last design_click) ---
666    function getPdpDesignId() {
667        if (typeof window.pa_product_id !== 'undefined' && window.pa_product_id !== null && window.pa_product_id !== '') {
668            return String(window.pa_product_id);
669        }
670        try {
671            if (window.product_options && window.product_options.design_id) {
672                return String(window.product_options.design_id);
673            }
674        } catch (e) {
675        }
676        return null;
677    }
678
679    function isPdpPage() {
680        if (document.body && (
681            document.body.classList.contains('details_page') ||
682            document.body.classList.contains('b2c_detail_page')
683        )) {
684            return true;
685        }
686        try {
687            if (/\/design-\d+/.test(location.pathname) || /-p-\d+/.test(location.pathname)) {
688                return true;
689            }
690        } catch (e) {
691        }
692        return getPdpDesignId() != null;
693    }
694
695    function isDesignerPage() {
696        if (document.body && (
697            document.body.classList.contains('designer_page') ||
698            document.body.classList.contains('designer_preview_page')
699        )) {
700            return true;
701        }
702        try {
703            const path = location.pathname;
704            if (path.indexOf('/products/designer') !== -1) {
705                return true;
706            }
707            if (path.indexOf('/preview-tool/step-2') !== -1 || path.indexOf('/preview-tool/step-1') !== -1) {
708                return true;
709            }
710        } catch (e) {
711        }
712        return false;
713    }
714
715    function getDwellMs() {
716        return Date.now() - SCRIPT_LOAD_TS;
717    }
718
719    function buildPdpEventData(context, extra) {
720        const data = {
721            guid: context.guid,
722            search_term: context.search_term,
723            design_id: context.design_id
724        };
725        if (context.click_guid) {
726            data.click_guid = context.click_guid;
727        }
728        if (extra) {
729            for (const key in extra) {
730                if (Object.prototype.hasOwnProperty.call(extra, key)) {
731                    data[key] = extra[key];
732                }
733            }
734        }
735        return data;
736    }
737
738    function initPdpTracking() {
739        const lastClick = readLastClick();
740        if (!lastClick || !lastClick.design_id) {
741            return;
742        }
743
744        let pdpInitialized = false;
745
746        function tryInitPdp() {
747            const designId = getPdpDesignId();
748            if (!designId) {
749                return false;
750            }
751            if (designId !== String(lastClick.design_id)) {
752                return true;
753            }
754            if (pdpInitialized) {
755                return true;
756            }
757            pdpInitialized = true;
758
759            const context = {
760                guid: lastClick.guid,
761                search_term: lastClick.search_term,
762                design_id: designId,
763                click_guid: lastClick.click_guid || null
764            };
765
766            let exitPushed = false;
767
768            function pushExitPdp() {
769                if (exitPushed) {
770                    return;
771                }
772                exitPushed = true;
773                api.push('exit_pdp', buildPdpEventData(context, {dwell: getDwellMs()}));
774                flush();
775            }
776
777            function pushHidePdp() {
778                api.push('hide_pdp', buildPdpEventData(context, {dwell: getDwellMs()}));
779                flush();
780            }
781
782            if (!isDesignerPage()) {
783                window.addEventListener('pagehide', pushExitPdp);
784                document.addEventListener('visibilitychange', function () {
785                    if (document.visibilityState === 'hidden') {
786                        pushHidePdp();
787                    }
788                });
789            }
790
791            document.addEventListener('click', function (e) {
792                const btn = e.target.closest && e.target.closest('[data-role-search-tracking]');
793                if (!btn || btn.disabled || btn.classList.contains('out-of-stock')
794                    || btn.classList.contains('disabled') || btn.classList.contains('disable')) {
795                    return;
796                }
797                const role = btn.getAttribute('data-role-search-tracking');
798                if (role === 'click_personalize' || role === 'add_to_cart') {
799                    api.push(role, buildPdpEventData(context));
800                }
801            }, true);
802
803            return true;
804        }
805
806        if (tryInitPdp()) {
807            return;
808        }
809
810        let attempts = 0;
811        const maxAttempts = 50;
812
813        function pollPdp() {
814            attempts++;
815            if (tryInitPdp() || attempts >= maxAttempts) {
816                return;
817            }
818            setTimeout(pollPdp, 100);
819        }
820
821        if (document.readyState === 'loading') {
822            document.addEventListener('DOMContentLoaded', pollPdp);
823        } else {
824            pollPdp();
825        }
826    }
827
828    if (isPdpPage()) {
829        initPdpTracking();
830    }
831})();

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.