Skip to content

Implement generic Prebid bid param override rules#618

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

Implement generic Prebid bid param override rules#618
prk-Jr wants to merge 18 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
@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 Outdated
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

Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/prebid.rs
Comment thread trusted-server.toml
Comment thread docs/guide/configuration.md
Comment thread docs/guide/integrations/prebid.md
prk-Jr and others added 2 commits April 20, 2026 12:39
Replace the recursive deep-merge in merge_bidder_param_object with a
flat shallow merge (top-level keys inserted/replaced, nested objects
replaced wholesale). This aligns the implementation with the design
spec, plan, operator docs, and trusted-server.toml — all of which
described shallow semantics while the code was doing deep merge.

Consequences:
- Existing bid_param_zone_overrides behavior is preserved byte-for-byte;
  the previous inline base.extend() was already shallow, and routing
  through the engine now produces the same result.
- The operator footgun is eliminated: set = { keywords = { genre = "news" } }
  replaces the entire keywords object rather than leaking stale client-side
  sub-keys like keywords.sport = "football" through to PBS.

Also addressed in this commit:
- Extract validate_bid_param_override_config() so the validation-only
  path in PrebidIntegration::try_new is explicit rather than a let _ =.
- Rename test bidder_param_override_deep_merges_nested_objects to
  bidder_param_override_replaces_nested_objects and flip the assertion
  on the nested keywords.sport key from "football" to Json::Null.
- Update rustdoc on BidParamOverrideRule and its set field to say
  "shallow-merged" with a note that nested objects are replaced wholesale.
- Add a comment above validate_override_matcher_string documenting the
  asymmetric whitespace trimming: config strings are trimmed, runtime
  facts are not (they come from controlled internal sources).
- Add a case-sensitivity note to the Bid Param Override Rules behavior
  list in docs/guide/integrations/prebid.md.
@prk-Jr prk-Jr requested a review from aram356 April 20, 2026 07:38
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

Well-scoped refactor: replaces two ad-hoc override paths with one ordered rule engine, introduces a canonical bid_param_override_rules surface, and the shallow-merge revert in eec40e2e correctly restores pre-PR bid_param_zone_overrides semantics. Two concerns remain before this lands cleanly — a user-facing docs section that still describes the old runtime, and an unannounced behavior change for non-object bidder params that slipped through the shallow-merge fix.

Blocking

🔧 wrench

  • Stale implementation bullet lies about runtime behaviordocs/guide/integrations/prebid.md:374 (outside the PR's diff hunks, so inline comment was not possible). Reads "Applies bid_param_zone_overrides to imp.ext.prebid.bidder before request dispatch" after this PR unifies three surfaces through one engine. Suggested replacement:

    - Applies `bid_param_overrides`, `bid_param_zone_overrides`, and `bid_param_override_rules` via the unified override engine before request dispatch
  • Silent behavior change for non-object bidder params — see inline comment on crates/trusted-server-core/src/integrations/prebid.rs:518. The new merge_bidder_param_object fallback replaces non-object params wholesale, whereas the pre-PR zone-override path was a no-op. This contradicts the "byte-for-byte preservation" rationale in the eec40e2e commit message.

Non-blocking

🤔 thinking

  • No integration-level fail-fast test — unit tests verify CompiledBidParamOverrideRule::try_from rejects empty when/set, but no test drives an invalid override config through register() (prebid.rs:357) or register_auction_provider() (prebid.rs:1484). The comment at prebid.rs:236 explicitly calls out the dual-validation pattern as fail-fast — worth one smoke test per entry-point to pin that promise.
  • Json::Null values in set accepted silently — see inline comment on prebid.rs:710.

♻️ refactor

  • Double-clone of canonical rules during engine compile — see inline comment on prebid.rs:580.

🌱 seedling

  • Canonical rule referencing unconfigured bidder is silently ignoredwhen.bidder = "misspelled" compiles and never fires, with no log. validate_bid_param_override_config is a natural place to emit a startup warning when a rule's when.bidder doesn't appear in config.bidders or config.client_side_bidders. Out of scope for this PR.

⛏ nitpick

  • Orphan empty table header in trusted-server.toml — see inline comment on trusted-server.toml:62.

CI Status

  • fmt: PASS
  • clippy: PASS
  • rust tests: PASS
  • js tests: PASS
  • codeql / integration / browser-integration: PASS

Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/prebid.rs
Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread trusted-server.toml Outdated
- Revert merge_bidder_param_object to no-op for non-object params,
  restoring byte-for-byte pre-PR bid_param_zone_overrides behavior
- Add TryFrom<&BidParamOverrideRule> to avoid redundant whole-struct
  clone during engine compile; owned impl now delegates to it
- Update stale docs bullet to list all three override surfaces
- Document Json::Null behavior in set values in prebid.md behavior list
- Comment out orphan empty table header in trusted-server.toml
@prk-Jr prk-Jr requested a review from aram356 April 21, 2026 03:21
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