Skip to content

Conversation

@doelwit
Copy link
Contributor

@doelwit doelwit commented Dec 18, 2025

Type of change

  • Bugfix

  • Feature

  • New bidder adapter

  • Updated bidder adapter

  • Code style update (formatting, local variables)

  • Refactoring (no functional changes, no api changes)

  • Build related changes

  • CI related changes

  • Does this change affect user-facing APIs or examples documented on http://prebid.org?

  • Other

Description of change

I have checked both new and updated because the old adapter didnt even work anymore because the urls became obsolete. Also added some new features like custom paramters and the single request

Other information

doelwit and others added 6 commits December 18, 2025 11:46
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings December 18, 2025 13:38
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the optout bid adapter with significant architectural changes. The adapter migrates from obsolete URLs to new domains (optoutadserving.com/optinadserving.com), implements single-request batching for multiple bids, and adds support for custom parameters and ortb2 data.

Key changes:

  • Switches from per-bid requests to a single batched request for all bids
  • Adds custom parameter normalization with support for arrays, objects, and circular reference handling
  • Implements conditional ortb2 payload inclusion via per-bid flag
  • Updates endpoint URLs to new domain infrastructure

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 20 comments.

File Description
modules/optoutBidAdapter.js Core adapter logic updated to batch requests, normalize custom parameters, handle ortb2 data, and route to new endpoints based on GDPR consent
test/spec/modules/optoutBidAdapter_spec.js Comprehensive test suite rewritten to cover single request batching, custom parameter normalization, GDPR routing logic, and response interpretation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to +55
const bidRequests = [
{
bidder: 'optout',
params: { adSlot: 'prebid_demo', publisher: '8' },
bidId: 'bidA'
},
'auctionId': 'e97cafd0-ebfc-4f5c-b7c9-baa0fd335a4a'
}];
{
bidder: 'optout',
params: { adSlot: 'testslot2', publisher: '8' },
bidId: 'bidB'
}
];
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no test coverage for the scenario where multiple bid requests have different publisher values. Given that buildRequests now batches all bids into a single request and only uses the publisher from the first bid, this edge case should be tested to verify the expected behavior.

Copilot uses AI. Check for mistakes.
Comment on lines 263 to 321
describe('interpretResponse', function () {
it('maps bids correctly using slot id or requestId', function () {
const bidRequest = {
data: {
slots: [
{ id: 'slotA', requestId: 'bidA' },
{ id: 'slotB', requestId: 'bidB' }
]
}
};

const serverResponse = {
body: {
bids: [
{ requestId: 'bidA', cpm: 1, currency: 'EUR', width: 300, height: 250, ad: '<div/>', ttl: 300, creativeId: 'c1' },
{ requestId: 'slotB', cpm: 2, currency: 'EUR', width: 728, height: 90, ad: '<div/>', ttl: 300, creativeId: 'c2' }
]
}
};

const out = spec.interpretResponse(serverResponse, bidRequest);
expect(out).to.have.lengthOf(2);
expect(out[0].netRevenue).to.equal(true);
});

it('filters bids whose requestId does not map to a sent slot/requestId', function () {
const bidRequest = {
data: {
slots: [{ id: 'slotA', requestId: 'bidA' }]
}
};

const serverResponse = {
body: {
bids: [{ requestId: 'unknown', cpm: 1, currency: 'EUR', width: 300, height: 250, ad: '<div/>', ttl: 300, creativeId: 'c1' }]
}
};

const out = spec.interpretResponse(serverResponse, bidRequest);
expect(out).to.deep.equal([]);
});

it('supports serverResponse.body as an array (fallback behavior)', function () {
const bidRequest = {
data: {
slots: [{ id: 'slotA', requestId: 'bidA' }]
}
};

const serverResponse = {
body: [{ requestId: 'bidA', cpm: '1.2', currency: 'EUR', width: '300', height: '250', ad: '<div/>', ttl: '120', creativeId: 'c1' }]
};

const out = spec.interpretResponse(serverResponse, bidRequest);
expect(out).to.have.lengthOf(1);
expect(out[0].cpm).to.equal(1.2);
expect(out[0].ttl).to.equal(120);
});
});
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the interpretResponse function when the server returns bids with missing or invalid requestId fields. This edge case should be tested to ensure the adapter handles malformed server responses gracefully.

Copilot uses AI. Check for mistakes.
Comment on lines 180 to 192
return {
requestId: prebidRequestId,
cpm: Number(bid.cpm) || 0,
currency: bid.currency,
width: Number(bid.width),
height: Number(bid.height),
ad: bid.ad,
ttl: Number(bid.ttl) || 300,
creativeId: bid.creativeId,
netRevenue: true,
optOutExt: bid.optOutExt,
meta: bid.meta
};
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interpretResponse function should validate required bid fields before constructing the bid object. Currently, if bid.currency, bid.ad, bid.width, or bid.height are missing, they'll be undefined in the returned object. Consider adding validation to filter out incomplete bids: if (!bid.cpm || !bid.currency || !bid.ad || !bid.width || !bid.height) return null;

Copilot uses AI. Check for mistakes.
Comment on lines 323 to 377
describe('getUserSyncs', function () {
it('returns [] when gdprConsent missing', function () {
const out = spec.getUserSyncs({ iframeEnabled: true }, [], null);
expect(out).to.deep.equal([]);
});

it('returns [] when iframeEnabled is false', function () {
const out = spec.getUserSyncs(
{ iframeEnabled: false },
[],
{ gdprApplies: false, consentString: 'abc' }
);
expect(out).to.deep.equal([]);
});

it('returns iframe sync when iframeEnabled and gdprApplies is false', function () {
const out = spec.getUserSyncs(
{ iframeEnabled: true },
[],
{ gdprApplies: false, consentString: 'abc' }
);

expect(out).to.have.lengthOf(1);
expect(out[0].type).to.equal('iframe');
expect(out[0].url).to.include('gdpr=0');
expect(out[0].url).to.include('gdpr_consent=' + encodeURIComponent('abc'));
});

it('returns iframe sync when gdprApplies true and purpose1 consent true', function () {
sinon.stub(gdprUtils, 'hasPurpose1Consent').returns(true);

const out = spec.getUserSyncs(
{ iframeEnabled: true },
[],
{ gdprApplies: true, consentString: 'CONSENT' }
);

expect(out).to.have.lengthOf(1);
expect(out[0].type).to.equal('iframe');
expect(out[0].url).to.include('gdpr=1');
expect(out[0].url).to.include('gdpr_consent=' + encodeURIComponent('CONSENT'));
});

it('returns [] when gdprApplies true and purpose1 consent false', function () {
sinon.stub(gdprUtils, 'hasPurpose1Consent').returns(false);

const out = spec.getUserSyncs(
{ iframeEnabled: true },
[],
{ gdprApplies: true, consentString: 'CONSENT' }
);

expect(out).to.deep.equal([]);
});
});
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for getUserSyncs when gdprConsent.consentString is undefined or when syncOptions has both iframeEnabled and pixelEnabled set. These scenarios should be tested to ensure the function handles all possible input combinations.

Copilot uses AI. Check for mistakes.
},
}

const addOrtb = validBidRequests.some((f) => !!f?.params?.includeOrtb2);
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'addOrtb' is abbreviated, which is inconsistent with the more descriptive naming style used elsewhere in the code (e.g., 'consentString', 'gdprConsent'). Consider renaming to 'includeOrtb2' or 'shouldIncludeOrtb2' for better clarity.

Copilot uses AI. Check for mistakes.
'https://umframe.optinadserving.com/matching/iframe?gdpr=' +
gdpr +
'&gdpr_consent=' +
encodeURIComponent(gdprConsent.consentString || '')
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getUserSyncs function accesses gdprConsent.consentString without verifying that gdprConsent is an object. Although there's an early return if !gdprConsent, between lines 198 and 216, gdprConsent could theoretically be a non-object truthy value. While unlikely in practice, add type validation for defensive programming.

Copilot uses AI. Check for mistakes.
describe('optoutAdapterTest', function () {
describe('bidRequestValidity', function () {
it('bidRequest with adslot param', function () {
describe('optoutAdapterTest (new adapter)', function () {
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test description says 'optoutAdapterTest (new adapter)' but this is an update to an existing adapter. The description should be more accurate, such as 'optoutAdapterTest' or 'Optout Bid Adapter Test'.

Suggested change
describe('optoutAdapterTest (new adapter)', function () {
describe('Optout Bid Adapter Test', function () {

Copilot uses AI. Check for mistakes.
Comment on lines 68 to 105
it('uses optout endpoint when no gdprConsent', function () {
const requests = spec.buildRequests(bidRequests, {});
requests.forEach(function(requestItem) {
expect(requestItem.url).to.match(new RegExp('adscience-nocookie\\.nl/prebid/display'));
});
expect(requests[0].url).to.match(/optoutadserving\.com\/prebid\/display/);
});

it('uses optin endpoint when gdprApplies is false', function () {
const bidderRequest = {
gdprConsent: {
gdprApplies: false,
consentString: 'test',
apiVersion: 2
}
};

const requests = spec.buildRequests(bidRequests, bidderRequest);
expect(requests[0].url).to.match(/optinadserving\.com\/prebid\/display/);
expect(requests[0].data.gdpr).to.equal(0);
});

it('always includes sdk_version=prebid', function () {
const requests = spec.buildRequests(bidRequests, {});
expect(requests[0].data.sdk_version).to.equal('prebid');
});

it('bidRequest id', function () {
it('currency defaults to EUR when not configured', function () {
config.resetConfig();
const requests = spec.buildRequests(bidRequests, {});
expect(requests[0].data.requestId).to.equal('9304jr394ddfj');
expect(requests[1].data.requestId).to.equal('893j4f94e8jei');
expect(requests[0].data.cur.adServerCurrency).to.equal('EUR');
});

it('bidRequest with config for currency', function () {
it('currency uses config when provided', function () {
config.setConfig({
currency: {
adServerCurrency: 'USD',
granularityMultiplier: 1
}
})
currency: { adServerCurrency: 'USD', granularityMultiplier: 1 }
});

const requests = spec.buildRequests(bidRequests, {});
expect(requests[0].data.cur.adServerCurrency).to.equal('USD');
expect(requests[1].data.cur.adServerCurrency).to.equal('USD');
});
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the scenario when buildRequests is called with different GDPR consent configurations (e.g., when apiVersion is 1 vs 2, or when consentString is an empty string). These edge cases should be tested to ensure the adapter handles various GDPR scenarios correctly.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +137 to +144
const mergedCustoms = Object.assign(
{},
firstBid?.params?.customs,
bidderRequest?.ortb2?.ext?.data,
bidderRequest?.ortb2?.site?.ext?.data,
bidderRequest?.ortb2?.app?.ext?.data,
bidderRequest?.ortb2?.user?.ext?.data
);
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first bid's params.customs are being merged into request-level customs (line 139) which will apply to all slots in the batched request, not just the first slot. This means that customs intended for only the first bid will incorrectly be applied to all bids in the batch.

Consider removing firstBid?.params?.customs from the mergedCustoms assignment, since per-bid customs are already handled separately at the slot level (line 125). Request-level customs should only come from ortb2 data sources.

Copilot uses AI. Check for mistakes.
const customs = normalizeCustoms(mergedCustoms);

const data = {
publisher: firstBid.params.publisher, // intentionally uses the first bid's publisher when batching
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When batching multiple bids with different publishers, only the first bid's publisher is used for all slots in the request. This could lead to incorrect attribution or billing when multiple publishers are involved in a single auction.

Consider either: (1) creating separate requests per publisher, or (2) documenting this batching limitation prominently in the adapter documentation, or (3) validating that all bids share the same publisher and returning separate requests otherwise.

Copilot uses AI. Check for mistakes.
doelwit and others added 6 commits December 18, 2025 15:32
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (1)

modules/optoutBidAdapter.js:53

  • This negation always evaluates to false.
  if (!input || typeof input !== 'object') return {};

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +131 to +133

const slot = {
adSlot: b.params.adSlot,
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical mismatch between slot construction and response mapping. Line 133 sets slot.adSlot = b.params.adSlot but line 192 in interpretResponse tries to map using s.id. The slot object should set id property with the adSlot value (line 133 should be id: b.params.adSlot || b.params.adslot), otherwise the response mapping will fail because s.id will be undefined for most slots (only defined when params.id is explicitly provided).

Suggested change
const slot = {
adSlot: b.params.adSlot,
const adSlot = b?.params?.adSlot || b?.params?.adslot;
const slot = {
adSlot: adSlot,
id: adSlot,

Copilot uses AI. Check for mistakes.
const customs = normalizeCustoms(mergedCustoms);

const data = {
publisher: firstBid.params.publisher, // intentionally uses the first bid's publisher when batching
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment at line 156 states "intentionally uses the first bid's publisher when batching", but there's no validation that all bids in the batch have the same publisher. This could lead to incorrect behavior when bids from different publishers are batched together, as only the first publisher ID will be used for all slots. Consider either validating that all bids have the same publisher, or creating separate requests per publisher.

Copilot uses AI. Check for mistakes.
const slotCustoms = normalizeCustoms(b?.params?.customs);

const slot = {
adSlot: b.params.adSlot,
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Property naming inconsistency: the slot object uses 'adSlot' (line 133) while the validator checks for both 'adSlot' and 'adslot' (line 95). This inconsistency should be resolved by standardizing on one property name throughout the adapter. The validator suggests supporting both for backward compatibility, but the buildRequests function only uses params.adSlot.

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +153
const mergedCustoms = Object.assign(
{},
firstBid?.params?.customs,
bidderRequest?.ortb2?.ext?.data,
bidderRequest?.ortb2?.site?.ext?.data,
bidderRequest?.ortb2?.app?.ext?.data,
bidderRequest?.ortb2?.user?.ext?.data
);

const customs = normalizeCustoms(mergedCustoms);
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The merging logic for customs on lines 144-151 could lead to unexpected behavior. It merges customs from firstBid.params.customs with multiple ortb2 data sources, but then applies normalizeCustoms to the merged result. Since normalizeCustoms expects to work with a clean object and doesn't handle nested merging well, properties from later sources will overwrite earlier ones even if they're of different types (e.g., an array from params could be overwritten by a string from ortb2). Consider documenting this merge precedence or handling the merge more carefully to preserve intended data types.

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +139
if (b.params && b.params.id != null) {
slot.id = String(b.params.id);
}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 137-139 set slot.id conditionally from params.id, which conflicts with the expected behavior where slot.id should be the adSlot identifier. This logic suggests params.id is meant to override the slot identifier, but this creates confusion. If params.id is meant to be a different field (like a publisher-specific ID), it should use a different property name. Otherwise, this conditional assignment should be removed once slot.id is properly set to the adSlot value.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +17
// If it's not a valid URL, return an empty string to avoid leaking potentially sensitive data
return '';
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the catch block, returning an empty string when URL parsing fails could cause issues downstream if the URL is expected. Consider returning a safe default like the origin of window.location, or at minimum document this behavior. An empty string for the 'url' field in the bid request might not be handled properly by the ad server.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@doelwit doelwit marked this pull request as draft December 18, 2025 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants