PageSourceSearch

https://ippyawards.com/wp-content/plugins/client-utm-persistence/assets/utm-persistence.js

js ippyawards.com collected 2026-09-24 20:34:52 UTC 10,028 bytes, 280 lines download raw bytes

1/**
2 * Client UTM Persistence
3 *
4 * 1. Captures tracked params from window.location on every page load.
5 * 2. Stores them in localStorage according to the merge strategy.
6 * 3. Appends stored params to outbound links matching the configured scope.
7 * 4. Re-runs against dynamically added links via MutationObserver.
8 */
9(function () {
10    'use strict';
11
12    var config = window.CUPConfig || {};
13
14    // Sensible defaults if the inline config is missing.
15    var PARAMS = config.paramsToTrack || [
16        'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid'
17    ];
18    var STORAGE_KEY = config.storageKey || 'cup_attribution';
19    var MERGE_STRATEGY = config.mergeStrategy || 'overwrite';
20    var TTL = typeof config.ttl === 'number' ? config.ttl : 0;
21    var LINK_SCOPE = config.linkScope || 'cross_domain';
22    var ALLOWED_HOSTS = (config.allowedHosts || []).map(normalizeHost);
23    var EXCLUDED_HOSTS = (config.excludedHosts || []).map(normalizeHost);
24    var WATCH_DOM = config.watchDom !== false;
25    var DEBUG = !!config.debug;
26
27    var CURRENT_HOST = normalizeHost(window.location.hostname);
28
29    function log() {
30        if (DEBUG && window.console) {
31            console.log.apply(console, ['[CUP]'].concat([].slice.call(arguments)));
32        }
33    }
34
35    function normalizeHost(host) {
36        return String(host || '').toLowerCase().replace(/^www\./, '');
37    }
38
39    // Treat sub.example.com as matching example.com in the excluded list.
40    function hostMatches(host, list) {
41        host = normalizeHost(host);
42        for (var i = 0; i < list.length; i++) {
43            var entry = list[i];
44            if (host === entry || host.endsWith('.' + entry)) {
45                return true;
46            }
47        }
48        return false;
49    }
50
51    function safeStorage() {
52        try {
53            var test = '__cup_test__';
54            window.localStorage.setItem(test, test);
55            window.localStorage.removeItem(test);
56            return window.localStorage;
57        } catch (e) {
58            log('localStorage unavailable:', e);
59            return null;
60        }
61    }
62
63    var storage = safeStorage();
64
65    function readStored() {
66        if (!storage) return null;
67        try {
68            var raw = storage.getItem(STORAGE_KEY);
69            if (!raw) return null;
70            var parsed = JSON.parse(raw);
71            // Expire if TTL is set and exceeded.
72            if (TTL > 0 && parsed.ts && (Date.now() - parsed.ts) > TTL) {
73                storage.removeItem(STORAGE_KEY);
74                return null;
75            }
76            return parsed.data || null;
77        } catch (e) {
78            log('Failed to read storage:', e);
79            return null;
80        }
81    }
82
83    function writeStored(data) {
84        if (!storage) return;
85        try {
86            storage.setItem(STORAGE_KEY, JSON.stringify({ ts: Date.now(), data: data }));
87        } catch (e) {
88            log('Failed to write storage:', e);
89        }
90    }
91
92    // Pull tracked params out of the current URL.
93    function captureFromUrl() {
94        var params = new URLSearchParams(window.location.search);
95        var captured = {};
96        var hasAny = false;
97        for (var i = 0; i < PARAMS.length; i++) {
98            var key = PARAMS[i];
99            if (params.has(key)) {
100                var val = params.get(key);
101                if (val !== null && val !== '') {
102                    captured[key] = val;
103                    hasAny = true;
104                }
105            }
106        }
107        return hasAny ? captured : null;
108    }
109
110    // Apply the configured merge strategy.
111    function mergeAttribution(existing, incoming) {
112        if (!incoming) return existing;
113        if (!existing) return incoming;
114
115        switch (MERGE_STRATEGY) {
116            case 'first_touch':
117                return existing;
118            case 'last_non_direct':
119                // Only overwrite if incoming has at least one utm_* param.
120                var hasUtm = Object.keys(incoming).some(function (k) {
121                    return k.indexOf('utm_') === 0;
122                });
123                return hasUtm ? incoming : existing;
124            case 'overwrite':
125            default:
126                return incoming;
127        }
128    }
129
130    // Decide whether a given link should have UTMs appended.
131    function shouldDecorate(anchor) {
132        // Skip non-http(s) — mailto:, tel:, javascript:, #anchor, etc.
133        var href = anchor.getAttribute('href');
134        if (!href) return false;
135        if (/^(mailto:|tel:|javascript:|sms:|#)/i.test(href)) return false;
136
137        // Anchor.host/protocol resolves relative URLs against the page.
138        var protocol = anchor.protocol;
139        if (protocol !== 'http:' && protocol !== 'https:') return false;
140
141        var linkHost = normalizeHost(anchor.hostname);
142        if (!linkHost) return false;
143
144        // Excluded hosts win regardless of scope.
145        if (EXCLUDED_HOSTS.length && hostMatches(linkHost, EXCLUDED_HOSTS)) {
146            return false;
147        }
148
149        switch (LINK_SCOPE) {
150            case 'external':
151                return linkHost !== CURRENT_HOST;
152            case 'allowlist':
153                return ALLOWED_HOSTS.length > 0 && hostMatches(linkHost, ALLOWED_HOSTS);
154            case 'cross_domain':
155            default:
156                // All links except same-domain.
157                return linkHost !== CURRENT_HOST;
158        }
159    }
160
161    // Append stored params to a link, preserving any params already on the link.
162    function decorateLink(anchor, attribution) {
163        try {
164            var url = new URL(anchor.href);
165            var changed = false;
166            Object.keys(attribution).forEach(function (key) {
167                // Link-level wins: don't overwrite a param that's already on the link.
168                if (!url.searchParams.has(key)) {
169                    url.searchParams.set(key, attribution[key]);
170                    changed = true;
171                }
172            });
173            if (changed) {
174                anchor.href = url.toString();
175                anchor.setAttribute('data-cup-decorated', '1');
176            }
177        } catch (e) {
178            log('Failed to decorate link:', anchor.href, e);
179        }
180    }
181
182    function decorateAll(root, attribution) {
183        if (!attribution || !Object.keys(attribution).length) return;
184        var anchors = (root || document).querySelectorAll('a[href]:not([data-cup-decorated])');
185        var count = 0;
186        for (var i = 0; i < anchors.length; i++) {
187            var a = anchors[i];
188            if (shouldDecorate(a)) {
189                decorateLink(a, attribution);
190                count++;
191            }
192        }
193        if (count) log('Decorated', count, 'link(s)');
194    }
195
196    // Watch for links added later (Elementor popups, AJAX widgets, mega menus, etc).
197    function startObserver(attribution) {
198        if (!WATCH_DOM || !window.MutationObserver) return;
199
200        var pending = false;
201        var observer = new MutationObserver(function (mutations) {
202            // Coalesce bursts of mutations into a single rAF callback.
203            if (pending) return;
204            pending = true;
205            window.requestAnimationFrame(function () {
206                pending = false;
207                for (var i = 0; i < mutations.length; i++) {
208                    var added = mutations[i].addedNodes;
209                    for (var j = 0; j < added.length; j++) {
210                        var node = added[j];
211                        if (node.nodeType !== 1) continue; // Element nodes only.
212                        if (node.tagName === 'A') {
213                            if (shouldDecorate(node) && !node.hasAttribute('data-cup-decorated')) {
214                                decorateLink(node, attribution);
215                            }
216                        } else if (node.querySelectorAll) {
217                            decorateAll(node, attribution);
218                        }
219                    }
220                }
221            });
222        });
223
224        observer.observe(document.documentElement, {
225            childList: true,
226            subtree: true
227        });
228    }
229
230    // Defensive catch-all: if an inline onclick or other handler swaps the
231    // href right before navigation, this re-applies UTMs at click time.
232    function attachClickGuard(getAttribution) {
233        document.addEventListener('click', function (e) {
234            var target = e.target;
235            // Walk up to the anchor (handles clicks on <span> inside <a>, etc).
236            while (target && target !== document && target.tagName !== 'A') {
237                target = target.parentNode;
238            }
239            if (!target || target.tagName !== 'A') return;
240            var attribution = getAttribution();
241            if (attribution && shouldDecorate(target)) {
242                // Force re-decoration in case href was modified.
243                target.removeAttribute('data-cup-decorated');
244                decorateLink(target, attribution);
245            }
246        }, true); // Capture phase so we run before most other click handlers.
247    }
248
249    // Main flow.
250    function init() {
251        var captured = captureFromUrl();
252        var existing = readStored();
253        var attribution = mergeAttribution(existing, captured);
254
255        if (attribution && attribution !== existing) {
256            writeStored(attribution);
257            log('Stored attribution:', attribution);
258        }
259
260        if (!attribution) {
261            log('No attribution to apply');
262            return;
263        }
264
265        // Run as soon as the DOM is parseable.
266        if (document.readyState === 'loading') {
267            document.addEventListener('DOMContentLoaded', function () {
268                decorateAll(document, attribution);
269                startObserver(attribution);
270            });
271        } else {
272            decorateAll(document, attribution);
273            startObserver(attribution);
274        }
275
276        attachClickGuard(function () { return readStored(); });
277    }
278
279    init();
280})();

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.