Skip to content

Conversation

@mkomorski
Copy link
Collaborator

@mkomorski mkomorski commented Oct 29, 2025

This module is scoped to define which bidders should be in a given auction based on a defined set of parameters. That would save money for the host company and for the bidder by eliminating network traffic for scenarios where they aren't likely to bid. It addresses these use cases:

  • Don't call specific bidders for users in particular geographies
  • Don't call specific bidders when there's no EID or FPD
  • General A/B testing for experimenting with individual new bidders
  • General A/B testing for experimenting with sets of bidders

This feature is also of interest to the Prebid Sustainability Taskforce, since making fewer requests saves money and reduces carbon emissions.

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

Other information

#12663

@mkomorski mkomorski force-pushed the mkomorski/rules-module branch from 42e6941 to 1b2ce0f Compare November 6, 2025 12:57
@github-actions
Copy link

github-actions bot commented Nov 6, 2025

Tread carefully! This PR adds 1 linter error (possibly disabled through directives):

  • modules/enrichmentLiftMeasurement/index.js (+1 error)

1 similar comment
@github-actions
Copy link

github-actions bot commented Nov 6, 2025

Tread carefully! This PR adds 1 linter error (possibly disabled through directives):

  • modules/enrichmentLiftMeasurement/index.js (+1 error)

@coveralls
Copy link
Collaborator

coveralls commented Nov 6, 2025

Pull Request Test Coverage Report for Build 20305964562

Details

  • 407 of 457 (89.06%) changed or added relevant lines in 5 files are covered.
  • 1 unchanged line in 1 file lost coverage.
  • Overall coverage decreased (-0.01%) to 96.215%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/auction.ts 4 5 80.0%
test/spec/modules/rules_spec.js 219 222 98.65%
modules/rules/index.ts 161 207 77.78%
Files with Coverage Reduction New Missed Lines %
test/spec/creative/crossDomainCreative_spec.js 1 93.75%
Totals Coverage Status
Change from base Build 19671913974: -0.01%
Covered Lines: 204347
Relevant Lines: 212386

💛 - Coveralls

@mkomorski mkomorski marked this pull request as ready for review November 6, 2025 15:06
@mkomorski mkomorski requested a review from dgirardi November 12, 2025 15:04
const getTid = tidFactory();

const mergeBidderFpd = (() => {
const fpdCache: Record<BidderCode, any> = {};
Copy link
Collaborator

Choose a reason for hiding this comment

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

this type is not correct, so why specify it? any is better.

const mergeBidderFpd = (() => {
const fpdCache: Record<BidderCode, any> = {};
return function(auctionId: string, bidderCode: BidderCode, s2sActivityParams?) {
const cacheKey = bidderCode + (s2sActivityParams != null ? s2sActivityParams[ACTIVITY_PARAM_S2S_NAME] : '');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Simple string concat is risky here, I could have bidders a, ab and an s2s named b.

.filter(uniques)
.forEach(incrementAuctionsCounter);

let {[PARTITIONS.CLIENT]: clientBidders, [PARTITIONS.SERVER]: serverBidders} = partitionBidders(adUnits, _s2sConfigs);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this wouldn't give the same result now that it happens before the filtering.


export function evaluateSchema(func, args, context) {
switch (func) {
case 'percent':
Copy link
Collaborator

Choose a reason for hiding this comment

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

An old fashioned map would be more readable (and less bug -prone) than a switch IMO

// Verify that excluded bidder is not allowed for processed-auction
expect(isActivityAllowed(ACTIVITY_ADD_BID_RESPONSE, activityParams(MODULE_TYPE_BIDDER, 'bidder3', {}))).to.eql(false);
// Verify that non-excluded bidder is allowed for processed-auction
expect(isActivityAllowed(ACTIVITY_ADD_BID_RESPONSE, activityParams(MODULE_TYPE_BIDDER, 'bidder4', {}))).to.eql(true);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not seeing how you're passing in all the extra context required by some rules (adUnit, regs, ortb2, etc), so I was looking for tests about them, and noticed that all the tests requiring it are not calling isActivityAllowed - so I'm guessing that part is still missing?

Copy link
Collaborator Author

@mkomorski mkomorski Dec 2, 2025

Choose a reason for hiding this comment

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

These describe('evaluateSchema') are unit tests limited only to check if schema functions return correct value for given params function, args, context (where context holds adUnit, bid, etc) so they don't register any activities.

I'm testing if activities are properly registered in describe('evaluateConfig') suite, and since it works correct for example schema:
schema: [{ function: 'adUnitCode', args: ['adUnit-0000'] }]
const bidder1Params = activityParams(MODULE_TYPE_BIDDER, 'bidder1', { adUnit: { code: 'adUnit-0000' } });
and all the schema functions unit tests also work correct, I didn't implement "end-to-end" tests for all possible schemas functions.

Copy link
Collaborator Author

@mkomorski mkomorski Dec 2, 2025

Choose a reason for hiding this comment

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

Here's where parameters are passed:
adapterManager.ts
auction.ts


it('should evaluate gppSidIn condition', function() {
const context1 = {
regs: {
Copy link
Collaborator

Choose a reason for hiding this comment

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

why is regs outside of ortb2 for this rule?

@github-actions
Copy link

github-actions bot commented Dec 1, 2025

Tread carefully! This PR adds 1 linter warning (possibly disabled through directives):

  • src/auctionIndex.js (+1 warning)

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

Tread carefully! This PR adds 1 linter warning (possibly disabled through directives):

  • src/auctionIndex.js (+1 warning)

@mkomorski mkomorski requested a review from dgirardi December 2, 2025 12:44
@patmmccann
Copy link
Collaborator

@mkomorski can we start the docs pr?

@robertrmartinez robertrmartinez self-requested a review December 3, 2025 17:08
@monis0395
Copy link
Collaborator

@mkomorski

  1. Should we allow caching of rules, similar to how topicsFpdModule handles it? This would help prevent auction delays when the endpoint is slow or unavailable, and reduce unnecessary network requests on subsequent page loads.
  2. Should we add way to pass a js functions in schema.[].function which will allow them to write their own rules ?
  3. The example rules.JSON shows excludeModules but it's not implemented, do we plan to add in future releases ?
  4. Currently, the percent function evaluates Math.random() on every check, which means the same bidder could be included or excluded inconsistently across different checks within the same auction. Should this be deterministic per auction or per user? For user-level consistency, we could generate a global random number once per session and use that as a seed.

@monis0395
Copy link
Collaborator

Thank you for review @monis0395

  1. we agreed on we don't make rules sticky
  2. I don't think so, that was not in the scope
  3. removed from the example
  4. nice catch! Implemented global random
  1. Make sense, both ways are fine.
  2. Okay, we can think about it in future
  3. Nice, there another example like that buyeruidAvailable
  4. Perfect!

@robertrmartinez
Copy link
Collaborator

Hi all @mkomorski @monis0395

Regarding the Math.random random stuff I actually would think we would want this to be evaluated every auction!

That is how PBS does it if I am not mistaken.

I get both reasonings, so I feel like maybe we should do both and make it configurable?

I have been asked by our data science team before to make decisions as granular as possible, per auction so that is my reasoning for asking.

@monis0395
Copy link
Collaborator

@robertrmartinez

I am fine with using per auction level too, as long as it is not generated at every request level.

  • user level
    • auction 1
      • ad unit 1 - bidder 1 - request 1
      • ad unit 2 - bidder 1 - request 2
    • auction 2
      • ad unit 1 - bidder 1 - request 3
      • ad unit 2 - bidder 1 - request 4

Here, the random value can be at the user level or the auction level. For auction level, auction 1 and auction 2 can have different random values. But generating it at the request level doesn't make sense.

@robertrmartinez
Copy link
Collaborator

Yes sorry if I worded incorrectly

I agree, its fine to just get the random number once per auction and use when selecting each rule.

}

const schemaEvaluators = {
percent: (args, context) => () => dep.getGlobalRandom() * 100 < args[0],
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this not always using one math.random per page load?

I was thinking each auction would be able to select a new random? Sorry If I am missing something here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd use a WeakMap with the keys being the auctions from auctionManager.index.getAuction

@monis0395
Copy link
Collaborator

Yes sorry if I worded incorrectly

I agree, its fine to just get the random number once per auction and use when selecting each rule.

Yes! Also if it is configurable at the user level or the auction level, that would be great @robertrmartinez @mkomorski


const stageRules = config.ruleSets;

assignModelGroups(stageRules || []);
Copy link
Collaborator

Choose a reason for hiding this comment

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

so we are picking one model per page load.

I have a similar comment on this being I would think this would want to be random per auction as to get the most uniform spread of model distributions.

PBS being stateless does everything new each auction.

I guess this becomes a bit murky here because we are just hooking into auction via activityControls which is not per auction rather per bidder.

Curious on others thoughts on this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We could perhaps use the existing requestBidsHook to optionally run assignModelGroups each time?

Though I do not think auctionId is guaranteed to be generated at this point yet so using that to offset model selected may not work.

And if you run auctions back to back things may get weird with analytics if we just run assignModelGroups without some sort of assignment

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think it's possible to achieve both calculating and assigning model groups on every auction and use activity controls by running the whole current logic in startAuction hook, pass auctionId to activity controls rules params and then unregister the rules on auction end. If this is how it works on pbs, we could just do the same, without making it optional. Also curious on others opinion on that

@pm-kapil-tuptewar
Copy link
Contributor

@mkomorski I’m assuming the country value will be set by the publisher through the ortb2 setConfig API so we can run country-based rules. But if a third-party rules provider creates rules that depend on country and the publisher forgets to pass it, how common is that? Would we need to rely on third-party APIs to fill in the country field in ortb2?

Also, do you think the browser should be another parameter we can use in rules — for example, including or excluding a bidder based on the user’s browser?

Lastly, regarding the bidPrice rule — I assume it removes bids below a certain price. But since the main goal of this module is to exclude bidders before sending them requests. Would love to hear your thoughts.

@mkomorski
Copy link
Collaborator Author

@mkomorski I’m assuming the country value will be set by the publisher through the ortb2 setConfig API so we can run country-based rules. But if a third-party rules provider creates rules that depend on country and the publisher forgets to pass it, how common is that? Would we need to rely on third-party APIs to fill in the country field in ortb2?

Also, do you think the browser should be another parameter we can use in rules — for example, including or excluding a bidder based on the user’s browser?

Lastly, regarding the bidPrice rule — I assume it removes bids below a certain price. But since the main goal of this module is to exclude bidders before sending them requests. Would love to hear your thoughts.

@pm-kapil-tuptewar This is a basic version of the module based on what was implemented in the Prebid Server. I think it would be best to release it now and then actively extend it based on what users request in terms of new functionalities.

Regarding bidPrice, there are two stages you can choose from: processed-auction-request and processed-auction the first is excluding bidders before fetching, and the second is excluding bidders after receiving responses. bidPrice can be used in the second stage.

@pm-kapil-tuptewar
Copy link
Contributor

pm-kapil-tuptewar commented Dec 10, 2025

thanks @mkomorski for the details.

method: string;
};
auctionDelay?: number;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @mkomorski, similar to the priceFloors and timeoutRtdProvider modules, which accept both static configuration data and endpoint-based data, can we adopt a comparable structure within shapingRules module as well?

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 to this we should support both

const auctionId = context.bid?.auctionId;
return dep.getGlobalRandom(auctionId) * 100 < args[0]
},
adUnitCode: (args, context) => () => context.adUnit.code === args[0],
Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @mkomorski, while testing with the above example, I observed that this condition consistently evaluates to false.
The conditions like context.adUnit.code === args[0] or context.ortb2?.device?.geo?.country === args[0] are consistently evaluating to false because context.adUnit.code and context.ortb2.device.geo.country are strings, whereas args[0] is an object, resulting in a data type mismatch. Please let me know if I may have misunderstood something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hi @pm-komal-kumari. Im not sure why you assume args[0] to be an object. In current shape it should be string, however I double checked with the prebid server implementation and in fact functions like adUnitCode should just return the value and comparision value should be took from conditions, so I updated the code. Please check the newest json file

@robertrmartinez
Copy link
Collaborator

another thing is can we provide

@mkomorski I’m assuming the country value will be set by the publisher through the ortb2 setConfig API so we can run country-based rules. But if a third-party rules provider creates rules that depend on country and the publisher forgets to pass it, how common is that? Would we need to rely on third-party APIs to fill in the country field in ortb2?
Also, do you think the browser should be another parameter we can use in rules — for example, including or excluding a bidder based on the user’s browser?
Lastly, regarding the bidPrice rule — I assume it removes bids below a certain price. But since the main goal of this module is to exclude bidders before sending them requests. Would love to hear your thoughts.

@pm-kapil-tuptewar This is a basic version of the module based on what was implemented in the Prebid Server. I think it would be best to release it now and then actively extend it based on what users request in terms of new functionalities.

Regarding bidPrice, there are two stages you can choose from: processed-auction-request and processed-auction the first is excluding bidders before fetching, and the second is excluding bidders after receiving responses. bidPrice can be used in the second stage.

regarding other schemaEvaluators, could we add in a hook to add custom schemaValidators?

Similar to additionalSchemaFields in the priceFloors module?

pbjs.setConfig({
  shapingRules: {
    // config stuff
    extraSchemaEvaluators: {
      browser: (args, context) => {
        // some fancy code to return browser string
      }
    }
  }
});

@pm-kapil-tuptewar
Copy link
Contributor

another thing is can we provide

@mkomorski I’m assuming the country value will be set by the publisher through the ortb2 setConfig API so we can run country-based rules. But if a third-party rules provider creates rules that depend on country and the publisher forgets to pass it, how common is that? Would we need to rely on third-party APIs to fill in the country field in ortb2?
Also, do you think the browser should be another parameter we can use in rules — for example, including or excluding a bidder based on the user’s browser?
Lastly, regarding the bidPrice rule — I assume it removes bids below a certain price. But since the main goal of this module is to exclude bidders before sending them requests. Would love to hear your thoughts.

@pm-kapil-tuptewar This is a basic version of the module based on what was implemented in the Prebid Server. I think it would be best to release it now and then actively extend it based on what users request in terms of new functionalities.
Regarding bidPrice, there are two stages you can choose from: processed-auction-request and processed-auction the first is excluding bidders before fetching, and the second is excluding bidders after receiving responses. bidPrice can be used in the second stage.

regarding other schemaEvaluators, could we add in a hook to add custom schemaValidators?

Similar to additionalSchemaFields in the priceFloors module?

pbjs.setConfig({
  shapingRules: {
    // config stuff
    extraSchemaEvaluators: {
      browser: (args, context) => {
        // some fancy code to return browser string
      }
    }
  }
});

Yes 👍

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.

9 participants