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.