1/*! 2 * tckt link tracking 3 * 4 * Runs on a client's own website (not in the shop). Remembers the campaign 5 * params a visitor landed with and adds them to links to the client's tckt 6 * ticket shop, so the shop can attribute the order. 7 * 8 * Install in <head> of the client's website (or as GTM Custom HTML tag): 9 * <script src="https://shop.<client-domain>/link-tracking.js" data-cookieconsent="ignore" async></script> 10 * data-cookieconsent="ignore" keeps Cookiebot auto-blocking from blocking it; 11 * the script checks marketing consent itself before it sets a cookie. 12 * 13 * Rules (same as the shop's TrackingService): 14 * - Only allowlisted params are kept: utm_*, gclid, gbraid, wbraid, ttclid. 15 * - A landing URL with any of them replaces the complete stored touch, so 16 * params from different visits never mix. A landing without them keeps it. 17 * - Stored in sessionStorage always; in a first-party cookie only while 18 * Cookiebot marketing consent is given (deleted on decline). The cookie 19 * expires 90 days after it was last written. 20 * - On a link to the shop, a stored touch replaces all tracking params already 21 * on the link as one set. Other params on the link (e.g. GA's _gl) are kept. 22 * - A link to the shop without its own referrerpolicy gets 23 * strict-origin-when-cross-origin (the browser default), so the shop sees the 24 * site's origin as referrer even when the site's Referrer-Policy (e.g. 25 * same-origin) would send none. Only the origin is sent, never the path. 26 * Shop buttons that open the shop with JavaScript are not covered. 27 * 28 * Shop links are links to shop.<site domain> (www. stripped), to our platform 29 * domain shop.ihavegotaticket.com, or to the host this script was loaded from. 30 * 31 * window.tcktLinkTracking.decorate(url) is available for custom buttons that 32 * open the shop with JavaScript instead of a link. 33 */ 34(function () { 35 'use strict'; 36 if (window.tcktLinkTracking) return; 37 38 var STORAGE_KEY = 'tckt_touch'; 39 var COOKIE_MAX_AGE = 90 * 24 * 60 * 60; 40 var CLICK_IDS = ['gclid', 'gbraid', 'wbraid', 'ttclid']; 41 var SHOP_LINK_REFERRER_POLICY = 'strict-origin-when-cross-origin'; 42 43 var shopHosts = ['shop.' + location.hostname.replace(/^www\./, ''), 'shop.ihavegotaticket.com']; 44 var script = document.currentScript; 45 if (script && script.src) { 46 var scriptHost = new URL(script.src, location.href).hostname; 47 // When the script is self-hosted the site itself is not a shop 48 if (scriptHost !== location.hostname && shopHosts.indexOf(scriptHost) === -1) shopHosts.push(scriptHost); 49 } 50 51 function isTrackingParam(key) { 52 return key.indexOf('utm_') === 0 || CLICK_IDS.indexOf(key) !== -1; 53 } 54 55 function parseTouch(raw) { 56 if (!raw) return null; 57 try { 58 var touch = JSON.parse(raw); 59 return touch && typeof touch === 'object' && Object.keys(touch).length ? touch : null; 60 } catch (e) { 61 return null; 62 } 63 } 64 65 function readSession() { 66 try { return parseTouch(sessionStorage.getItem(STORAGE_KEY)); } catch (e) { return null; } 67 } 68 69 function writeSession(touch) { 70 try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(touch)); } catch (e) {} 71 } 72 73 function readCookie() { 74 var match = document.cookie.match(new RegExp('(?:^|; )' + STORAGE_KEY + '=([^;]*)')); 75 if (!match) return null; 76 try { return parseTouch(decodeURIComponent(match[1])); } catch (e) { return null; } 77 } 78 79 function writeCookie(touch) { 80 document.cookie = STORAGE_KEY + '=' + encodeURIComponent(JSON.stringify(touch)) + 81 '; max-age=' + COOKIE_MAX_AGE + '; path=/; SameSite=Lax' + 82 (location.protocol === 'https:' ? '; Secure' : ''); 83 } 84 85 function deleteCookie() { 86 document.cookie = STORAGE_KEY + '=; max-age=0; path=/'; 87 } 88 89 // This session's touch is always the newest; the cookie carries one over from an earlier visit 90 function readTouch() { 91 return readSession() || readCookie(); 92 } 93 94 // null until Cookiebot has loaded and the visitor's choice is known 95 function marketingConsent() { 96 var consent = window.Cookiebot && window.Cookiebot.consent; 97 return consent && typeof consent.marketing === 'boolean' ? consent.marketing : null; 98 } 99 100 function capture() { 101 var touch = {};
102 new URLSearchParams(location.search).forEach(function (value, key) { 103 if (value && isTrackingParam(key)) touch[key] = value; 104 }); 105 if (!Object.keys(touch).length) return; 106 writeSession(touch); 107 if (marketingConsent() === true) writeCookie(touch); 108 } 109 110 function applyConsent() { 111 var consent = marketingConsent(); 112 if (consent === true) { 113 var touch = readSession(); 114 if (touch) writeCookie(touch); 115 } else if (consent === false) { 116 deleteCookie(); 117 } 118 } 119 120 function isShopUrl(href) { 121 try { return shopHosts.indexOf(new URL(href, location.href).hostname) !== -1; } catch (e) { return false; } 122 } 123 124 function decorate(href) { 125 var touch = readTouch(); 126 if (!touch || !isShopUrl(href)) return href; 127 var url = new URL(href, location.href); 128 129 var existing = []; 130 url.searchParams.forEach(function (value, key) { existing.push(key); }); 131 existing.forEach(function (key) { if (isTrackingParam(key)) url.searchParams.delete(key); }); 132 Object.keys(touch).forEach(function (key) { 133 if (isTrackingParam(key)) url.searchParams.set(key, touch[key]); 134 }); 135 return url.toString(); 136 } 137 138 // Rewrites the link just before the browser follows it, which also covers 139 // links added later, middle/ctrl-click and "open in new tab" from the context menu 140 function onLinkInteraction(event) { 141 var link = event.target && event.target.closest ? event.target.closest('a[href]') : null; 142 // SVG links have a non-string href 143 if (!link || typeof link.href !== 'string' || !isShopUrl(link.href)) return; 144 if (!link.referrerPolicy) link.referrerPolicy = SHOP_LINK_REFERRER_POLICY; 145 var decorated = decorate(link.href); 146 if (decorated !== link.href) link.href = decorated; 147 } 148 149 capture(); 150 applyConsent(); 151 ['CookiebotOnConsentReady', 'CookiebotOnLoad', 'CookiebotOnAccept', 'CookiebotOnDecline'].forEach(function (name) { 152 window.addEventListener(name, applyConsent); 153 }); 154 ['mousedown', 'touchstart', 'click', 'auxclick', 'contextmenu'].forEach(function (name) { 155 document.addEventListener(name, onLinkInteraction, { capture: true, passive: true }); 156 }); 157 158 window.tcktLinkTracking = { decorate: decorate, touch: readTouch, shopHosts: shopHosts }; 159})();
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.