PageSourceSearch

https://developerakademie.com/wp-content/themes/developerakademie-…/js/sections/ab-testing.js?ver=1790766710

js developerakademie.com collected 2026-10-02 15:06:28 UTC 26,311 bytes, 552 lines download raw bytes

1/**
2 * A/B-test runtime for landing sections (Spec G, PP-1609).
3 *
4 * The template-part renders every variant as a hidden sibling carrying
5 * data-da-exp / data-da-variant (+ optional data-da-goal). This module asks
6 * Statsig (via the theme's same-origin config proxy — no US call pre-consent)
7 * which variant to reveal, removes the losers, and wires up event logging:
8 *
9 *   - exposure: logged by the SDK on getExperiment() (= reveal time), mirrored
10 *     to the dataLayer as `da_ab_exposure` for GA reporting.
11 *   - click:    `<exp_key>_click` to Statsig + `da_ab_click` to the dataLayer.
12 *   - goal:     dataLayer events named in data-da-goal are forwarded to Statsig.
13 *
14 * Consent: SDK starts with logging DISABLED — Events werden nicht verworfen,
15 * sondern vom SDK gepuffert, damit der erste Seitenaufruf eines neuen Besuchers
16 * messbar bleibt, wenn er danach einwilligt. Erteilt das Gate den Consent, wird
17 * Logging eingeschaltet und der Puffer geht raus; bei Ablehnung oder Widerruf
18 * wird er samt allen statsig.*-Schluesseln verworfen. Nichts wird neu geloggt.
19 *
20 * Failsafe: the inline head script (template-functions/ab-testing.php) reveals
21 * the control after 2500 ms for every key this module hasn't decided.
22 * Coordination happens via window.__daAb.{done,failed}.
23 *
24 * Preview: ?da-exp=<key>:<variant>[,<key>:<variant>] reveals directly —
25 * no SDK init, no exposure, no proxy request (when all keys are overridden).
26 */
27(function () {
28    'use strict';
29
30    var cfg = window.daAb || {};
31    var state = (window.__daAb = window.__daAb || { done: {}, failed: {} });
32
33    var nodes = document.querySelectorAll('[data-da-exp]');
34    if (!nodes.length) {
35        return;
36    }
37
38    var groups = {};
39    var goals = {}; // exp key -> dataLayer event name to forward
40    Array.prototype.forEach.call(nodes, function (node) {
41        var key = node.getAttribute('data-da-exp');
42        (groups[key] = groups[key] || []).push(node);
43        var goal = node.getAttribute('data-da-goal');
44        if (goal) {
45            goals[key] = goal;
46        }
47    });
48
49    window.dataLayer = window.dataLayer || [];
50
51    // CookieYes-Kategorie, an der das Statsig-Logging haengt. MUSS hier oben stehen:
52    // hasCookieConsent() laeuft schon vor der SDK-Initialisierung, und eine weiter
53    // unten zugewiesene var waere dann noch undefined — der Regex hiesse
54    // "undefined:yes" und eine erteilte Einwilligung galte faelschlich als fehlend.
55    var CONSENT_CATEGORY = 'analytics';
56
57    // Consent state (PP-1808). Events logged before consent are buffered by
58    // the SDK in localStorage and only flushed once logging is enabled.
59    var consentApplied = false;
60    // Tri-State: solange niemand entschieden hat, puffert das SDK — die Exposure
61    // dieses Seitenaufrufs geht dann bei einer Einwilligung noch raus. Nach einer
62    // ABLEHNUNG darf nichts mehr entstehen, auch nicht im Puffer.
63    var consentWithdrawn = false;
64
65    // Manueller Hook fuer andere Banner (docs/ab-testing-betrieb.md). Als Stub, damit
66    // ein Aufruf auf den Early-Return-Pfaden (Preview, kein SDK, Ablehnung) nicht
67    // wirft; die echte Implementierung ueberschreibt ihn weiter unten.
68    window.daAbSetConsent = function () {
69        return;
70    };
71
72    /**
73     * Reveal one variant of an experiment, drop the rest. Falls back to the
74     * control when the assigned variant has no node (e.g. variant removed in
75     * WP while the experiment still runs).
76     */
77    function reveal(key, variant, silent) {
78        if (state.done[key] || state.failed[key]) {
79            return;
80        }
81        var list = groups[key];
82        var hasVariant = list.some(function (n) {
83            return n.getAttribute('data-da-variant') === variant;
84        });
85        if (!hasVariant) {
86            variant = 'control';
87        }
88        list.forEach(function (n) {
89            if (n.getAttribute('data-da-variant') === variant) {
90                n.removeAttribute('hidden');
91                bindClick(key, variant, n);
92            } else if (n.parentNode) {
93                n.parentNode.removeChild(n);
94            }
95        });
96        state.done[key] = variant;
97        if (!silent) {
98            window.dataLayer.push({ event: 'da_ab_exposure', exp: key, variant: variant });
99        }
100    }
101
102    function revealAllControl(silent) {
103        Object.keys(groups).forEach(function (key) {
104            reveal(key, 'control', silent);
105        });
106    }
107
108    function bindClick(key, variant, node) {
109        var target = node.closest ? (node.closest('a,button') || node) : node;
110        if (target.__daAbBound) {
111            return;
112        }
113        target.__daAbBound = true;
114        target.addEventListener('click', function () {
115            // Nach einem Widerruf nicht einmal puffern: bei disableLogging verwirft
116            // das SDK das Event nicht, es schreibt es in den localStorage.
117            if (client && !consentWithdrawn) {
118                client.logEvent(key + '_click', variant);
119                // Click often precedes navigation — flush immediately so the
120                // event doesn't die in the queue when the page unloads.
121                try { client.flush(); } catch (e) { /* noop */ }
122            }
123            window.dataLayer.push({ event: 'da_ab_click', exp: key, variant: variant });
124        });
125    }
126
127    // ---- Preview override: ?da-exp=key:b,other_key:c --------------------------
128    var pending = Object.keys(groups);
129    var override = null;
130    try {
131        override = new URLSearchParams(window.location.search).get('da-exp');
132    } catch (e) { /* very old browsers: no preview support */ }
133    if (override) {
134        override.split(',').forEach(function (pair) {
135            var parts = pair.split(':');
136            if (parts.length === 2 && groups[parts[0]]) {
137                reveal(parts[0], parts[1], true);
138                pending = pending.filter(function (k) { return k !== parts[0]; });
139            }
140        });
141    }
142    if (!pending.length) {
143        return; // fully previewed — no SDK, no requests
144    }
145    // Partial override (multi-experiment page, only some keys in ?da-exp=…):
146    // the overridden keys stay silent, but the SDK/proxy still runs for the
147    // remaining ones — QA sessions on such pages DO produce exposures there.
148
149    // ---- Statsig client (bootstrap via same-origin proxy) ----------------------
150    var Statsig = window.__STATSIG__;
151    if (!cfg.clientKey || !cfg.initUrl || !Statsig || !Statsig.StatsigClient) {
152        revealAllControl(true); // degrade silently: everyone sees the control
153        return;
154    }
155
156    // Explicit rejection on record (the visitor answered the banner and said no to
157    // analytics): do not start the SDK at all. Otherwise it would write its five
158    // localStorage keys again on EVERY pageview and keep assigning a variant, both
159    // against a stated "no" — § 25 Abs. 1 TDDDG attaches to the storage, not to the
160    // transmission. Everything an earlier pageview left behind goes now, and
161    // everyone in this state sees the control (PP-1808).
162    if (consentDeclined()) {
163        dropStatsigStorage();
164        revealAllControl(true);
165        // Stimmt der Besucher hier doch noch zu, bleibt es bei der Kontrolle —
166        // ohne SDK und mit absichtlich leerem Speicher gibt es auf diesem Pageview
167        // keine Zuweisung mehr. Ab dem naechsten Aufruf laeuft alles normal. Ein
168        // location.reload() waere der groebere Hebel und haette einen Fehlpfad:
169        // feuert das Event, bevor CookieYes das Cookie geschrieben hat, greift
170        // dieser Guard nach dem Reload erneut und der Besucher haengt trotz
171        // Einwilligung in der Kontrolle (PP-1808).
172        return;
173    }
174
175    // The SDK retries events that a previous session could not flush at shutdown:
176    // EventLogger.start() → loadAndRetryShutdownFailedEvents() puts them straight
177    // into the batch queue, bypassing the disableLogging gate. Without consent that
178    // would be a US call before any consent exists — drop them first (verified
179    // against the vendored SDK 3.33.3, PP-1808).
180    //
181    // `hasCookieConsent()` allein reicht als Bedingung NICHT: hier laeuft der Code
182    // synchron, das Gate hat in aller Regel noch nichts gesagt, und dann wuerde der
183    // Puffer bei JEDEM Aufruf sterben — auch beim Eingewilligten. Darin stehen meist
184    // `<key>_click`-Events, deren Seitenaufruf mit dem Wechsel auf weiterbildung.
185    // endete; sie still zu loeschen senkt die Sekundaer-Metrik.
186    //
187    // Deshalb positive Evidenz verlangen statt blosser Abwesenheit einer Ablehnung:
188    // entweder das Gate hat bereits erteilt, oder im Cookie steht eine Entscheidung,
189    // die Analytics erteilt. Bleibt beides aus — Erstbesuch ohne Entscheidung, oder
190    // ein Re-Prompt nach abgelaufenem Consent, bei dem noch ein Puffer aus der
191    // Consent-Zeit liegt — wird geraeumt.
192    if (!hasCookieConsent() && !consentGrantedInCookie()) {
193        dropShutdownFailedEvents();
194    }
195
196    var client = new Statsig.StatsigClient(
197        cfg.clientKey,
198        {},
199        {
200            // Environment-Tier (PP-1808 A13). Ein Projekt, ein Client-Key fuer alle
201            // Umgebungen: ohne diese Angabe zaehlen Staging- und Abnahmelaeufe als
202            // Produktionsdaten und verschieben die Scorecards laufender Experimente.
203            // Statsig wertet in Pulse nur `production` aus, der Rest bleibt in den
204            // Diagnostics sichtbar.
205            //
206            // Fehlt die Angabe, wird sie NICHT geraten: ein Default 'development'
207            // wuerde bei einem Ausfall auf Produktion saemtliche echten Daten
208            // stillschweigend aus Pulse nehmen. Ohne Angabe bleibt es beim
209            // SDK-Standard, also beim Verhalten von vor A13.
210            environment: cfg.tier ? { tier: cfg.tier } : undefined,
211            disableLogging: true, // vor dem Consent verlaesst kein Event den Browser
212            // Plain-JSON body (no statsig encoding) so the theme proxy can read
213            // the stableID for its transient cache and forward verbatim.
214            disableStatsigEncoding: true,
215            networkConfig: { initializeUrl: cfg.initUrl }
216        }
217    );
218
219    client.initializeAsync()
220        .then(function () {
221            // The real stableID of this client — utm-params.js would otherwise have
222            // to guess it from the localStorage key suffix (PP-1808).
223            try {
224                // Nicht nach einem Widerruf, der in dieses Init-Fenster gefallen ist:
225                // der Widerrufs-Zweig hat die Kennung geloescht, und sie hier wieder
226                // zu veroeffentlichen haengt sie bei einer erneuten Zustimmung im
227                // selben Pageview an die Links — fuer Exposures, die es nie gab
228                // (PP-1808, festgenagelt von T1.8b).
229                if (!consentWithdrawn) {
230                    state.stableId = client.getContext().stableID;
231                    publishLinkParams();
232                }
233            } catch (e) { /* older SDK build without getContext */ }
234            pending.forEach(function (key) {
235                // Race guard: if the 2500 ms failsafe already revealed the
236                // control (slow proxy), getExperiment() would still log an
237                // exposure for a variant the user never saw and silently
238                // bias the experiment. Skip entirely.
239                //
240                // `consentWithdrawn` aus demselben Grund wie in bindClick() und
241                // forwardGoal(): nach einer Ablehnung darf nicht einmal gepuffert
242                // werden. Dass heute auch ohne diese Zeile nichts entsteht, haengt
243                // allein an `disableStorage` im Widerrufs-Zweig — die Invariante
244                // gehoert in den eigenen Code, nicht in die Annahme ueber das SDK
245                // (PP-1808).
246                if (state.failed[key] || consentWithdrawn) {
247                    return;
248                }
249                var variant = 'control';
250                try {
251                    // Loggt die Exposure. Vor dem Consent landet sie im
252                    // localStorage-Puffer des SDK und geht erst raus, wenn der
253                    // Consent-Hook das Logging einschaltet. Kein
254                    // disableExposureLog: das SDK zaehlt dann einen "non exposed
255                    // check" und flusht das Aggregat schon vor dem Consent.
256                    variant = client.getExperiment(key).get('variant', 'control');
257                } catch (e) { /* unknown experiment -> control */ }
258                reveal(key, variant);
259            });
260        })
261        .catch(function () {
262            revealAllControl(true);
263        });
264
265    // ---- Goal events: forward configured dataLayer events to Statsig -----------
266    // Ordering caveat: GTM replaces dataLayer.push when its container loads.
267    // If that happens AFTER this deferred module, a single wrap would be
268    // silently discarded (goal events stop forwarding). Hence: re-wrap after
269    // the page settles (load + 3 s), and dedupe via WeakSet in case an event
270    // object travels through two of our wrappers.
271    if (Object.keys(goals).length) {
272        var forwarded = typeof WeakSet === 'function' ? new WeakSet() : null;
273        var forwardGoal = function (entry) {
274            if (!entry || typeof entry !== 'object' || !entry.event) {
275                return;
276            }
277            if (forwarded) {
278                if (forwarded.has(entry)) {
279                    return;
280                }
281                forwarded.add(entry);
282            }
283            if (consentWithdrawn) {
284                return; // s. bindClick(): nach einer Ablehnung nicht einmal puffern
285            }
286            Object.keys(goals).forEach(function (key) {
287                if (goals[key] === entry.event && state.done[key]) {
288                    client.logEvent(goals[key], state.done[key], { exp: key });
289                    try { client.flush(); } catch (e) { /* noop */ }
290                }
291            });
292        };
293        var wrapPush = function () {
294            var dl = window.dataLayer;
295            if (!dl || typeof dl.push !== 'function' || dl.push.__daAbWrapped) {
296                return;
297            }
298            var orig = dl.push.bind(dl);
299            var wrapper = function () {
300                for (var i = 0; i < arguments.length; i++) {
301                    forwardGoal(arguments[i]);
302                }
303                return orig.apply(null, arguments);
304            };
305            wrapper.__daAbWrapped = true;
306            dl.push = wrapper;
307        };
308        wrapPush();
309        window.addEventListener('load', wrapPush);
310        setTimeout(wrapPush, 3000);
311    }
312
313    // ---- Consent hook -----------------------------------------------------------
314    /**
315     * Removes everything Statsig keeps on the device (`statsig.*` — stableID,
316     * session, cached evaluations, buffers). Used on a rejection or withdrawal,
317     * where none of it may survive, and before skipping the SDK on a later pageview.
318     *
319     * utm-params.js hat eine gleichlautende Kopie, und das bleibt so: die dortige
320     * haengt am Gate-Ereignis und raeumt auf JEDER Seite auf (die Cookie-
321     * Einstellungen stehen im Footer, auch wo kein Test laeuft). Diese hier muss
322     * SYNCHRON vor dem SDK-Start laufen — ein Moment, in dem noch kein Ereignis
323     * gefallen ist: liegt eine Ablehnung vor, darf das SDK die Reste eines
324     * frueheren Besuchs nicht erst einlesen und weiterbenutzen. Die Kopie ins
325     * Gate zu ziehen scheidet aus einem dritten Grund aus: dort liefe sie bei
326     * jedem update() ohne Analytics-Consent, also auch beim Erstbesuch mit
327     * offenem Banner, wo der Puffer erhalten bleiben muss.
328     */
329    function dropStatsigStorage() {
330        dropStorageKeys(function (key) {
331            return key.indexOf('statsig.') === 0;
332        });
333    }
334
335    /**
336     * Removes every localStorage key the predicate accepts.
337     *
338     * @param {function(string): boolean} matches Predicate over the key name.
339     */
340    function dropStorageKeys(matches) {
341        try {
342            var stale = [];
343            for (var i = 0; i < window.localStorage.length; i++) {
344                var key = window.localStorage.key(i);
345                if (key && matches(key)) {
346                    stale.push(key);
347                }
348            }
349            stale.forEach(function (key) {
350                window.localStorage.removeItem(key);
351            });
352        } catch (e) { /* storage disabled — nothing stored either */ }
353    }
354
355    /**
356     * Removes events a previous session could not flush at shutdown. The SDK sends
357     * these on the next start() past the logging gate, so they must not survive a
358     * withdrawal and must not exist before the first consent.
359     */
360    function dropShutdownFailedEvents() {
361        dropStorageKeys(function (key) {
362            return key.indexOf('statsig.failed_shutdown_events.') === 0;
363        });
364    }
365
366    /**
367     * Sagt utm-params.js, dass sich die Grundlage der Link-Parameter geaendert hat:
368     * die Kennung steht (nach dem Init) oder der Consent-Stand ist ein anderer.
369     *
370     * Den Consent-Stand erfaehrt `utm-params.js` seit PP-1785 selbst, ueber
371     * `da_consent_change` aus dem Gate. Was es NICHT selbst erfaehrt, ist die
372     * Kennung: die steht erst, wenn das SDK geantwortet hat. Ohne dieses Signal
373     * ginge die Exposure raus, der Link bliebe ohne `da_sid`, der Lead waere nicht
374     * zuordenbar. Stille Unter-Attribution, genau der Fehlermodus, gegen den T1.7k die
375     * Gegenrichtung absichert (PP-1808).
376     *
377     * `appendQueryParametersToLinks()` drueben ist idempotent und setzt oder
378     * entfernt die Parameter je nach Consent-Stand selbst — deshalb ist dasselbe
379     * Signal fuer Erteilung und Widerruf richtig.
380     */
381    function publishLinkParams() {
382        document.dispatchEvent(new CustomEvent('da_ab_link_params'));
383    }
384
385    window.daAbSetConsent = function (granted) {
386        if (!client) {
387            return;
388        }
389        // Widerruf: Logging aus, Speicher weg, Zuweisungs-Store nicht mehr fuellen.
390        // Ohne das verlieren die Links ihre Parameter (utm-params.js), waehrend das
391        // SDK weiter Klicks sendet — die beiden Haelften widersprechen sich dann.
392        if (!granted) {
393            consentWithdrawn = true;
394            consentApplied = false;
395            try {
396                // disableStorage mit: der laufende Client wuerde Session und
397                // Evaluations sonst nach dem Aufraeumen gleich neu schreiben. Ab dem
398                // naechsten Seitenaufruf haelt der Guard oben das SDK ganz an.
399                client.updateRuntimeOptions({ disableLogging: true, disableStorage: true });
400            } catch (e) { /* SDK gone — nothing to disable */ }
401            // Auch die veroeffentlichte Kennung zuruecknehmen: utm-params.js liest
402            // sie primaer von dort, und das Consent-Gate soll nicht das Einzige
403            // sein, was sie von den Links fernhaelt (PP-1808).
404            delete state.stableId;
405            dropStatsigStorage();
406            publishLinkParams();
407            return;
408        }
409        if (consentApplied) {
410            return;
411        }
412        consentApplied = true;
413        consentWithdrawn = false;
414        try {
415            // Logging einschalten reicht: das SDK laedt seinen localStorage-Puffer
416            // (die Exposures dieses Seitenaufrufs) und flusht ihn. Es gibt kein
417            // Custom-Feld am User mehr, das vorher zu korrigieren waere — dass bei
418            // Statsig nur Eingewilligtes ankommt, garantiert der Ablauf selbst
419            // (PP-1808).
420            client.updateRuntimeOptions({ disableLogging: false, disableStorage: false });
421        } catch (e) {
422            return; // SDK gone — nothing to enable
423        }
424        // Auf die Einwilligung folgt oft der Klick auf den CTA — die Events nicht
425        // auf den geplanten Flush warten lassen.
426        try { client.flush(); } catch (e) { /* noop */ }
427        publishLinkParams();
428    };
429
430    // ---- Consent-Anbindung ---------------------------------------------------
431    // Die Consent-Hoheit liegt seit PP-1785 beim zentralen Gate in
432    // template-parts/tracking-head.php. Es kennt die drei Zustaende (erteilt /
433    // abgelehnt / noch nicht entschieden), prueft `action:yes`, haelt einen
434    // Bestaetigungs-Poll fuer Events, deren Cookie noch nicht geschrieben ist, und
435    // feuert `da_consent_change` bei jedem Zustandswechsel.
436    //
437    // Die lokale CookieYes-Auswertung aus PP-1808 (ckyDecision, eigener Cookie-Poll,
438    // Abonnement von consent_update/banner_load) ist damit **ersatzlos entfallen**:
439    // sie war der Nachbau genau dieser Regeln fuer die Zeit, in der das Gate noch
440    // nicht gemerged war. Alle dort teuer erkauften Faelle deckt das Gate ab — auch
441    // der Erstbesucher, dessen `consent_update` mit voller `rejected`-Liste kommt,
442    // bevor er geklickt hat (PP-1808): das Gate startet dafuer den
443    // Bestaetigungs-Poll, statt eine Ablehnung anzuwenden.
444    //
445    // Andere Banner koennen weiterhin direkt window.daAbSetConsent(true|false) rufen.
446
447    /**
448     * Steht im hinterlegten Cookie eine Entscheidung, die diese Kategorie ERTEILT?
449     *
450     * Synchron beantwortbar, anders als daHasConsent() vor der ersten Antwort des
451     * Gates. "Entschieden und nicht abgelehnt" ist genau eine Erteilung, weil
452     * daConsentDeclined() als "entschieden UND nicht erteilt" definiert ist.
453     *
454     * Nur fuer den Retry-Puffer gedacht (s. Aufrufstelle) — nichts, was Dritt-Code
455     * freischaltet, darf daran haengen.
456     *
457     * @returns {boolean} True nur bei hinterlegter Erteilung; ohne Gate false.
458     */
459    function consentGrantedInCookie() {
460        return typeof window.daConsentDecided === 'function'
461            && window.daConsentDecided()
462            && !consentDeclined();
463    }
464
465    /**
466     * Hat der Besucher die Analytics-Kategorie ausdruecklich ABGELEHNT?
467     *
468     * Beantwortet das Gate (`window.daConsentDeclined`), nicht dieses Modul. Hier stand
469     * frueher ein eigener Cookie-Regex — der nahm das ERSTE `cookieyes-consent=` aus
470     * document.cookie, waehrend auf Produktion zwei davon existieren koennen (host-only
471     * neben `.developerakademie.com`, aeltere zuerst gelistet). Das Gate loest den Fall
472     * in parseCookie() auf und beruecksichtigt zusaetzlich staleRaw beim Re-Prompt; zwei
473     * Parser nebeneinander konnten bei denselben Cookies gegensaetzlich entscheiden
474     * (PP-1808).
475     *
476     * Warum ueberhaupt eine zweite Frage neben hasCookieConsent(): das Gate antwortet
477     * asynchron und meldet bis zur ersten CookieYes-Antwort "kein Consent" — das ist von
478     * einer Ablehnung nicht zu unterscheiden. Der Guard unten muss aber SYNCHRON
479     * entscheiden, bevor der Client gebaut wird.
480     *
481     * @returns {boolean} True nur bei einer vorliegenden Ablehnung; ohne Gate false.
482     */
483    function consentDeclined() {
484        return typeof window.daConsentDeclined === 'function'
485            && window.daConsentDeclined(CONSENT_CATEGORY);
486    }
487
488    /**
489     * Eine Definition von "Consent liegt vor" — die des Gates.
490     * Fehlt der Helper, wird nicht freigegeben (PP-1785: kein Fallback, der laedt).
491     */
492    function hasCookieConsent() {
493        return typeof window.daHasConsent === 'function' && window.daHasConsent(CONSENT_CATEGORY);
494    }
495
496    /**
497     * Meldet dem Gate, dass wegen `analytics` etwas scharfgeschaltet wurde. Ohne die
498     * Meldung steht das Modul nicht in der live-Liste und ein Widerruf loest keinen
499     * Reload aus (PP-1785).
500     */
501    function markConsentUsed() {
502        if (typeof window.daMarkConsentUsed === 'function') {
503            window.daMarkConsentUsed(CONSENT_CATEGORY);
504        }
505    }
506
507    /**
508     * @param {boolean} gateHasSpoken True, wenn der Aufruf aus `da_consent_change` kommt.
509     *
510     * Zwei verschiedene Fragen, die frueher eine waren:
511     *   - Aus dem Gate-Event heraus HAT das Gate geantwortet. "Kein Consent" ist dann eine
512     *     echte Aussage — auch wenn das Cookie noch das Alte sagt, weil CookieYes es erst
513     *     nach dem Event schreibt: der ausgesprochene Stand gewinnt.
514     *   - Beim synchronen Erstaufruf hat es das nicht. Dort zaehlt nur eine im Cookie
515     *     hinterlegte Ablehnung, sonst wuerde jeder Eingewilligte abgeschaltet, bevor das
516     *     Gate ueberhaupt dazu kommt.
517     */
518    function applyConsentState(gateHasSpoken) {
519        if (hasCookieConsent()) {
520            markConsentUsed();
521            window.daAbSetConsent(true);
522            return;
523        }
524        // Abschalten nur bei einer AUSDRUECKLICHEN Ablehnung im Cookie — nicht schon,
525        // wenn das Gate bloss noch nichts gesagt hat.
526        //
527        // `hasCookieConsent()` oben kommt vom Gate und ist asynchron (3 s Grace, danach
528        // Events oder Cookie-Poll). Beim Sofort-Check unten hat es also noch nicht
529        // geantwortet. Stand hier frueher ein `action:yes`-Check, feuerte bei jedem
530        // wiederkehrenden Besucher mit erteiltem Consent sofort ein daAbSetConsent(false):
531        // consentWithdrawn gesetzt, Speicher geloescht, Kennung verworfen — noch bevor
532        // initializeAsync() durch war. Ergebnis: keine Zuweisung, kein `da_sid`, und das
533        // ausgerechnet im haeufigsten Fall (PP-1808).
534        //
535        // `consentDeclined()` fragt das Gate synchron nach einer ausgesprochenen
536        // Ablehnung und ist damit eindeutig — dieselbe Quelle wie der Guard oben.
537        // `consentApplied` statt blossem `gateHasSpoken`: das Gate feuert auch beim ERSTEN
538        // Stand eines Erstbesuchers ("nichts erteilt", Banner offen). Das als Widerruf zu
539        // lesen wuerde den Puffer dieses Seitenaufrufs verwerfen. Ein Widerruf ist nur
540        // einer, wenn vorher Consent galt.
541        if ((gateHasSpoken && consentApplied) || consentDeclined()) {
542            window.daAbSetConsent(false);
543        }
544    }
545
546    document.addEventListener('da_consent_change', function () {
547        applyConsentState(true);
548    });
549
550    // Sofort-Check fuer "Stand lag beim Laden schon vor"; alles spaetere per Event.
551    applyConsentState(false);
552})();

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.