PageSourceSearch

https://drain-master.de/wp-content/mu-plugins/dm-meta-events.js?ver=1787756787

js drain-master.de collected 2026-09-24 13:35:26 UTC 13,677 bytes, 405 lines download raw bytes

1/**
2 * DrainMaster - Meta-Events vervollstaendigen (Browser-Teil)
3 *
4 * Gegenstueck zu dm-meta-events.php. Setzt an der Filterschnittstelle des
5 * Pixel Managers an (pmw.hooks), nicht am Pixel selbst - dadurch bleiben
6 * Deduplizierung, Einwilligungssteuerung und die uebrigen Kanaele unberuehrt.
7 *
8 * Vier Aufgaben:
9 *
10 *   1. external_id  Jeder Besucher bekommt eine eigene, zufaellige Kennung in
11 *                   einem First-Party-Keks. Sie geht an Browser-Pixel UND
12 *                   Conversions API und ist der groesste Hebel fuer die
13 *                   Uebereinstimmungsqualitaet der Trichter-Events
14 *                   (gemessene Abdeckung vorher: 12,5 %).
15 *   2. Menge        Der Konfigurator legt die Menge in ein eigenes Feld
16 *                   (drainmaster_quantity). Der Pixel Manager liest aber
17 *                   `.input-text.qty`, und das steht immer auf 1. Gemessen am
18 *                   26.08.2026: 6 Sets im Warenkorb, gemeldet 21,99 EUR.
19 *   3. contents     Einzelpreis und Menge je Position - ohne sie nimmt Meta
20 *                   den Katalogpreis an, der bei Staffelpreisen nie stimmt.
21 *   4. Warenkorb    Der Pixel Manager fuehrt einen eigenen Warenkorb im
22 *                   Speicher des Browsers. Lief die Menge einmal falsch hinein,
23 *                   bleibt er dauerhaft daneben - deshalb nach jeder Aenderung
24 *                   den echten Warenkorb vom Server nachladen.
25 *
26 * ⚠️ Der Keks wird NUR gesetzt, wenn tatsaechlich ein Ereignis den Filter
27 *    durchlaeuft. Der Pixel Manager laeuft mit `explicit_consent`, feuert also
28 *    erst nach Einwilligung - damit entsteht ohne Einwilligung auch keine
29 *    Kennung. Zusaetzlich wird der Borlabs-Keks gegengeprueft.
30 */
31( function () {
32	'use strict';
33
34	var K = ( window.dmMetaEvents || {} );
35	var KEKS = K.cookie || 'dm_eid';
36	var GRATIS = ( K.gratis || [] ).map( String );
37	var WAEHRUNG = K.waehrung || 'EUR';
38	var EIN_JAHR = 60 * 60 * 24 * 365;
39
40	// ---------------------------------------------------------------- Kekse
41
42	function keksLesen( name ) {
43		var treffer = document.cookie.match( '(^|;)\\s*' + name + '\\s*=\\s*([^;]+)' );
44		return treffer ? decodeURIComponent( treffer.pop() ) : null;
45	}
46
47	function keksSchreiben( name, wert, sekunden ) {
48		var domain = location.hostname.replace( /^www\./, '' );
49		document.cookie = name + '=' + encodeURIComponent( wert ) +
50			';path=/;max-age=' + sekunden +
51			';domain=.' + domain +
52			';SameSite=Lax' + ( location.protocol === 'https:' ? ';Secure' : '' );
53	}
54
55	/**
56	 * Liegt eine Einwilligung fuer Marketing vor?
57	 *
58	 * ⚠️ Fehlt der Borlabs-Keks ganz, wird KEINE Kennung angelegt. Lieber eine
59	 *    Luecke in der Messung als eine Kennung ohne Einwilligung.
60	 */
61	function marketingErlaubt() {
62		var roh = keksLesen( 'borlabs-cookie' );
63		if ( ! roh ) {
64			return false;
65		}
66		try {
67			var daten = JSON.parse( roh );
68			var zustimmungen = daten.consents || {};
69			return Object.prototype.hasOwnProperty.call( zustimmungen, 'marketing' );
70		} catch ( e ) {
71			return false;
72		}
73	}
74
75	/**
76	 * Die Besucherkennung holen, beim ersten Mal erzeugen.
77	 *
78	 * Bewusst eine rohe UUID: Meta hasht selbst, und Browser wie Server muessen
79	 * denselben Wert senden. Wer auf einer Seite hasht und auf der anderen
80	 * nicht, erzeugt zwei Kennungen fuer denselben Menschen - das ist
81	 * schlechter als gar keine.
82	 */
83	function kennung() {
84		var vorhanden = keksLesen( KEKS );
85		if ( vorhanden ) {
86			return vorhanden;
87		}
88		if ( ! marketingErlaubt() ) {
89			return null;
90		}
91
92		var neu;
93		if ( window.crypto && typeof window.crypto.randomUUID === 'function' ) {
94			neu = window.crypto.randomUUID();
95		} else {
96			// Rueckfall fuer aeltere Browser - dieselbe Form, damit die
97			// serverseitige Formatpruefung greift.
98			neu = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, function ( c ) {
99				var r = ( Math.random() * 16 ) | 0;
100				return ( c === 'x' ? r : ( ( r & 0x3 ) | 0x8 ) ).toString( 16 );
101			} );
102		}
103
104		keksSchreiben( KEKS, neu, EIN_JAHR );
105		return neu;
106	}
107
108	// ------------------------------------------------------------ Konfigurator
109
110	/** Die im Konfigurator gewaehlte Menge, oder null wenn keiner da ist. */
111	function konfiguratorMenge() {
112		var feld = document.getElementById( 'drainmaster-quantity-input' );
113		if ( ! feld ) {
114			return null;
115		}
116		var menge = parseInt( feld.value, 10 );
117		return ( isNaN( menge ) || menge < 1 ) ? null : menge;
118	}
119
120	/**
121	 * Der Gesamtpreis der aktuellen Auswahl.
122	 *
123	 * Erste Quelle ist der Zustand, den der Konfigurator selbst bereitstellt
124	 * (dort steht der per AJAX gerechnete Preis). Faellt der aus, wird die
125	 * aktive Kachel gelesen - sie traegt den fertigen Betrag.
126	 */
127	function konfiguratorPreis( menge ) {
128		var zustand = window.dmKonfigurator;
129		if ( zustand && zustand.menge === menge && zustand.gesamtpreis > 0 ) {
130			return zustand.gesamtpreis;
131		}
132
133		var kachel = document.querySelector( '.dm-qty-tile.active .dm-qty-tile__price' );
134		if ( kachel ) {
135			var betrag = betragLesen( kachel.textContent );
136			if ( betrag > 0 ) {
137				return betrag;
138			}
139		}
140
141		return null;
142	}
143
144	/** „118,45 €" -> 118.45 ; „1.234,50 €" -> 1234.50 */
145	function betragLesen( text ) {
146		if ( ! text ) {
147			return 0;
148		}
149		var sauber = String( text ).replace( /[^0-9.,]/g, '' ).replace( /\.(?=\d{3}(\D|$))/g, '' ).replace( ',', '.' );
150		var zahl = parseFloat( sauber );
151		return isNaN( zahl ) ? 0 : zahl;
152	}
153
154	// ------------------------------------------------------------- Hilfsmittel
155
156	function istGratis( id ) {
157		return GRATIS.indexOf( String( id ) ) !== -1;
158	}
159
160	/**
161	 * contents-Liste des Warenkorbs.
162	 *
163	 * Erste und beste Quelle ist der Server: dm-meta-events.php legt den
164	 * echten Warenkorb als `window.dmWarenkorb` bei, bereits nach der
165	 * Set-Regel abgebildet.
166	 *
167	 * ⚠️ Der Warenkorb des Pixel Managers ist NUR die Rueckfallebene. Er wird
168	 *    im Browser fortgeschrieben und driftet ab, sobald eine Menge einmal
169	 *    falsch hineingelaufen ist - was beim Konfigurator jahrelang der Fall
170	 *    war (gemeldet 21,99 EUR bei 341,77 EUR im Korb).
171	 */
172	function contentsAusWarenkorb() {
173		if ( Array.isArray( window.dmWarenkorb ) && window.dmWarenkorb.length ) {
174			return window.dmWarenkorb;
175		}
176
177		var warenkorb = ( window.pmwDataLayer && window.pmwDataLayer.cart ) || {};
178		var contents = [];
179
180		Object.keys( warenkorb ).forEach( function ( id ) {
181			var p = warenkorb[ id ];
182			if ( ! p || istGratis( p.dyn_r_ids && p.dyn_r_ids.dm_de_post_id ? p.dyn_r_ids.dm_de_post_id : id ) ) {
183				return;
184			}
185			var preis = parseFloat( p.price );
186			var menge = parseInt( p.quantity, 10 ) || 1;
187			if ( ! preis || preis < 0.5 ) {
188				return;
189			}
190			contents.push( {
191				id: String( metaId( p, id ) ),
192				quantity: menge,
193				item_price: Math.round( preis * 100 ) / 100
194			} );
195		} );
196
197		return contents;
198	}
199
200	/** Die ID, die Meta kennt - dieselbe Auswahl wie im Pixel Manager. */
201	function metaId( produkt, rueckfall ) {
202		var typ = ( window.pmwDataLayer && window.pmwDataLayer.pixels && window.pmwDataLayer.pixels.facebook &&
203			window.pmwDataLayer.pixels.facebook.dynamic_remarketing &&
204			window.pmwDataLayer.pixels.facebook.dynamic_remarketing.id_type ) || null;
205
206		if ( typ && produkt && produkt.dyn_r_ids && produkt.dyn_r_ids[ typ ] ) {
207			return produkt.dyn_r_ids[ typ ];
208		}
209		return rueckfall;
210	}
211
212	function warenkorbWert() {
213		return contentsAusWarenkorb().reduce( function ( summe, p ) {
214			return summe + p.item_price * p.quantity;
215		}, 0 );
216	}
217
218	// ------------------------------------------------------------- Filter
219
220	function filterEinhaengen() {
221		var pmw = window.pmw;
222
223		/**
224		 * Stufe 1: die Menge des Konfigurators in das Ereignis schreiben,
225		 * bevor die einzelnen Kanaele daraus ihre Nutzlast bauen.
226		 *
227		 * Damit stimmt der Wert in EINEM Zug fuer Meta, GA4 und Google Ads -
228		 * eine Korrektur nur am Meta-Adapter haette die anderen Kanaele
229		 * weiterhin mit 21,99 EUR laufen lassen.
230		 */
231		pmw.hooks.addFilter( 'pmw_event_payload_pre', 'drainmaster', function ( payload, ereignis ) {
232			try {
233				if ( ereignis !== 'add_to_cart' && ereignis !== 'view_item' ) {
234					return payload;
235				}
236
237				var produkt = payload && payload.event_data && payload.event_data.product;
238				if ( ! produkt ) {
239					return payload;
240				}
241
242				var menge = konfiguratorMenge();
243				if ( ! menge || menge < 2 ) {
244					return payload;
245				}
246
247				var gesamt = konfiguratorPreis( menge );
248
249				produkt.quantity = menge;
250				if ( gesamt ) {
251					// Der Pixel Manager rechnet Menge x Preis, also gehoert
252					// hier der Stueckpreis der gewaehlten Staffel hinein.
253					produkt.price = Math.round( ( gesamt / menge ) * 100 ) / 100;
254				}
255			} catch ( e ) {
256				console.error( 'dm-meta-events: Mengenkorrektur fehlgeschlagen', e );
257			}
258			return payload;
259		}, 10 );
260
261		/**
262		 * Stufe 2: die fertige Meta-Nutzlast vervollstaendigen.
263		 */
264		pmw.hooks.addFilter( 'pmw_pixel_data_facebook', 'drainmaster', function ( daten, ereignis ) {
265			try {
266				if ( ! daten ) {
267					return daten;
268				}
269
270				// -- Besucherkennung an JEDES Ereignis
271				var eid = kennung();
272				if ( eid ) {
273					daten.user_data = daten.user_data || {};
274					if ( ! daten.user_data.external_id ) {
275						daten.user_data.external_id = eid;
276					}
277				}
278
279				daten.custom_data = daten.custom_data || {};
280
281				// -- Alle Warenkorb-Ereignisse aus dem ECHTEN Warenkorb speisen.
282				//
283				//    „Zahlungsdaten hinzufuegen" trug bisher gar nichts
284				//    (custom_data war leer), und „Kaufvorgang starten" nahm den
285				//    abgedrifteten Warenkorb des Pixel Managers.
286				if ( ereignis === 'add_payment_info' || ereignis === 'begin_checkout' || ereignis === 'view_cart' ) {
287					var contents = contentsAusWarenkorb();
288					if ( contents.length ) {
289						daten.custom_data.content_type = 'product';
290						daten.custom_data.contents = contents;
291						daten.custom_data.content_ids = contents.map( function ( p ) { return p.id; } );
292						daten.custom_data.value = Math.round( warenkorbWert() * 100 ) / 100;
293						daten.custom_data.currency = WAEHRUNG;
294						daten.custom_data.num_items = contents.reduce( function ( s, p ) { return s + p.quantity; }, 0 );
295					}
296					return daten;
297				}
298
299				// -- Gratis-Zugaben gehoeren nicht in die Produktliste:
300				//    sie stehen zu Recht nicht im Katalog und druecken sonst
301				//    die Uebereinstimmungsquote (gemessen 72,9 %).
302				if ( Array.isArray( daten.custom_data.content_ids ) ) {
303					var gefiltert = daten.custom_data.content_ids.filter( function ( id ) {
304						return ! istGratis( id );
305					} );
306					if ( gefiltert.length ) {
307						daten.custom_data.content_ids = gefiltert;
308					}
309				}
310
311				// -- Einzelpreise ergaenzen, wo sie fehlen
312				if ( ! daten.custom_data.contents && daten.custom_data.content_ids ) {
313					daten.custom_data.contents = contentsFuerEreignis( daten, ereignis );
314				}
315			} catch ( e ) {
316				console.error( 'dm-meta-events: Anreicherung fehlgeschlagen', e );
317			}
318			return daten;
319		}, 10 );
320	}
321
322	/**
323	 * contents fuer ein einzelnes Ereignis bilden.
324	 *
325	 * Warenkorb- und Kassen-Ereignisse nehmen den Warenkorb, Produktaufrufe
326	 * den Wert des Ereignisses selbst.
327	 */
328	function contentsFuerEreignis( daten, ereignis ) {
329		if ( ereignis === 'begin_checkout' || ereignis === 'view_cart' ) {
330			var ausWarenkorb = contentsAusWarenkorb();
331			if ( ausWarenkorb.length ) {
332				return ausWarenkorb;
333			}
334		}
335
336		var ids = daten.custom_data.content_ids;
337		var wert = parseFloat( daten.custom_data.value );
338
339		if ( ! Array.isArray( ids ) || ! ids.length || ! wert ) {
340			return undefined;
341		}
342
343		var menge = konfiguratorMenge();
344		if ( ids.length === 1 && menge && menge > 1 ) {
345			return [ {
346				id: String( ids[ 0 ] ),
347				quantity: menge,
348				item_price: Math.round( ( wert / menge ) * 100 ) / 100
349			} ];
350		}
351
352		var jeStueck = Math.round( ( wert / ids.length ) * 100 ) / 100;
353		return ids.map( function ( id ) {
354			return { id: String( id ), quantity: 1, item_price: jeStueck };
355		} );
356	}
357
358	/**
359	 * Nach jeder Warenkorbaenderung den echten Warenkorb nachladen.
360	 *
361	 * ⚠️ Der Pixel Manager rechnet seinen Warenkorb sonst fort
362	 *    (cart[id].quantity += menge). Eine einmal falsch gezaehlte Menge
363	 *    bleibt damit fuer die ganze Sitzung falsch - und faellt beim
364	 *    „Kaufvorgang starten" niemandem auf, weil dort kein Fehler entsteht,
365	 *    sondern nur ein zu kleiner Betrag.
366	 */
367	function warenkorbNachladen() {
368		if ( window.pmw && typeof window.pmw.getCartItemsFromBackend === 'function' ) {
369			try {
370				window.pmw.getCartItemsFromBackend();
371			} catch ( e ) {
372				console.error( 'dm-meta-events: Warenkorb nachladen fehlgeschlagen', e );
373			}
374		}
375	}
376
377	// --------------------------------------------------------------- Start
378
379	/**
380	 * Warten, bis die Filterschnittstelle steht.
381	 *
382	 * Der Pixel Manager laedt seine Bausteine nach, deshalb reicht
383	 * DOMContentLoaded nicht. Es wird kurz gepollt statt auf ein Ereignis
384	 * gewartet, damit ein umbenanntes Ereignis die Anreicherung nicht wieder
385	 * still ausfallen laesst - genau so ist die Konfigurator-Anbindung
386	 * jahrelang unbemerkt tot gewesen.
387	 */
388	var versuche = 0;
389	var takt = setInterval( function () {
390		versuche++;
391		if ( window.pmw && window.pmw.hooks && typeof window.pmw.hooks.addFilter === 'function' ) {
392			clearInterval( takt );
393			filterEinhaengen();
394
395			if ( window.jQuery ) {
396				window.jQuery( document.body ).on( 'added_to_cart removed_from_cart updated_cart_totals', function () {
397					setTimeout( warenkorbNachladen, 300 );
398				} );
399			}
400		} else if ( versuche > 200 ) {
401			clearInterval( takt );
402			console.warn( 'dm-meta-events: pmw.hooks nicht gefunden - Anreicherung laeuft NICHT' );
403		}
404	}, 50 );
405} )();

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.