Skip to content

Implement generic Prebid bid param override rules#618

Open
prk-Jr wants to merge 15 commits intomainfrom
feature/generic-prebid-bidder-param-overrides
Open

Implement generic Prebid bid param override rules#618
prk-Jr wants to merge 15 commits intomainfrom
feature/generic-prebid-bidder-param-overrides

Conversation

@prk-Jr
Copy link
Copy Markdown
Collaborator

@prk-Jr prk-Jr commented Apr 6, 2026

Summary

  • Replaces the split Prebid bidder-param override runtime with one generic ordered override engine.
  • Keeps bid_param_overrides and bid_param_zone_overrides as compatibility config, while adding canonical bid_param_override_rules for future config-driven overrides.
  • Normalizes all override config into one validated rule list at startup and applies rules by exact bidder / zone match with shallow last-write-wins merges.

Changes

File Change
crates/trusted-server-core/src/integrations/prebid.rs Add BidParamOverrideRule, BidParamOverrideWhen, and BidParamOverrideEngine; normalize compatibility fields and canonical rules into one runtime path; apply rules during OpenRTB construction; update tests
crates/trusted-server-core/src/settings.rs Add env var override test for TRUSTED_SERVER__INTEGRATIONS__PREBID__BID_PARAM_OVERRIDE_RULES
trusted-server.toml Document canonical bid_param_override_rules and clarify that compatibility fields normalize into the same engine
docs/superpowers/plans/2026-04-08-prebid-generic-bid-param-override-rules.md Add implementation plan used for this branch

Closes

Closes #617

Related: #339, #383, #384

Test plan

  • cargo fmt --all -- --check
  • cargo test -p trusted-server-core
  • cargo clippy -p trusted-server-core --all-targets --all-features -- -D warnings
  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features -- -D warnings

Checklist

  • Changes follow CLAUDE.md conventions
  • Uses log macros (not println!)
  • New code has tests
  • No secrets or credentials committed

@prk-Jr prk-Jr self-assigned this Apr 6, 2026
Renames the new field to match the existing `bid_param_zone_overrides`
naming convention. Updates all references: struct field, env var key,
doc comments, TOML example, tests, and .env.example.

Also replaces production IDs in test fixtures and examples with
generic placeholder values.
@prk-Jr prk-Jr marked this pull request as draft April 8, 2026 09:34
@prk-Jr prk-Jr changed the title Add generic per-bidder static param overrides for PBS Implement generic Prebid bid param override rules Apr 8, 2026
Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

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

Well-designed feature. The BidParamOverrideEngine with its canonical rule format and compatibility normalization from bid_param_overrides/bid_param_zone_overrides is a clean architecture. Good validation at startup via try_from_config and json_object_for_override. Test coverage is thorough — especially the engine unit tests and precedence ordering tests. A few suggestions below.

Comment thread .env.example Outdated
Comment thread crates/trusted-server-core/src/integrations/prebid.rs
Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
@prk-Jr prk-Jr marked this pull request as ready for review April 8, 2026 15:38
Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

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

Well-designed PR — the unified rule engine is clean, well-tested, and the compatibility migration strategy is sound. Approving with two recommended fixes.

Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/prebid.rs
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

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

Summary

Replaces the split bid-param override paths with a single generic BidParamOverrideEngine that normalizes all three config surfaces into an ordered rule list at startup. Clean design, solid compatibility story, comprehensive tests.

Non-blocking

♻️ refactor

  • Redundant validation-only engine build in PrebidIntegration::try_new: builds and discards the engine just for validation — same work is repeated in PrebidAuctionProvider::try_new (prebid.rs:233)

🤔 thinking

  • deny_unknown_fields on BidParamOverrideWhen: strict is good, but adding new matcher fields later becomes a breaking config change (prebid.rs:189)
  • Shallow merge semantics vs. nested override values: real-world bidder params can be nested objects — shallow merge replaces the entire value rather than deep-merging (prebid.rs:504)

⛏ nitpick

  • parse_prebid_toml_result error message: "should be enabled" is misleading when None could also mean the section is missing entirely (prebid.rs:1627)

🌱 seedling

  • Zone-only rules (no bidder): the engine requires at least one matcher, so "for all bidders in zone X" rules aren't possible yet — could be useful for zone-wide floor overrides

CI Status

  • fmt: PASS
  • clippy: PASS
  • cargo test: PASS
  • vitest: PASS
  • integration tests: PASS
  • browser integration tests: PASS

Comment thread crates/trusted-server-core/src/integrations/prebid.rs
Comment thread crates/trusted-server-core/src/integrations/prebid.rs
Comment thread crates/trusted-server-core/src/integrations/prebid.rs
Comment thread crates/trusted-server-core/src/integrations/prebid.rs
@prk-Jr prk-Jr requested a review from aram356 April 10, 2026 11:12
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

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

Summary

Adds a canonical ordered bid_param_override_rules engine and normalizes the two compatibility surfaces (bid_param_overrides, bid_param_zone_overrides) into the same runtime engine. Architecture is sound, validation fails startup fast, and the test coverage for parsing, determinism, and application order is strong. The blocker is a spec/impl/docs contradiction on merge semantics that the PR ships consistently wrong across six places.

Blocking

🔧 wrench

  • Deep merge contradicts the design spec's explicit non-goal: merge_bidder_param_object in crates/trusted-server-core/src/integrations/prebid.rs:509 recurses into nested objects, but the spec (docs/superpowers/specs/2026-04-08-prebid-generic-bid-param-override-rules-design.md line 32 goal, line 40 non-goal, line 81 rule semantics) and the plan (docs/superpowers/plans/2026-04-08-prebid-generic-bid-param-override-rules.md:7) both explicitly say shallow. The rustdoc on BidParamOverrideRule and the unit test bidder_param_override_deep_merges_nested_objects say deep. Inline fix suggestion on the helper.
  • Silent behavior change for existing bid_param_zone_overrides: the pre-PR code at the previous apply site did base.extend(...) (shallow). This PR routes the same config through the recursive helper, so any deployment with a nested-object zone override gets a different outgoing request body with no opt-out. Compatibility sugar must preserve runtime semantics. Inline comment at crates/trusted-server-core/src/integrations/prebid.rs:862.
  • trusted-server.toml:66 describes semantics incorrectly: ships in the operator template as "shallow last-write-wins merge" while the code is recursive. Inline comment on the file.
  • docs/guide/configuration.md describes merge as shallow in both the table rows (lines 706-707) and the Bid Param Override Surfaces bullets (lines 774-775). Inline comment.
  • docs/guide/integrations/prebid.md describes merge as shallow across the table rows (58-59), the TOML example comments (33, 38), the bid_param_overrides behavior bullets (170), the bid_param_zone_overrides behavior bullets (198), and the new Bid Param Override Rules section (234). Inline comment on the canonical-rules section.

The resolution is to pick one answer (shallow or deep) and make all six places agree. My recommendation is shallow, because (a) it matches the spec and plan, (b) it preserves existing bid_param_zone_overrides behavior with no silent break, and (c) deep merge is an operator footgun — a rule like set = { keywords = { genre = "news" } } will silently leak stale client-side keys (keywords.sport = "football") through to PBS, which is rarely what an operator intends when they write an override. If deep merge is the intentional choice, the spec, plan, trusted-server.toml:66, both docs pages, the PR description, and a prominent callout of the keywords footgun all need to be updated as part of this PR — the status quo of "code says deep, everything else says shallow" is not acceptable to ship.

Non-blocking

♻️ refactor

  • Engine compiled twice on registration (crates/trusted-server-core/src/integrations/prebid.rs:232-238): PrebidIntegration::try_new builds BidParamOverrideEngine, drops it, then PrebidAuctionProvider::try_new rebuilds it from the same config. The comment in the code already acknowledges this. Cleanest fix is to cache the compiled engine on PrebidIntegration and thread it into PrebidAuctionProvider::try_new, so validation and runtime share one instance and can't drift. Alternatively, extract a cheap validate_override_config(&config) -> Result<(), …> for the validation-only path.
  • or_insert(Json::Null) placeholder in merge_bidder_param_object (crates/trusted-server-core/src/integrations/prebid.rs:513): writes a Null and immediately overwrites it on every new key. A serde_json::map::Entry match is cleaner. Skip if you restructure for the shallow-merge fix anyway.

🤔 thinking

  • Asymmetric whitespace trimming: matcher strings are .trim()-ed at compile time (validate_override_matcher_string, line 327), but runtime facts (facts.bidder, facts.zone) are not. Today the runtime facts come from bidder-map keys and trustedServer.zone, so mismatches are unlikely — but the asymmetry is subtle and worth either dropping the config-side trim (strict equality, "configure what you mean") or explicitly documenting it. Not blocking.
  • Undocumented case sensitivity: matching is exact and case-sensitive. An operator with when.bidder = "Kargo" and a runtime bidder kargo gets a silently non-matching rule. Recommend a one-line note in the prebid docs. A nice-to-have stretch goal is a startup warning when a rule's when.bidder is not in config.bidders / client_side_bidders — it catches typos without false positives.

📝 note

  • Public API shape change on PrebidIntegrationConfig.bid_param_zone_overrides (line 115): type went from HashMap<String, HashMap<String, Json>> to HashMap<String, HashMap<String, serde_json::Map<String, Json>>>. Deserialization is unaffected for any real object-shaped TOML/JSON; it hardens against previously-accepted non-object values that the old runtime silently skipped. Flagging so downstream consumers constructing the struct programmatically are aware.

🌱 seedling

  • apply is O(bidders × rules) per imp. Fine at today's rule counts, but the rule engine is the natural landing spot for broader override catalogs. If the rule count grows, an index keyed by (bidder, zone) with a wildcard bucket for bidder = None turns this into O(matching-rules). Not a concern for this PR.

👍 praise

  • engine_compiles_compatibility_rules_in_sorted_matcher_order runs the same config 16 times and asserts the compiled order on each iteration. Exactly the right instinct for catching HashMap iteration non-determinism. Nice defensive test.
  • deny_unknown_fields on BidParamOverrideRule and BidParamOverrideWhen (lines 178-198) turns operator typos like when.bidders = "kargo" into hard config errors at startup, which is the right failure mode.
  • Eager validation via try_new in both PrebidIntegration and PrebidAuctionProvider propagates config errors through Report<TrustedServerError>, matching the project rule that invalid enabled config must surface as startup errors rather than be silently disabled.

CI Status

  • fmt: PASS
  • clippy: PASS
  • rust tests: PASS
  • vitest: PASS
  • format-typescript: PASS
  • format-docs: PASS
  • integration tests: PASS
  • browser integration tests: PASS
  • CodeQL: PASS

);
} else {
*existing = v.clone();
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrench — Deep merge contradicts the design spec's explicit non-goal.

This helper recurses into nested objects, but the spec at docs/superpowers/specs/2026-04-08-prebid-generic-bid-param-override-rules-design.md lists:

  • Goal (line 32): "shallow merge into bidder params"
  • Non-goal (line 40): "Deep JSON merge semantics."
  • Line 81: "set: a non-empty JSON object shallow-merged into bidder params"

The plan at docs/superpowers/plans/2026-04-08-prebid-generic-bid-param-override-rules.md:7 reiterates: "Keep the current shallow-merge semantics and last-write-wins precedence".

On top of that, every operator-facing doc that landed in this same PR (trusted-server.toml:66, docs/guide/configuration.md:706-775, docs/guide/integrations/prebid.md:33/38/58-59/170/198/234) tells operators the merge is shallow, while the rustdoc on BidParamOverrideRule.set and the test bidder_param_override_deep_merges_nested_objects say the opposite. Spec, code, tests, and docs are all out of sync on the one semantic that matters for this feature.

Concrete operator footgun: with deep merge, a rule like set = { keywords = { genre = "news" } } silently leaks client-side keys such as keywords.sport = "football" through to PBS. An operator reading the docs would reasonably expect the entire keywords object to be replaced.

Preferred fix — implement shallow merge as specified:

fn merge_bidder_param_object(params: &mut Json, override_obj: &serde_json::Map<String, Json>) {
    match params {
        Json::Object(base) => {
            for (k, v) in override_obj {
                base.insert(k.clone(), v.clone());
            }
        }
        _ => {
            *params = Json::Object(override_obj.clone());
        }
    }
}

Then:

  • flip bidder_param_override_deep_merges_nested_objects to assert nested objects are replaced, not merged;
  • update the rustdoc on BidParamOverrideRule.set (line 183-184) and BidParamOverrideRule (line 176-177) to say "shallow-merged".

Alternative — intentionally adopt deep merge. If that's the call, the spec, plan, trusted-server.toml:66, both docs pages, and the PR description all need to be updated to advertise deep merge, and the prebid docs should call out the keywords footgun explicitly so operators aren't surprised. Either way, pick one answer and make everything agree.

}
self.bid_param_override_engine
.apply(BidParamOverrideFacts { bidder: name, zone }, params);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrench — Silent behavior change for existing bid_param_zone_overrides users.

Pre-PR, zone overrides were applied inline with:

base.extend(ovr.iter().map(|(k, v)| (k.clone(), v.clone())));

That was a flat, shallow merge. This PR routes the same config through merge_bidder_param_object, which recurses into nested objects. Any existing deployment with a nested object under a zone override (for example header = { ext = { foo = "bar" } }) now gets a different outgoing request body with no opt-out and no release note.

bid_param_zone_overrides is explicitly framed as "compatibility sugar" in the rustdoc (lines 103-112) and the design doc (Goals → "Preserve the existing operator-friendly config shape"). Compatibility sugar must preserve runtime semantics byte-for-byte — otherwise the normalization is a breaking change in disguise.

Fixing the merge helper per the previous comment resolves this one as a side-effect. If you decide to keep deep merge, at minimum add an integration test that pins the old byte-for-byte behavior for a nested-object zone override and document the change in the PR description.

Comment thread trusted-server.toml
# header = {placementId = "_abc"}

# Preferred canonical override format for future rules.
# Rules run in order with exact-match conditions and shallow last-write-wins merge.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrench — Comment describes merge semantics incorrectly.

# Rules run in order with exact-match conditions and shallow last-write-wins merge.

The implementation (merge_bidder_param_object in crates/trusted-server-core/src/integrations/prebid.rs:509) is recursive, not shallow. This file ships in the repo template operators copy from, so the comment is load-bearing — it is the first thing a new operator reads.

Once the shallow-vs-deep question is resolved (see inline comment on merge_bidder_param_object), update this line to match. If deep merge stays, at minimum replace "shallow" with "deep" and add a one-line note that nested objects are merged recursively so operators aren't surprised when stale client sub-keys leak through.

| `timeout_ms` | Integer | `1000` | Request timeout in milliseconds |
| `bidders` | Array[String] | `["mocktioneer"]` | List of enabled bidders |
| `bid_param_overrides` | Table | `{}` | Static per-bidder param overrides; normalized into the canonical override-rule engine and shallow-merged into bidder params |
| `bid_param_zone_overrides` | Table | `{}` | Per-bidder, per-zone param overrides; normalized into the canonical override-rule engine and shallow-merged into bidder params |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrench — Docs describe merge as shallow; implementation is recursive deep merge.

Both the bid_param_overrides (line 706) and bid_param_zone_overrides (line 707) table rows say "shallow-merged into bidder params", and the "Bid Param Override Surfaces" bullets below (lines 774-775) repeat the same. The code does recursive deep merge and has a unit test enforcing it.

This is the third copy of the same mismatch that lands in this PR (trusted-server.toml:66, crates/trusted-server-core/src/integrations/prebid.rs:509, and this file). Please resolve the spec-vs-impl question first (see inline comment on merge_bidder_param_object) and then make configuration.md, the prebid integration page, the inline toml comments, the rustdoc, the spec, and the plan all agree on one answer.


### Bid Param Override Rules

Use `bid_param_override_rules` for the canonical ordered override format. Each rule contains exact-match `when` conditions and a non-empty `set` object that is shallow-merged into bidder params when all populated matchers match.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrench — "Bid Param Override Rules" section claims shallow merge.

This is the most user-visible description of the new canonical surface and it contradicts the implementation. The table rows (lines 58-59), the bid_param_overrides behavior bullet (line 170), the bid_param_zone_overrides behavior bullet (line 198), and the TOML-comment shallow-merge annotations (lines 33, 38) all carry the same claim.

If the implementation stays at deep merge, this section is also the natural place to document the footgun: a rule like set = { keywords = { genre = "news" } } will deep-merge into the client's keywords object, preserving keys like keywords.sport = "football" that the operator likely intended to replace. Operators writing rules against bidders that use nested param objects (appnexus keywords, openx customParams, etc.) need to know this up front.

Same resolution as the other 🔧 findings — pick shallow or deep, then update this page, configuration.md, trusted-server.toml, the rustdoc, the spec, and the plan together.

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.

Add generic per-bidder static param overrides for PBS

3 participants