From cf627a16ab2bfb35b56ad7e18d17dd94c44e3f26 Mon Sep 17 00:00:00 2001 From: ym-ainiguez Date: Wed, 3 Jun 2026 10:53:40 -0700 Subject: [PATCH 1/4] FS-12403: add bcat/badv support for banner + fix video merge - Add mergeBlocklist() helper: union ortb2. + params., dedupe, filter invalid/non-string entries with logWarn, never drop the bid. - Banner (non-video) path now sends bcat/badv as comma-delimited params (per the AS-5349 ad-server endpoint); omitted when empty; never subject to URL-length trimming. - Video path reads ortb2.bcat/ortb2.badv (was the dead bidderRequest.bcat path) and merges with params instead of || precedence. - Remove bcat/badv checks from validateVideoParams (now normalized in mergeBlocklist; no longer bid-dropping, applies to all media types). - Add unit tests and yieldmoBidAdapter.md docs for both params. Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/yieldmoBidAdapter.js | 51 ++++++++++-- modules/yieldmoBidAdapter.md | 21 ++++- test/spec/modules/yieldmoBidAdapter_spec.js | 90 +++++++++++++++++++++ 3 files changed, 154 insertions(+), 8 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index f606194e132..361af85e4b1 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -12,6 +12,7 @@ import { isNumber, isStr, logError, + logWarn, parseQueryStringParameters, parseUrl } from '../src/utils.js'; @@ -151,6 +152,18 @@ export const spec = { if (eids.length) { serverRequest.eids = JSON.stringify(eids); }; + + // Blocklists (request-level): merge ortb2 + params, send as comma-delimited + // params per the ad server's prebid-js endpoint (AS-5349). Omitted when empty. + const bcat = mergeBlocklist(deepAccess(bidderRequest, 'ortb2.bcat'), deepAccess(bannerBidRequests[0], 'params.bcat')); + if (bcat.length) { + serverRequest.bcat = bcat.join(','); + } + const badv = mergeBlocklist(deepAccess(bidderRequest, 'ortb2.badv'), deepAccess(bannerBidRequests[0], 'params.badv')); + if (badv.length) { + serverRequest.badv = badv.join(','); + } + // check if url exceeded max length const fullUrl = `${bannerUrl}?${parseQueryStringParameters(serverRequest)}`; let extraCharacters = fullUrl.length - MAX_BANNER_REQUEST_URL_LENGTH; @@ -398,6 +411,36 @@ function getId(request, idType) { return (typeof deepAccess(request, 'userId') === 'object') ? request.userId[idType] : undefined; } +/** + * Merge a request-level blocklist field (bcat/badv) from its two sources — the + * standardized ORTB global (`ortb2.`) and the Yieldmo-specific param + * (`params.`) — into a single deduped array of strings. Neither source is + * allowed to silently win (union, not precedence). Invalid values are ignored and + * logged rather than dropping the bid: a source that isn't an array, and any + * non-string/empty element, are filtered out (with a warning) so a misconfiguration + * is surfaced without losing the impression. + * @param {*} ortb2Value value at bidderRequest.ortb2. + * @param {*} paramsValue value at bid.params. + * @return {string[]} deduped, trimmed, non-empty string entries (possibly empty) + */ +function mergeBlocklist(ortb2Value, paramsValue) { + const normalize = (value, source) => { + if (value === undefined || value === null) { + return []; + } + if (!isArray(value)) { + logWarn(`yieldmo: ignoring ${source} blocklist value; expected an array of strings, got ${JSON.stringify(value)}`); + return []; + } + const dropped = value.filter(item => !isStr(item) || !item.trim()); + if (dropped.length) { + logWarn(`yieldmo: ignoring invalid ${source} blocklist entries (expected non-empty strings): ${JSON.stringify(dropped)}`); + } + return value.filter(item => isStr(item) && item.trim()).map(item => item.trim()); + }; + return [...new Set([...normalize(ortb2Value, 'ortb2'), ...normalize(paramsValue, 'params')])]; +} + /** * @param {BidRequest[]} bidRequests bid request object * @param {BidderRequest} bidderRequest bidder request object @@ -412,8 +455,8 @@ function openRtbRequest(bidRequests, bidderRequest) { imp: bidRequests.map(bidRequest => openRtbImpression(bidRequest)), site: openRtbSite(bidRequests[0], bidderRequest), device: deepAccess(bidderRequest, 'ortb2.device'), - badv: bidRequests[0].params.badv || [], - bcat: deepAccess(bidderRequest, 'bcat') || bidRequests[0].params.bcat || [], + badv: mergeBlocklist(deepAccess(bidderRequest, 'ortb2.badv'), deepAccess(bidRequests[0], 'params.badv')), + bcat: mergeBlocklist(deepAccess(bidderRequest, 'ortb2.bcat'), deepAccess(bidRequests[0], 'params.bcat')), ext: { prebid: '$prebid.version$', }, @@ -680,10 +723,6 @@ function validateVideoParams(bid) { validate('video.skippable', val => !isDefined(val) || isBoolean(val), paramInvalid); validate('video.skipafter', val => !isDefined(val) || isNumber(val), paramInvalid); validate('video.pos', val => !isDefined(val) || isNumber(val), paramInvalid); - validate('params.badv', val => !isDefined(val) || isArray(val), paramInvalid, - 'array of strings, ex: ["ford.com","pepsi.com"]'); - validate('params.bcat', val => !isDefined(val) || isArray(val), paramInvalid, - 'array of strings, ex: ["IAB1-5","IAB1-6"]'); return true; } catch (e) { logError(e.message); diff --git a/modules/yieldmoBidAdapter.md b/modules/yieldmoBidAdapter.md index c98e2ab5c74..8f1ea50b43a 100644 --- a/modules/yieldmoBidAdapter.md +++ b/modules/yieldmoBidAdapter.md @@ -31,7 +31,9 @@ var adUnits = [{ // Banner adUnit params: { placementId: '1779781193098233305', // string with at most 19 characters (may include numbers only) bidFloor: .28, // optional param - lr_env: '***' // Optional. Live Ramp ATS envelope + lr_env: '***', // Optional. Live Ramp ATS envelope + bcat: ['IAB1-5', 'IAB1-6'], // optional, array of blocked IAB content categories (strings) + badv: ['ford.com', 'pepsi.com'] // optional, array of blocked advertiser domains (strings) } }] }]; @@ -66,7 +68,9 @@ var adUnits = [{ // Video adUnit skippable: true, // optional, boolean skipafter: 10 // optional, integer }, - lr_env: '***' // Optional. Live Ramp ATS envelope + lr_env: '***', // Optional. Live Ramp ATS envelope + bcat: ['IAB1-5', 'IAB1-6'], // optional, array of blocked IAB content categories (strings) + badv: ['ford.com', 'pepsi.com'] // optional, array of blocked advertiser domains (strings) } }] }]; @@ -104,3 +108,16 @@ Please also note, that we support the following OpenRTB params: 'mimes', 'startdelay', 'placement', 'startdelay', 'skipafter', 'protocols', 'api', 'playbackmethod', 'maxduration', 'minduration', 'pos', 'skip', 'skippable'. They can be specified in `mediaTypes.video` or in `bids[].params.video`. + +# Blocklists (bcat / badv) + +`bcat` (blocked IAB content categories) and `badv` (blocked advertiser domains) are +supported for **both banner and video**. Each is an optional array of strings and can +be supplied from either source: + +* the standardized first-party-data global — `ortb2.bcat` / `ortb2.badv`, or +* the Yieldmo bid params — `bids[].params.bcat` / `bids[].params.badv`. + +When both sources are present they are **merged** (union) and de-duplicated — neither +source overrides the other. Invalid entries (non-strings, empty strings) and +non-array values are ignored with a console warning rather than dropping the bid. diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 4f4454c17ae..95a7ddd79a8 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -829,6 +829,96 @@ describe('YieldmoAdapter', function () { expect(payload.device.language).to.exist; }); }); + + describe('bcat / badv blocklists (FS-12403)', function () { + it('banner: sends merged bcat/badv as comma-delimited GET params', function () { + const bidderReq = mockBidderRequest({ ortb2: { bcat: ['IAB1-1'], badv: ['ortb.com'] } }); + const data = buildAndGetData([mockBannerBid({}, { bcat: ['IAB2-2'], badv: ['param.com'] })], 0, bidderReq); + expect(data.bcat).to.equal('IAB1-1,IAB2-2'); + expect(data.badv).to.equal('ortb.com,param.com'); + }); + + it('banner: unions ortb2 + params (neither source silently wins)', function () { + const bidderReq = mockBidderRequest({ ortb2: { bcat: ['A'] } }); + const data = buildAndGetData([mockBannerBid({}, { bcat: ['B'] })], 0, bidderReq); + expect(data.bcat.split(',')).to.have.members(['A', 'B']); + }); + + it('banner: dedupes values across the two sources, preserving order', function () { + const bidderReq = mockBidderRequest({ ortb2: { bcat: ['DUP', 'A'] } }); + const data = buildAndGetData([mockBannerBid({}, { bcat: ['DUP', 'B'] })], 0, bidderReq); + expect(data.bcat).to.equal('DUP,A,B'); + }); + + it('banner: reads from ortb2 alone', function () { + const bidderReq = mockBidderRequest({ ortb2: { bcat: ['IAB1-1'], badv: ['x.com'] } }); + const data = buildAndGetData([mockBannerBid()], 0, bidderReq); + expect(data.bcat).to.equal('IAB1-1'); + expect(data.badv).to.equal('x.com'); + }); + + it('banner: reads from params alone', function () { + const data = buildAndGetData([mockBannerBid({}, { bcat: ['IAB1-1'], badv: ['x.com'] })], 0, mockBidderRequest()); + expect(data.bcat).to.equal('IAB1-1'); + expect(data.badv).to.equal('x.com'); + }); + + it('banner: omits bcat/badv entirely when empty', function () { + const data = buildAndGetData([mockBannerBid()], 0, mockBidderRequest()); + expect(data).to.not.have.property('bcat'); + expect(data).to.not.have.property('badv'); + }); + + it('video: sends merged bcat/badv as deduped arrays', function () { + const bidderReq = mockBidderRequest({ ortb2: { bcat: ['IAB1-1'], badv: ['ortb.com'] } }, [mockVideoBid()]); + const payload = buildAndGetData([mockVideoBid({}, { bcat: ['IAB2-2'], badv: ['param.com'] })], 0, bidderReq); + expect(payload.bcat).to.deep.equal(['IAB1-1', 'IAB2-2']); + expect(payload.badv).to.deep.equal(['ortb.com', 'param.com']); + }); + + it('video: reads ortb2.bcat (not the legacy bidderRequest.bcat path)', function () { + const bidderReq = mockBidderRequest({ bcat: ['WRONG'], ortb2: { bcat: ['RIGHT'] } }, [mockVideoBid()]); + const payload = buildAndGetData([mockVideoBid()], 0, bidderReq); + expect(payload.bcat).to.deep.equal(['RIGHT']); + }); + + it('video: defaults to empty arrays when no blocklists are set', function () { + const payload = buildAndGetData([mockVideoBid()], 0, mockBidderRequest({}, [mockVideoBid()])); + expect(payload.bcat).to.deep.equal([]); + expect(payload.badv).to.deep.equal([]); + }); + + describe('malformed handling (filter + warn, never drop the bid)', function () { + let logWarnStub; + beforeEach(function () { logWarnStub = sinon.stub(utils, 'logWarn'); }); + afterEach(function () { logWarnStub.restore(); }); + + it('ignores a non-array source and warns', function () { + const bidderReq = mockBidderRequest({ ortb2: { bcat: 'IAB1-1' } }); + const data = buildAndGetData([mockBannerBid()], 0, bidderReq); + expect(data).to.not.have.property('bcat'); + expect(logWarnStub.called).to.be.true; + }); + + it('filters non-string / empty elements and warns', function () { + const data = buildAndGetData([mockBannerBid({}, { bcat: ['IAB1-1', '', 5, ' '] })], 0, mockBidderRequest()); + expect(data.bcat).to.equal('IAB1-1'); + expect(logWarnStub.called).to.be.true; + }); + + it('trims whitespace around entries', function () { + const data = buildAndGetData([mockBannerBid({}, { bcat: [' IAB1-1 '] })], 0, mockBidderRequest()); + expect(data.bcat).to.equal('IAB1-1'); + }); + + it('does not drop a banner or video bid when bcat/badv are malformed', function () { + logWarnStub.restore(); // isBidRequestValid doesn't warn; avoid unused-stub noise + expect(spec.isBidRequestValid(mockBannerBid({}, { bcat: 'oops' }))).to.be.true; + expect(spec.isBidRequestValid(mockVideoBid({}, { bcat: 'oops' }))).to.be.true; + logWarnStub = sinon.stub(utils, 'logWarn'); + }); + }); + }); }); describe('interpretResponse', function () { From c5e52ab6d5aeba34917b8b3f97f10db371b9acec Mon Sep 17 00:00:00 2001 From: ym-ainiguez Date: Wed, 3 Jun 2026 13:29:49 -0700 Subject: [PATCH 2/4] Address review: reject malformed bcat/badv, allow when missing Per review discussion (Aaron + Niko): keep brand-safety strict for malformed input while not penalizing absence. - Add validateBlocklistParams(), run for ALL bids (banner + video) via isBidRequestValid: a missing bcat/badv passes; a value that is present but not an array fails validation (drops the bid). - mergeBlocklist still merges + dedupes, and defensively filters the ortb2 source (not bid-validated) and non-string/empty array elements, with logWarn. - Update spec (missing-allowed / malformed-dropped cases) and the .md docs. Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/yieldmoBidAdapter.js | 24 +++++++++++++- modules/yieldmoBidAdapter.md | 8 +++-- test/spec/modules/yieldmoBidAdapter_spec.js | 35 +++++++++++++++------ 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 361af85e4b1..e43ade95c93 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -60,7 +60,7 @@ export const spec = { */ isBidRequestValid: function (bid) { return !!(bid && bid.adUnitCode && bid.bidId && (hasBannerMediaType(bid) || hasVideoMediaType(bid)) && - validateVideoParams(bid)); + validateVideoParams(bid) && validateBlocklistParams(bid)); }, /** @@ -730,6 +730,28 @@ function validateVideoParams(bid) { } } +/** + * Validate the publisher-set blocklist params (`bcat`/`badv`) for all media types + * (banner and video). A missing value is allowed; a value that is present but is + * not an array is rejected (drops the bid) — the agreed middle ground between + * dropping nothing and dropping over an absent optional field. See FS-12403. + * @param {BidRequest} bid bid request + * @return {boolean} true if valid (or absent), false if present but malformed + */ +function validateBlocklistParams(bid) { + return ['bcat', 'badv'].every(key => { + const val = deepAccess(bid, `params.${key}`); + if (val === undefined || val === null) { + return true; + } + if (!isArray(val)) { + logError(`yieldmo: bid.params.${key} must be an array of strings when provided; dropping bid`); + return false; + } + return true; + }); +} + /** * Shortcut object property and check if required characters count was deleted * diff --git a/modules/yieldmoBidAdapter.md b/modules/yieldmoBidAdapter.md index 8f1ea50b43a..135cd89c54f 100644 --- a/modules/yieldmoBidAdapter.md +++ b/modules/yieldmoBidAdapter.md @@ -119,5 +119,9 @@ be supplied from either source: * the Yieldmo bid params — `bids[].params.bcat` / `bids[].params.badv`. When both sources are present they are **merged** (union) and de-duplicated — neither -source overrides the other. Invalid entries (non-strings, empty strings) and -non-array values are ignored with a console warning rather than dropping the bid. +source overrides the other. + +Validation: a **missing** `bcat`/`badv` is always allowed, but if `params.bcat` / +`params.badv` is **present and not an array** the bid is rejected (`isBidRequestValid` +returns false). Non-string / empty elements inside an otherwise-valid array, and a +malformed `ortb2` value, are dropped with a console warning rather than failing the bid. diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 95a7ddd79a8..24dc7c4721b 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -148,6 +148,28 @@ describe('YieldmoAdapter', function () { expect(spec.isBidRequestValid(getBidAndExclude('api'))).to.be.false; }); }); + + describe('Blocklist params (bcat / badv):', function () { + it('allows a bid when bcat/badv are absent (missing is fine)', function () { + expect(spec.isBidRequestValid(mockBannerBid())).to.be.true; + expect(spec.isBidRequestValid(mockVideoBid())).to.be.true; + }); + + it('allows a bid when bcat/badv are arrays', function () { + expect(spec.isBidRequestValid(mockBannerBid({}, { bcat: ['IAB1-1'], badv: ['x.com'] }))).to.be.true; + expect(spec.isBidRequestValid(mockVideoBid({}, { bcat: ['IAB1-1'], badv: ['x.com'] }))).to.be.true; + }); + + it('drops a bid when bcat is present but not an array', function () { + expect(spec.isBidRequestValid(mockBannerBid({}, { bcat: 'IAB1-1' }))).to.be.false; + expect(spec.isBidRequestValid(mockVideoBid({}, { bcat: 'IAB1-1' }))).to.be.false; + }); + + it('drops a bid when badv is present but not an array', function () { + expect(spec.isBidRequestValid(mockBannerBid({}, { badv: 'ford.com' }))).to.be.false; + expect(spec.isBidRequestValid(mockVideoBid({}, { badv: 'ford.com' }))).to.be.false; + }); + }); }); describe('buildRequests', function () { @@ -888,19 +910,19 @@ describe('YieldmoAdapter', function () { expect(payload.badv).to.deep.equal([]); }); - describe('malformed handling (filter + warn, never drop the bid)', function () { + describe('blocklist normalization in mergeBlocklist', function () { let logWarnStub; beforeEach(function () { logWarnStub = sinon.stub(utils, 'logWarn'); }); afterEach(function () { logWarnStub.restore(); }); - it('ignores a non-array source and warns', function () { + it('ignores a non-array ortb2 source and warns (ortb2 is not bid-validated)', function () { const bidderReq = mockBidderRequest({ ortb2: { bcat: 'IAB1-1' } }); const data = buildAndGetData([mockBannerBid()], 0, bidderReq); expect(data).to.not.have.property('bcat'); expect(logWarnStub.called).to.be.true; }); - it('filters non-string / empty elements and warns', function () { + it('filters non-string / empty elements out of a valid array and warns', function () { const data = buildAndGetData([mockBannerBid({}, { bcat: ['IAB1-1', '', 5, ' '] })], 0, mockBidderRequest()); expect(data.bcat).to.equal('IAB1-1'); expect(logWarnStub.called).to.be.true; @@ -910,13 +932,6 @@ describe('YieldmoAdapter', function () { const data = buildAndGetData([mockBannerBid({}, { bcat: [' IAB1-1 '] })], 0, mockBidderRequest()); expect(data.bcat).to.equal('IAB1-1'); }); - - it('does not drop a banner or video bid when bcat/badv are malformed', function () { - logWarnStub.restore(); // isBidRequestValid doesn't warn; avoid unused-stub noise - expect(spec.isBidRequestValid(mockBannerBid({}, { bcat: 'oops' }))).to.be.true; - expect(spec.isBidRequestValid(mockVideoBid({}, { bcat: 'oops' }))).to.be.true; - logWarnStub = sinon.stub(utils, 'logWarn'); - }); }); }); }); From 8b6eedef2d4d63defbe977d4b35aafc24c39bee8 Mon Sep 17 00:00:00 2001 From: ym-ainiguez Date: Wed, 3 Jun 2026 13:48:31 -0700 Subject: [PATCH 3/4] Refactor: lift shared param validators to module scope Move isDefined / paramRequired / paramInvalid and the field validator out of validateVideoParams to module scope (createParamValidator binds them to a bid), so validateVideoParams and validateBlocklistParams share one validation style. validateBlocklistParams now uses the same validate(...) + try/catch pattern instead of its own bespoke check. Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/yieldmoBidAdapter.js | 109 +++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index e43ade95c93..864f7e54625 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -645,6 +645,54 @@ function populateOpenRtbGdpr(openRtbRequest, bidderRequest) { } } +const isDefined = val => typeof val !== 'undefined'; + +const paramRequired = (paramStr, value, conditionStr) => { + let error = `"${paramStr}" is required`; + if (conditionStr) { + error += ' when ' + conditionStr; + } + throw new Error(error); +}; + +const paramInvalid = (paramStr, value, expectedStr) => { + expectedStr = expectedStr ? ', expected: ' + expectedStr : ''; + value = JSON.stringify(value); + throw new Error(`"${paramStr}"=${value} is invalid${expectedStr}`); +}; + +/** + * Build a field validator bound to a bid. `video.*` paths are checked against both + * `params.video.*` and `mediaTypes.video.*`; any other path is read directly. + * The error callback (paramRequired/paramInvalid) throws, so callers wrap in try/catch. + * @param {BidRequest} bid bid request + * @return {(fieldPath: string, validateCb: Function, errorCb: Function, errorCbParam?: string) => *} + */ +const createParamValidator = (bid) => (fieldPath, validateCb, errorCb, errorCbParam) => { + if (fieldPath.indexOf('video') === 0) { + const valueFieldPath = 'params.' + fieldPath; + const mediaFieldPath = 'mediaTypes.' + fieldPath; + const valueParams = deepAccess(bid, valueFieldPath); + const mediaTypesParams = deepAccess(bid, mediaFieldPath); + const hasValidValueParams = validateCb(valueParams); + const hasValidMediaTypesParams = validateCb(mediaTypesParams); + + if (hasValidValueParams) return valueParams; + else if (hasValidMediaTypesParams) return hasValidMediaTypesParams; + else { + if (!hasValidValueParams) errorCb(valueFieldPath, valueParams, errorCbParam); + else if (!hasValidMediaTypesParams) errorCb(mediaFieldPath, mediaTypesParams, errorCbParam); + } + return valueParams || mediaTypesParams; + } else { + const value = deepAccess(bid, fieldPath); + if (!validateCb(value)) { + errorCb(fieldPath, value, errorCbParam); + } + return value; + } +}; + /** * Determines whether or not the given video bid request is valid. If it's not a video bid, returns true. * @param {object} bid bid to validate @@ -654,46 +702,7 @@ function validateVideoParams(bid) { if (!hasVideoMediaType(bid)) { return true; } - - const paramRequired = (paramStr, value, conditionStr) => { - let error = `"${paramStr}" is required`; - if (conditionStr) { - error += ' when ' + conditionStr; - } - throw new Error(error); - }; - - const paramInvalid = (paramStr, value, expectedStr) => { - expectedStr = expectedStr ? ', expected: ' + expectedStr : ''; - value = JSON.stringify(value); - throw new Error(`"${paramStr}"=${value} is invalid${expectedStr}`); - }; - - const isDefined = val => typeof val !== 'undefined'; - const validate = (fieldPath, validateCb, errorCb, errorCbParam) => { - if (fieldPath.indexOf('video') === 0) { - const valueFieldPath = 'params.' + fieldPath; - const mediaFieldPath = 'mediaTypes.' + fieldPath; - const valueParams = deepAccess(bid, valueFieldPath); - const mediaTypesParams = deepAccess(bid, mediaFieldPath); - const hasValidValueParams = validateCb(valueParams); - const hasValidMediaTypesParams = validateCb(mediaTypesParams); - - if (hasValidValueParams) return valueParams; - else if (hasValidMediaTypesParams) return hasValidMediaTypesParams; - else { - if (!hasValidValueParams) errorCb(valueFieldPath, valueParams, errorCbParam); - else if (!hasValidMediaTypesParams) errorCb(mediaFieldPath, mediaTypesParams, errorCbParam); - } - return valueParams || mediaTypesParams; - } else { - const value = deepAccess(bid, fieldPath); - if (!validateCb(value)) { - errorCb(fieldPath, value, errorCbParam); - } - return value; - } - }; + const validate = createParamValidator(bid); try { validate('video.context', val => !isEmpty(val), paramRequired); @@ -739,17 +748,17 @@ function validateVideoParams(bid) { * @return {boolean} true if valid (or absent), false if present but malformed */ function validateBlocklistParams(bid) { - return ['bcat', 'badv'].every(key => { - const val = deepAccess(bid, `params.${key}`); - if (val === undefined || val === null) { - return true; - } - if (!isArray(val)) { - logError(`yieldmo: bid.params.${key} must be an array of strings when provided; dropping bid`); - return false; - } + const validate = createParamValidator(bid); + try { + validate('params.bcat', val => !isDefined(val) || isArray(val), paramInvalid, + 'array of strings, ex: ["IAB1-5","IAB1-6"]'); + validate('params.badv', val => !isDefined(val) || isArray(val), paramInvalid, + 'array of strings, ex: ["ford.com","pepsi.com"]'); return true; - }); + } catch (e) { + logError(e.message); + return false; + } } /** From c776a276e6ce0e2e83ce4d0c553e0ac6397ccd2c Mon Sep 17 00:00:00 2001 From: ym-ainiguez Date: Wed, 3 Jun 2026 14:15:16 -0700 Subject: [PATCH 4/4] Refactor: getBlocklist resolves ortb2/params internally Rename mergeBlocklist -> getBlocklist and have it take (bidderRequest, bid, field), doing the ortb2. / params. deepAccess lookups itself. Call sites no longer repeat deepAccess or the path strings; the field name appears once per call. Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/yieldmoBidAdapter.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 864f7e54625..425f1d7af11 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -155,11 +155,11 @@ export const spec = { // Blocklists (request-level): merge ortb2 + params, send as comma-delimited // params per the ad server's prebid-js endpoint (AS-5349). Omitted when empty. - const bcat = mergeBlocklist(deepAccess(bidderRequest, 'ortb2.bcat'), deepAccess(bannerBidRequests[0], 'params.bcat')); + const bcat = getBlocklist(bidderRequest, bannerBidRequests[0], 'bcat'); if (bcat.length) { serverRequest.bcat = bcat.join(','); } - const badv = mergeBlocklist(deepAccess(bidderRequest, 'ortb2.badv'), deepAccess(bannerBidRequests[0], 'params.badv')); + const badv = getBlocklist(bidderRequest, bannerBidRequests[0], 'badv'); if (badv.length) { serverRequest.badv = badv.join(','); } @@ -412,18 +412,19 @@ function getId(request, idType) { } /** - * Merge a request-level blocklist field (bcat/badv) from its two sources — the - * standardized ORTB global (`ortb2.`) and the Yieldmo-specific param - * (`params.`) — into a single deduped array of strings. Neither source is - * allowed to silently win (union, not precedence). Invalid values are ignored and - * logged rather than dropping the bid: a source that isn't an array, and any - * non-string/empty element, are filtered out (with a warning) so a misconfiguration - * is surfaced without losing the impression. - * @param {*} ortb2Value value at bidderRequest.ortb2. - * @param {*} paramsValue value at bid.params. + * Resolve a request-level blocklist field (bcat/badv) from its two sources — the + * standardized ORTB global (`bidderRequest.ortb2.`) and the Yieldmo-specific + * param (`bid.params.`) — into a single deduped array of strings. Neither + * source is allowed to silently win (union, not precedence). Invalid values are + * ignored and logged rather than dropping the bid: a source that isn't an array, and + * any non-string/empty element, are filtered out (with a warning) so a + * misconfiguration is surfaced without losing the impression. + * @param {BidderRequest} bidderRequest bidder request (source of ortb2.) + * @param {BidRequest} bid bid request (source of params.) + * @param {string} field blocklist field name — 'bcat' or 'badv' * @return {string[]} deduped, trimmed, non-empty string entries (possibly empty) */ -function mergeBlocklist(ortb2Value, paramsValue) { +function getBlocklist(bidderRequest, bid, field) { const normalize = (value, source) => { if (value === undefined || value === null) { return []; @@ -438,7 +439,10 @@ function mergeBlocklist(ortb2Value, paramsValue) { } return value.filter(item => isStr(item) && item.trim()).map(item => item.trim()); }; - return [...new Set([...normalize(ortb2Value, 'ortb2'), ...normalize(paramsValue, 'params')])]; + return [...new Set([ + ...normalize(deepAccess(bidderRequest, `ortb2.${field}`), 'ortb2'), + ...normalize(deepAccess(bid, `params.${field}`), 'params'), + ])]; } /** @@ -455,8 +459,8 @@ function openRtbRequest(bidRequests, bidderRequest) { imp: bidRequests.map(bidRequest => openRtbImpression(bidRequest)), site: openRtbSite(bidRequests[0], bidderRequest), device: deepAccess(bidderRequest, 'ortb2.device'), - badv: mergeBlocklist(deepAccess(bidderRequest, 'ortb2.badv'), deepAccess(bidRequests[0], 'params.badv')), - bcat: mergeBlocklist(deepAccess(bidderRequest, 'ortb2.bcat'), deepAccess(bidRequests[0], 'params.bcat')), + badv: getBlocklist(bidderRequest, bidRequests[0], 'badv'), + bcat: getBlocklist(bidderRequest, bidRequests[0], 'bcat'), ext: { prebid: '$prebid.version$', },