PageSourceSearch

https://www.gap.at/web-vitals-init.js

js gap.at collected 2026-10-06 13:17:26 UTC 12,048 bytes, 370 lines download raw bytes

1/**
2 * Core Web Vitals RUM collector. A plain static script (outside the app
3 * bundle) so measuring survives failed hydration or unavailable GTM.
4 * Config comes from <meta name="web-vitals-config">; no meta or
5 * sampleRate <= 0 means it does nothing. No cookies, storage or user ids.
6 */
7(function initWebVitals() {
8    "use strict";
9
10    var CONFIG_SELECTOR = 'meta[name="web-vitals-config"]';
11    // Keep beacons well below the ~64 kB sendBeacon limit browsers enforce
12    // (measured in bytes, not string length - selectors may be non-ASCII).
13    var MAX_PAYLOAD_BYTES = 32000;
14    var MAX_STRING_LENGTH = 200;
15    var MAX_URL_LENGTH = 300;
16    var MAX_QUEUE_LENGTH = 50;
17
18    // Attribution fields forwarded per metric. An explicit allow list keeps
19    // the payload small and guarantees no unexpected field (element text,
20    // full URLs with query strings) ever leaves the browser.
21    var ATTRIBUTION_FIELDS = {
22        CLS: ["largestShiftTarget", "largestShiftValue", "loadState"],
23        FCP: ["timeToFirstByte", "firstByteToFCP", "loadState"],
24        INP: [
25            "interactionTarget",
26            "interactionType",
27            "inputDelay",
28            "processingDuration",
29            "presentationDelay",
30            "totalScriptDuration",
31            "loadState",
32        ],
33        LCP: [
34            "target",
35            "url",
36            "timeToFirstByte",
37            "resourceLoadDelay",
38            "resourceLoadDuration",
39            "elementRenderDelay",
40        ],
41        TTFB: [
42            "waitingDuration",
43            "cacheDuration",
44            "dnsDuration",
45            "connectionDuration",
46            "requestDuration",
47        ],
48    };
49
50    var configMeta = document.querySelector(CONFIG_SELECTOR);
51
52    if (!configMeta) {
53        return;
54    }
55
56    var config;
57
58    try {
59        config = JSON.parse(configMeta.getAttribute("content") || "");
60        // eslint-disable-next-line @typescript-eslint/no-unused-vars -- optional catch binding is ES2019; a bare catch would make the whole ES5 script unparsable in legacy browsers
61    } catch (parseError) {
62        return;
63    }
64
65    if (!config || !config.endpoint || !config.libSrc) {
66        return;
67    }
68
69    var sampleRate = Number(config.sampleRate);
70
71    // One sampling decision per page load, so a sampled-in page reports all
72    // of its metrics and a sampled-out page does not even fetch the library.
73    if (!(sampleRate > 0) || Math.random() >= sampleRate) {
74        return;
75    }
76
77    var queue = [];
78    var batchIndex = 0;
79    var pageloadId = createPageloadId();
80
81    function createPageloadId() {
82        // crypto.randomUUID is unavailable outside secure contexts.
83        if (window.crypto && typeof window.crypto.randomUUID === "function") {
84            return window.crypto.randomUUID();
85        }
86
87        return (
88            Date.now().toString(36) +
89            "-" +
90            Math.random().toString(36).slice(2, 12)
91        );
92    }
93
94    function truncate(value, maxLength) {
95        if (typeof value !== "string") {
96            return null;
97        }
98
99        return value.length > maxLength ? value.slice(0, maxLength) : value;
100    }
101
102    function byteLength(text) {
103        // Blob measures UTF-8 bytes; falls back to string length where
104        // Blob is unavailable (never in browsers this script targets).
105        return typeof Blob === "function" ? new Blob([text]).size : text.length;
106    }
107
108    function roundValue(value) {
109        // Sub-millisecond precision is noise; CLS needs decimals, so keep
110        // four of them.
111        return Math.round(value * 10000) / 10000;
112    }
113
114    function stripQuery(value) {
115        return typeof value === "string" ? value.split("?")[0] : value;
116    }
117
118    function sanitizeAttributionValue(key, value) {
119        if (typeof value === "number") {
120            return isFinite(value) ? roundValue(value) : null;
121        }
122
123        if (typeof value === "string") {
124            // URLs lose their query strings - they may carry signed tokens
125            // or campaign parameters and add nothing to diagnostics.
126            return key === "url"
127                ? truncate(stripQuery(value), MAX_URL_LENGTH)
128                : truncate(value, MAX_STRING_LENGTH);
129        }
130
131        return null;
132    }
133
134    function pickAttribution(metric) {
135        var source = metric.attribution;
136        var fields = ATTRIBUTION_FIELDS[metric.name];
137
138        if (!source || !fields) {
139            return null;
140        }
141
142        var result = {};
143        var hasValue = false;
144
145        for (var i = 0; i < fields.length; i++) {
146            var key = fields[i];
147            var value = sanitizeAttributionValue(key, source[key]);
148
149            if (value !== null) {
150                result[key] = value;
151                hasValue = true;
152            }
153        }
154
155        // The slowest script behind a bad INP - this is what points at
156        // third-party tags (GTM and friends) as the actual cost.
157        var longestScript = source.longestScript;
158
159        if (longestScript && longestScript.entry) {
160            var scriptUrl = truncate(
161                stripQuery(longestScript.entry.sourceURL),
162                MAX_URL_LENGTH
163            );
164
165            if (scriptUrl) {
166                result.longestScriptUrl = scriptUrl;
167                hasValue = true;
168            }
169
170            if (typeof longestScript.intersectingDuration === "number") {
171                result.longestScriptDuration = roundValue(
172                    longestScript.intersectingDuration
173                );
174                hasValue = true;
175            }
176
177            if (typeof longestScript.subpart === "string") {
178                result.longestScriptSubpart = longestScript.subpart;
179                hasValue = true;
180            }
181        }
182
183        return hasValue ? result : null;
184    }
185
186    function getBuildId() {
187        // Production builds use the git hash as the Next.js build id
188        // (generateBuildId in next.config), which makes before/after
189        // deploy comparisons possible without any extra configuration.
190        var nextData = window.__NEXT_DATA__;
191
192        if (nextData && typeof nextData.buildId === "string") {
193            return truncate(nextData.buildId, 40);
194        }
195
196        return null;
197    }
198
199    function getPageType() {
200        // Route pattern (for example /catalog/[id]) groups metrics by
201        // template. The Next.js client router stays current across
202        // client-side navigation, __NEXT_DATA__ only holds the entry route.
203        var nextGlobal = window.next;
204
205        if (
206            nextGlobal &&
207            nextGlobal.router &&
208            typeof nextGlobal.router.pathname === "string"
209        ) {
210            return truncate(nextGlobal.router.pathname, MAX_STRING_LENGTH);
211        }
212
213        var nextData = window.__NEXT_DATA__;
214
215        if (nextData && typeof nextData.page === "string") {
216            return truncate(nextData.page, MAX_STRING_LENGTH);
217        }
218
219        return null;
220    }
221
222    function getConnection() {
223        var connection =
224            navigator.connection ||
225            navigator.mozConnection ||
226            navigator.webkitConnection;
227
228        return connection || null;
229    }
230
231    function buildDevice() {
232        var connection = getConnection();
233
234        return {
235            viewportW: window.innerWidth || null,
236            deviceMemory:
237                typeof navigator.deviceMemory === "number"
238                    ? navigator.deviceMemory
239                    : null,
240            effectiveType: connection
241                ? truncate(connection.effectiveType, 20)
242                : null,
243            saveData: connection ? !!connection.saveData : null,
244        };
245    }
246
247    function handleMetric(metric) {
248        if (queue.length >= MAX_QUEUE_LENGTH) {
249            return;
250        }
251
252        queue.push({
253            name: metric.name,
254            id: metric.id,
255            value: roundValue(metric.value),
256            delta: roundValue(metric.delta),
257            rating: metric.rating || null,
258            navigationType: metric.navigationType || null,
259            source: "web-vitals",
260            // Captured when the metric is reported, not when it is sent -
261            // CLS and INP are only finalized as the page is hidden, which
262            // may happen on a different route than the one that was loaded.
263            pathname: truncate(location.pathname, MAX_STRING_LENGTH),
264            pageType: getPageType(),
265            attribution: pickAttribution(metric),
266        });
267    }
268
269    function send(body) {
270        if (
271            navigator.sendBeacon &&
272            navigator.sendBeacon(config.endpoint, body)
273        ) {
274            return;
275        }
276
277        // sendBeacon refuses oversized or queued-up payloads; keepalive is
278        // the only fetch mode that survives an unloading document.
279        if (typeof fetch === "function") {
280            fetch(config.endpoint, {
281                body: body,
282                method: "POST",
283                keepalive: true,
284                credentials: "omit",
285            }).catch(function ignoreDeliveryFailure() {});
286        }
287    }
288
289    function flush() {
290        if (queue.length === 0) {
291            return;
292        }
293
294        var metrics = queue.splice(0, queue.length);
295        var payload = {
296            // Stable per delivery, so a retried beacon is deduplicated by
297            // BigQuery while a genuine second flush is kept.
298            batchId: pageloadId + ":" + batchIndex++,
299            pageloadId: pageloadId,
300            brand: config.brand,
301            site: config.site || null,
302            locale: config.locale || null,
303            appVersion: getBuildId(),
304            sampling: sampleRate,
305            gtmLoaded: !!window.google_tag_manager,
306            device: buildDevice(),
307            abTests: config.abTests || [],
308            metrics: metrics,
309        };
310        var body = JSON.stringify(payload);
311
312        if (byteLength(body) > MAX_PAYLOAD_BYTES) {
313            // Attribution is the bulk of the payload; better to deliver the
314            // metric values without diagnostics than to lose the batch.
315            for (var i = 0; i < metrics.length; i++) {
316                metrics[i].attribution = null;
317            }
318            body = JSON.stringify(payload);
319
320            if (byteLength(body) > MAX_PAYLOAD_BYTES) {
321                return;
322            }
323        }
324
325        send(body);
326    }
327
328    function registerMetrics(webVitals) {
329        webVitals.onCLS(handleMetric);
330        webVitals.onINP(handleMetric);
331        webVitals.onLCP(handleMetric);
332        webVitals.onFCP(handleMetric);
333        webVitals.onTTFB(handleMetric);
334
335        // Registered only after the library has its own listeners in place.
336        // web-vitals finalizes CLS and INP from a capture-phase
337        // visibilitychange listener on window, so a bubble-phase listener
338        // added here always runs after the metrics have been queued.
339        //
340        // Deliberately NO pagehide listener: during unload pagehide fires
341        // BEFORE visibilitychange, so flushing there would send a premature
342        // partial beacon and the finalized CLS/INP in a second one. All
343        // browsers this script measures (visibilitychange has been fired on
344        // navigation since Chrome 68 / Safari 14.5) reach this handler.
345        addEventListener("visibilitychange", function onVisibilityChange() {
346            if (document.visibilityState === "hidden") {
347                flush();
348            }
349        });
350    }
351
352    function loadLibrary() {
353        var script = document.createElement("script");
354
355        script.src = config.libSrc;
356        script.async = true;
357
358        script.onload = function onLibraryLoad() {
359            if (window.webVitals) {
360                registerMetrics(window.webVitals);
361            }
362        };
363
364        document.head.appendChild(script);
365    }
366
367    // The library reads buffered performance entries, so loading it late
368    // costs no data while keeping it off the critical path.
369    loadLibrary();
370})();

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.