@@ -13,7 +13,7 @@ import { getGlobal } from '../src/prebidGlobal.js';
1313// Constants
1414const REAL_TIME_MODULE = 'realTimeData' ;
1515const MODULE_NAME = 'wurfl' ;
16- const MODULE_VERSION = '2.1 .0' ;
16+ const MODULE_VERSION = '2.3 .0' ;
1717
1818// WURFL_JS_HOST is the host for the WURFL service endpoints
1919const WURFL_JS_HOST = 'https://prebid.wurflcloud.com' ;
@@ -53,8 +53,7 @@ const ENRICHMENT_TYPE = {
5353 LCE : 'lce' ,
5454 LCE_ERROR : 'lcefailed' ,
5555 WURFL_PUB : 'wurfl_pub' ,
56- WURFL_SSP : 'wurfl_ssp' ,
57- WURFL_PUB_SSP : 'wurfl_pub_ssp'
56+ WURFL_SSP : 'wurfl_ssp'
5857} ;
5958
6059// Consent class constants
@@ -76,7 +75,9 @@ const AB_TEST = {
7675 CONTROL_GROUP : 'control' ,
7776 TREATMENT_GROUP : 'treatment' ,
7877 DEFAULT_SPLIT : 0.5 ,
79- DEFAULT_NAME : 'unknown'
78+ DEFAULT_NAME : 'unknown' ,
79+ ENRICHMENT_TYPE_LCE : 'lce' ,
80+ ENRICHMENT_TYPE_WURFL : 'wurfl'
8081} ;
8182
8283const logger = prefixLog ( '[WURFL RTD Submodule]' ) ;
@@ -105,9 +106,6 @@ let tier;
105106// overQuota stores the over_quota flag from wurfl_pbjs data (possible values: 0, 1)
106107let overQuota ;
107108
108- // abTest stores A/B test configuration and variant (set by init)
109- let abTest ;
110-
111109/**
112110 * Safely gets an object from localStorage with JSON parsing
113111 * @param {string } key The storage key
@@ -322,21 +320,6 @@ function shouldSample(rate) {
322320 return randomValue < rate ;
323321}
324322
325- /**
326- * getABVariant determines A/B test variant assignment based on split
327- * @param {number } split Treatment group split from 0-1 (float, e.g., 0.5 = 50% treatment)
328- * @returns {string } AB_TEST.TREATMENT_GROUP or AB_TEST.CONTROL_GROUP
329- */
330- function getABVariant ( split ) {
331- if ( split >= 1 ) {
332- return AB_TEST . TREATMENT_GROUP ;
333- }
334- if ( split <= 0 ) {
335- return AB_TEST . CONTROL_GROUP ;
336- }
337- return Math . random ( ) < split ? AB_TEST . TREATMENT_GROUP : AB_TEST . CONTROL_GROUP ;
338- }
339-
340323/**
341324 * getConsentClass calculates the consent classification level
342325 * @param {Object } userConsent User consent data
@@ -942,26 +925,9 @@ const WurflLCEDevice = {
942925 return window . screen . deviceXDPI / window . screen . logicalXDPI ;
943926 }
944927
945- const screenWidth = window . screen . availWidth ;
946- const docWidth = window . document ?. documentElement ?. clientWidth ;
947-
948- if ( screenWidth && docWidth && docWidth > 0 ) {
949- return Math . round ( screenWidth / docWidth ) ;
950- }
951-
952928 return undefined ;
953929 } ,
954930
955- _getScreenWidth ( pixelRatio ) {
956- // Assumes window.screen exists (caller checked)
957- return Math . round ( window . screen . width * pixelRatio ) ;
958- } ,
959-
960- _getScreenHeight ( pixelRatio ) {
961- // Assumes window.screen exists (caller checked)
962- return Math . round ( window . screen . height * pixelRatio ) ;
963- } ,
964-
965931 _getMake ( ua ) {
966932 for ( const [ makeToken , brandName ] of this . _makeMapping ) {
967933 if ( ua . includes ( makeToken ) ) {
@@ -1039,16 +1005,6 @@ const WurflLCEDevice = {
10391005 const pixelRatio = this . _getDevicePixelRatioValue ( ) ;
10401006 if ( pixelRatio !== undefined ) {
10411007 device . pxratio = pixelRatio ;
1042-
1043- const width = this . _getScreenWidth ( pixelRatio ) ;
1044- if ( width !== undefined ) {
1045- device . w = width ;
1046- }
1047-
1048- const height = this . _getScreenHeight ( pixelRatio ) ;
1049- if ( height !== undefined ) {
1050- device . h = height ;
1051- }
10521008 }
10531009 }
10541010
@@ -1066,6 +1022,105 @@ const WurflLCEDevice = {
10661022} ;
10671023// ==================== END WURFL LCE DEVICE MODULE ====================
10681024
1025+ // ==================== A/B TEST MANAGER ====================
1026+
1027+ const ABTestManager = {
1028+ _enabled : false ,
1029+ _name : null ,
1030+ _variant : null ,
1031+ _excludeLCE : true ,
1032+ _enrichmentType : null ,
1033+
1034+ /**
1035+ * Initializes A/B test configuration
1036+ * @param {Object } params Configuration params from config.params
1037+ */
1038+ init ( params ) {
1039+ this . _enabled = false ;
1040+ this . _name = null ;
1041+ this . _variant = null ;
1042+ this . _excludeLCE = true ;
1043+ this . _enrichmentType = null ;
1044+
1045+ const abTestEnabled = params ?. abTest ?? false ;
1046+ if ( ! abTestEnabled ) {
1047+ return ;
1048+ }
1049+
1050+ this . _enabled = true ;
1051+ this . _name = params ?. abName ?? AB_TEST . DEFAULT_NAME ;
1052+ this . _excludeLCE = params ?. abExcludeLCE ?? true ;
1053+
1054+ const split = params ?. abSplit ?? AB_TEST . DEFAULT_SPLIT ;
1055+ this . _variant = this . _computeVariant ( split ) ;
1056+
1057+ logger . logMessage ( `A/B test "${ this . _name } ": user in ${ this . _variant } group (exclude_lce: ${ this . _excludeLCE } )` ) ;
1058+ } ,
1059+
1060+ /**
1061+ * _computeVariant determines A/B test variant assignment based on split
1062+ * @param {number } split Treatment group split from 0-1 (float, e.g., 0.5 = 50% treatment)
1063+ * @returns {string } AB_TEST.TREATMENT_GROUP or AB_TEST.CONTROL_GROUP
1064+ */
1065+ _computeVariant ( split ) {
1066+ if ( split >= 1 ) {
1067+ return AB_TEST . TREATMENT_GROUP ;
1068+ }
1069+ if ( split <= 0 ) {
1070+ return AB_TEST . CONTROL_GROUP ;
1071+ }
1072+ return Math . random ( ) < split ? AB_TEST . TREATMENT_GROUP : AB_TEST . CONTROL_GROUP ;
1073+ } ,
1074+
1075+ /**
1076+ * Sets the enrichment type encountered in current auction
1077+ * @param {string } enrichmentType 'lce' or 'wurfl'
1078+ */
1079+ setEnrichmentType ( enrichmentType ) {
1080+ this . _enrichmentType = enrichmentType ;
1081+ } ,
1082+
1083+ /**
1084+ * Checks if A/B test is enabled for current auction
1085+ * @returns {boolean } True if A/B test should be applied
1086+ */
1087+ isEnabled ( ) {
1088+ if ( ! this . _enabled ) return false ;
1089+ if ( this . _enrichmentType === AB_TEST . ENRICHMENT_TYPE_LCE && this . _excludeLCE ) {
1090+ return false ;
1091+ }
1092+ return true ;
1093+ } ,
1094+
1095+ /**
1096+ * Checks if enrichment should be skipped (control group)
1097+ * @returns {boolean } True if enrichment should be skipped
1098+ */
1099+ isInControlGroup ( ) {
1100+ if ( ! this . isEnabled ( ) ) {
1101+ return false ;
1102+ }
1103+ return ( this . _variant === AB_TEST . CONTROL_GROUP )
1104+ } ,
1105+
1106+ /**
1107+ * Gets beacon payload fields (returns null if not active for auction)
1108+ * @returns {Object|null }
1109+ */
1110+ getBeaconPayload ( ) {
1111+ if ( ! this . isEnabled ( ) ) {
1112+ return null ;
1113+ }
1114+
1115+ return {
1116+ ab_name : this . _name ,
1117+ ab_variant : this . _variant
1118+ } ;
1119+ }
1120+ } ;
1121+
1122+ // ==================== END A/B TEST MANAGER MODULE ====================
1123+
10691124// ==================== EXPORTED FUNCTIONS ====================
10701125
10711126/**
@@ -1084,22 +1139,12 @@ const init = (config, userConsent) => {
10841139 samplingRate = DEFAULT_SAMPLING_RATE ;
10851140 tier = '' ;
10861141 overQuota = DEFAULT_OVER_QUOTA ;
1087- abTest = null ;
1088-
1089- // A/B testing: set if enabled
1090- const abTestEnabled = config ?. params ?. abTest ?? false ;
1091- if ( abTestEnabled ) {
1092- const abName = config ?. params ?. abName ?? AB_TEST . DEFAULT_NAME ;
1093- const abSplit = config ?. params ?. abSplit ?? AB_TEST . DEFAULT_SPLIT ;
1094- const abVariant = getABVariant ( abSplit ) ;
1095- abTest = { ab_name : abName , ab_variant : abVariant } ;
1096- logger . logMessage ( `A/B test "${ abName } ": user in ${ abVariant } group` ) ;
1097- }
10981142
1099- logger . logMessage ( 'initialized' , {
1100- version : MODULE_VERSION ,
1101- abTest : abTest ? `${ abTest . ab_name } :${ abTest . ab_variant } ` : 'disabled'
1102- } ) ;
1143+ logger . logMessage ( 'initialized' , { version : MODULE_VERSION } ) ;
1144+
1145+ // A/B testing: initialize ABTestManager
1146+ ABTestManager . init ( config ?. params ) ;
1147+
11031148 return true ;
11041149}
11051150
@@ -1123,8 +1168,16 @@ const getBidRequestData = (reqBidsConfigObj, callback, config, userConsent) => {
11231168 } ) ;
11241169 } ) ;
11251170
1126- // A/B test: Skip enrichment for control group but allow beacon sending
1127- if ( abTest && abTest . ab_variant === AB_TEST . CONTROL_GROUP ) {
1171+ // Determine enrichment type based on cache availability
1172+ WurflDebugger . cacheReadStart ( ) ;
1173+ const cachedWurflData = getObjectFromStorage ( WURFL_RTD_STORAGE_KEY ) ;
1174+ WurflDebugger . cacheReadStop ( ) ;
1175+
1176+ const abEnrichmentType = cachedWurflData ? AB_TEST . ENRICHMENT_TYPE_WURFL : AB_TEST . ENRICHMENT_TYPE_LCE ;
1177+ ABTestManager . setEnrichmentType ( abEnrichmentType ) ;
1178+
1179+ // A/B test: Skip enrichment for control group
1180+ if ( ABTestManager . isInControlGroup ( ) ) {
11281181 logger . logMessage ( 'A/B test control group: skipping enrichment' ) ;
11291182 enrichmentType = ENRICHMENT_TYPE . NONE ;
11301183 bidders . forEach ( bidder => bidderEnrichment . set ( bidder , ENRICHMENT_TYPE . NONE ) ) ;
@@ -1134,10 +1187,6 @@ const getBidRequestData = (reqBidsConfigObj, callback, config, userConsent) => {
11341187 }
11351188
11361189 // Priority 1: Check if WURFL.js response is cached
1137- WurflDebugger . cacheReadStart ( ) ;
1138- const cachedWurflData = getObjectFromStorage ( WURFL_RTD_STORAGE_KEY ) ;
1139- WurflDebugger . cacheReadStop ( ) ;
1140-
11411190 if ( cachedWurflData ) {
11421191 const isExpired = cachedWurflData . expire_at && Date . now ( ) > cachedWurflData . expire_at ;
11431192
@@ -1248,8 +1297,14 @@ function onAuctionEndEvent(auctionDetails, config, userConsent) {
12481297 host = statsHost ;
12491298 }
12501299
1251- const url = new URL ( host ) ;
1252- url . pathname = STATS_ENDPOINT_PATH ;
1300+ let url ;
1301+ try {
1302+ url = new URL ( host ) ;
1303+ url . pathname = STATS_ENDPOINT_PATH ;
1304+ } catch ( e ) {
1305+ logger . logError ( 'Invalid stats host URL:' , host ) ;
1306+ return ;
1307+ }
12531308
12541309 // Calculate consent class
12551310 let consentClass ;
@@ -1261,12 +1316,6 @@ function onAuctionEndEvent(auctionDetails, config, userConsent) {
12611316 consentClass = CONSENT_CLASS . ERROR ;
12621317 }
12631318
1264- // Only send beacon if there are bids to report
1265- if ( ! auctionDetails . bidsReceived || auctionDetails . bidsReceived . length === 0 ) {
1266- logger . logMessage ( 'auction completed - no bids received' ) ;
1267- return ;
1268- }
1269-
12701319 // Build a lookup object for winning bid request IDs
12711320 const winningBids = getGlobal ( ) . getHighestCpmBids ( ) || [ ] ;
12721321 const winningBidIds = { } ;
@@ -1277,8 +1326,9 @@ function onAuctionEndEvent(auctionDetails, config, userConsent) {
12771326
12781327 // Build a lookup object for bid responses: "adUnitCode:bidderCode" -> bid
12791328 const bidResponseMap = { } ;
1280- for ( let i = 0 ; i < auctionDetails . bidsReceived . length ; i ++ ) {
1281- const bid = auctionDetails . bidsReceived [ i ] ;
1329+ const bidsReceived = auctionDetails . bidsReceived || [ ] ;
1330+ for ( let i = 0 ; i < bidsReceived . length ; i ++ ) {
1331+ const bid = bidsReceived [ i ] ;
12821332 const adUnitCode = bid . adUnitCode ;
12831333 const bidderCode = bid . bidderCode || bid . bidder ;
12841334 const key = adUnitCode + ':' + bidderCode ;
@@ -1295,8 +1345,9 @@ function onAuctionEndEvent(auctionDetails, config, userConsent) {
12951345 const bidders = [ ] ;
12961346
12971347 // Check each bidder configured for this ad unit
1298- for ( let j = 0 ; j < adUnit . bids . length ; j ++ ) {
1299- const bidConfig = adUnit . bids [ j ] ;
1348+ const bids = adUnit . bids || [ ] ;
1349+ for ( let j = 0 ; j < bids . length ; j ++ ) {
1350+ const bidConfig = bids [ j ] ;
13001351 const bidderCode = bidConfig . bidder ;
13011352 const key = adUnitCode + ':' + bidderCode ;
13021353 const bidResponse = bidResponseMap [ key ] ;
@@ -1329,7 +1380,7 @@ function onAuctionEndEvent(auctionDetails, config, userConsent) {
13291380 }
13301381
13311382 logger . logMessage ( 'auction completed' , {
1332- bidsReceived : auctionDetails . bidsReceived . length ,
1383+ bidsReceived : auctionDetails . bidsReceived ? auctionDetails . bidsReceived . length : 0 ,
13331384 bidsWon : winningBids . length ,
13341385 adUnits : adUnits . length
13351386 } ) ;
@@ -1349,16 +1400,19 @@ function onAuctionEndEvent(auctionDetails, config, userConsent) {
13491400 } ;
13501401
13511402 // Add A/B test fields if enabled
1352- if ( abTest ) {
1353- payloadData . ab_name = abTest . ab_name ;
1354- payloadData . ab_variant = abTest . ab_variant ;
1403+ const abPayload = ABTestManager . getBeaconPayload ( ) ;
1404+ if ( abPayload ) {
1405+ payloadData . ab_name = abPayload . ab_name ;
1406+ payloadData . ab_variant = abPayload . ab_variant ;
13551407 }
13561408
13571409 const payload = JSON . stringify ( payloadData ) ;
13581410
1411+ // Both sendBeacon and fetch send as text/plain to avoid CORS preflight requests.
1412+ // Server must parse body as JSON regardless of Content-Type header.
13591413 const sentBeacon = sendBeacon ( url . toString ( ) , payload ) ;
13601414 if ( sentBeacon ) {
1361- WurflDebugger . setBeaconPayload ( JSON . parse ( payload ) ) ;
1415+ WurflDebugger . setBeaconPayload ( payloadData ) ;
13621416 return ;
13631417 }
13641418
@@ -1367,9 +1421,11 @@ function onAuctionEndEvent(auctionDetails, config, userConsent) {
13671421 body : payload ,
13681422 mode : 'no-cors' ,
13691423 keepalive : true
1424+ } ) . catch ( ( e ) => {
1425+ logger . logError ( 'Failed to send beacon via fetch:' , e ) ;
13701426 } ) ;
13711427
1372- WurflDebugger . setBeaconPayload ( JSON . parse ( payload ) ) ;
1428+ WurflDebugger . setBeaconPayload ( payloadData ) ;
13731429}
13741430
13751431// ==================== MODULE EXPORT ====================
0 commit comments