Skip to content

Commit c1318e9

Browse files
authored
WURFL RTD: Remove fingerprinting APIs from LCE detection (prebid#14251)
* WURFL RTD: send beacon on no bids * WURFL RTD: allow to not apply abtest to LCE bids (#4) * WURFL RTD: remove fingerprinting APIs from LCE detection Remove screen.availWidth usage from LCE device detection to comply with fingerprinting API restrictions. * WURFL RTD: remove setting of `device.w` and `device.h` in LCE enrichment LCE enrichment no longer sets `device.w` and `device.h` as these fields are already populated by Prebid.
1 parent 78a27e7 commit c1318e9

File tree

3 files changed

+606
-99
lines changed

3 files changed

+606
-99
lines changed

modules/wurflRtdProvider.js

Lines changed: 144 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { getGlobal } from '../src/prebidGlobal.js';
1313
// Constants
1414
const REAL_TIME_MODULE = 'realTimeData';
1515
const 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
1919
const 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

8283
const 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)
106107
let 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 ====================

modules/wurflRtdProvider.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,16 @@ pbjs.setConfig({
4949

5050
### Parameters
5151

52-
| Name | Type | Description | Default |
53-
| :------------- | :------ | :--------------------------------------------------------------- | :------------- |
54-
| name | String | Real time data module name | Always 'wurfl' |
55-
| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` |
56-
| params | Object | | |
57-
| params.altHost | String | Alternate host to connect to WURFL.js | |
58-
| params.abTest | Boolean | Enable A/B testing mode | `false` |
59-
| params.abName | String | A/B test name identifier | `'unknown'` |
60-
| params.abSplit | Number | Fraction of users in treatment group (0-1) | `0.5` |
52+
| Name | Type | Description | Default |
53+
| :------------------ | :------ | :--------------------------------------------------------------- | :------------- |
54+
| name | String | Real time data module name | Always 'wurfl' |
55+
| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` |
56+
| params | Object | | |
57+
| params.altHost | String | Alternate host to connect to WURFL.js | |
58+
| params.abTest | Boolean | Enable A/B testing mode | `false` |
59+
| params.abName | String | A/B test name identifier | `'unknown'` |
60+
| params.abSplit | Number | Fraction of users in treatment group (0-1) | `0.5` |
61+
| params.abExcludeLCE | Boolean | Don't apply A/B testing to LCE bids | `true` |
6162

6263
### A/B Testing
6364

0 commit comments

Comments
 (0)