Summary
creative_opportunities.slot.gam_unit_path is a static string, so a publisher whose ad unit varies by site section cannot express that in one rule. The only way to model it is one slot rule per (slot × section), which multiplies out fast: a site with 3 slots and 10 sections needs 30 near-identical rules to express a single pattern.
Why it happens
resolved_gam_unit_path is literal-or-default — there is no substitution step:
pub fn resolved_gam_unit_path(&self, gam_network_id: &str) -> String {
self.gam_unit_path
.clone()
.unwrap_or_else(|| format!("/{}/{}", gam_network_id, self.id))
}
crates/trusted-server-core/src/creative_opportunities.rs
So gam_unit_path is either a fixed path or /<network_id>/<slot_id>. Neither can vary by request path, even though page_patterns already matches on exactly that.
Impact
Each duplicated rule repeats id, div_id, formats, and the [creative_opportunities.slot.providers.prebid] sub-table. Only gam_unit_path and page_patterns differ. That means:
- Config bloat — 30 rules where 3 would do; ~270 lines to express one mapping.
- Silent drift — adding a format or changing
div_id requires editing every section variant. Miss one and that section quietly diverges.
- Rot on growth — every new site section requires N new rules (one per slot), and forgetting them means those pages fall through to whatever rule matches, bidding on the wrong inventory.
The failure mode is not loud: a rule with the wrong gam_unit_path still returns bids, just against inventory that does not correspond to the page. That is a discrepancy/reporting problem that is easy to ship and hard to notice.
Proposed direction
Some way to express "unit varies by matched section" in a single rule. Options, roughly in order of blast radius:
- Placeholder substitution in
gam_unit_path, resolved at request time from the matched path — e.g. gam_unit_path = "/12345678/publisher/{section}".
- Explicit mapping table on the slot — pattern → unit — keeping resolution data-driven rather than string-templated:
[[creative_opportunities.slot]]
id = "ad-header"
[creative_opportunities.slot.unit_by_pattern]
"/" = "/12345678/publisher/homepage"
"/news/*" = "/12345678/publisher/news"
- Derivation rule with an explicit default (e.g. first path segment, with a configurable value for
/).
Design constraint worth calling out
Whatever the mechanism, the derivation policy is publisher-specific. "First path segment, with / mapping to homepage" is one site's URL convention, not a universal one — it should not be hardcoded in core. Option 2 keeps the policy in config; option 1 needs a well-defined, documented placeholder set plus a decision on what {section} means for /, multi-segment paths, and unmatched routes.
Implementation note
There is exactly one runtime call site:
crates/trusted-server-core/src/publisher.rs — build_slot_json(slot, co_config)
It does not currently have the request path in scope. It is shared by build_ad_slots_script (initial render) and handle_page_bids (SPA navigation), both of which do know the path, so threading it through is mechanical. CreativeOpportunitySlot::validate also calls resolved_gam_unit_path without a path and would need a path-independent validation story.
Acceptance criteria
Summary
creative_opportunities.slot.gam_unit_pathis a static string, so a publisher whose ad unit varies by site section cannot express that in one rule. The only way to model it is one slot rule per (slot × section), which multiplies out fast: a site with 3 slots and 10 sections needs 30 near-identical rules to express a single pattern.Why it happens
resolved_gam_unit_pathis literal-or-default — there is no substitution step:crates/trusted-server-core/src/creative_opportunities.rsSo
gam_unit_pathis either a fixed path or/<network_id>/<slot_id>. Neither can vary by request path, even thoughpage_patternsalready matches on exactly that.Impact
Each duplicated rule repeats
id,div_id,formats, and the[creative_opportunities.slot.providers.prebid]sub-table. Onlygam_unit_pathandpage_patternsdiffer. That means:div_idrequires editing every section variant. Miss one and that section quietly diverges.The failure mode is not loud: a rule with the wrong
gam_unit_pathstill returns bids, just against inventory that does not correspond to the page. That is a discrepancy/reporting problem that is easy to ship and hard to notice.Proposed direction
Some way to express "unit varies by matched section" in a single rule. Options, roughly in order of blast radius:
gam_unit_path, resolved at request time from the matched path — e.g.gam_unit_path = "/12345678/publisher/{section}"./).Design constraint worth calling out
Whatever the mechanism, the derivation policy is publisher-specific. "First path segment, with
/mapping tohomepage" is one site's URL convention, not a universal one — it should not be hardcoded in core. Option 2 keeps the policy in config; option 1 needs a well-defined, documented placeholder set plus a decision on what{section}means for/, multi-segment paths, and unmatched routes.Implementation note
There is exactly one runtime call site:
crates/trusted-server-core/src/publisher.rs—build_slot_json(slot, co_config)It does not currently have the request path in scope. It is shared by
build_ad_slots_script(initial render) andhandle_page_bids(SPA navigation), both of which do know the path, so threading it through is mechanical.CreativeOpportunitySlot::validatealso callsresolved_gam_unit_pathwithout a path and would need a path-independent validation story.Acceptance criteria
/, multi-segment paths, and paths matching no rulegam_unit_pathconfigs keep working unchangedvalidate()still catches an empty/unresolvable unit path