New Module: CTV VAST Enrichment#4673
Conversation
This module provides comprehensive VAST processing for CTV ads: - vast.go: Main orchestration and BuildVastFromBidResponse entrypoint - handler.go: HTTP handler for VAST requests - types.go: Type definitions, interfaces (BidSelector, Enricher, Formatter) - config.go: PBS-style layered configuration (host/account/profile merge) - model/: VAST XML structures, parser, and helper functions - select/: Bid selection logic with SINGLE/TOP_N strategies - enrich/: VAST enrichment with VAST_WINS collision policy - format/: VAST XML formatting for GAM_SSU receiver Features: - Bid selection by price with deal prioritization - VAST XML parsing and skeleton generation - Metadata enrichment (pricing, advertiser, categories, debug) - Pod support with sequence numbering - Golden file tests for XML output
…Builder pattern - Rename vast.go to pipeline.go for clarity - Add module.go with Builder() and HandleRawBidderResponseHook() - Add module_test.go with comprehensive tests - Update README.md and README_EN.md with new structure - Fix import paths for new module location - Module now follows ortb2blocking/rulesengine patterns
…v_vast_enrichment)
- Change package name from 'vast' to 'ctv_vast_enrichment' - Register module in modules/builder.go - Fix ChangeSet mutation logic to use UpdateBids pattern - Add 'vast' import alias in subpackages (enrich, select, format) - Update tests to apply ChangeSet mutations before assertions
…\n\n- Add module registration section (modules/builder.go)\n- Replace YAML enabled_modules with proper host_execution_plan JSON config\n- Document ChangeSet/UpdateBids mutation pattern\n- Document clearInnerXML() XML serialization fix\n- Add package naming note (ctv_vast_enrichment + vast alias)\n- Add step-by-step PBS integration instructions
| // boolPtr is a helper function to create a pointer to a bool value. | ||
| func boolPtr(b bool) *bool { | ||
| return &b | ||
| } | ||
|
|
||
| // float64Ptr is a helper function to create a pointer to a float64 value. | ||
| func float64Ptr(f float64) *float64 { | ||
| return &f | ||
| } |
There was a problem hiding this comment.
can we use ToPtr from ptrutil.go instead of rolling you're own function
https://github.com/prebid/prebid-server/blob/master/util/ptrutil/ptrutil.go#L3
| mergedCfg := MergeCTVVastConfig(&m.hostConfig, accountCfg, nil) | ||
|
|
||
| // Check if module is enabled | ||
| if mergedCfg.Enabled != nil && !*mergedCfg.Enabled { |
There was a problem hiding this comment.
issue: this wont stop module from processing by default when enabled is nil, only if its explicitly false.
if mergedCfg.Enabled == nil || !*mergedCfg.Enabled { return result, nil }
| result, err := BuildVastFromBidResponse(ctx, bidRequest, bidResponse, h.Config, h.Selector, h.Enricher, h.Formatter) | ||
| if err != nil { | ||
| // Log error but still try to return valid VAST | ||
| // result.VastXML should contain no-ad VAST |
There was a problem hiding this comment.
add actual logging here if error
| // SelectedBid represents a bid that was selected for inclusion in the VAST response. | ||
| type SelectedBid struct { | ||
| // Bid is the OpenRTB bid object. | ||
| Bid openrtb2.Bid |
There was a problem hiding this comment.
should be a pointer to the underlying bid right?
| if cfg.SelectionStrategy != "" { | ||
| rc.SelectionStrategy = SelectionStrategy(cfg.SelectionStrategy) | ||
| } else { | ||
| rc.SelectionStrategy = SelectionStrategy(DefaultSelectionStrategy) | ||
| } |
There was a problem hiding this comment.
validation on SelectionStrategy?
| if cfg.CollisionPolicy != "" { | ||
| rc.CollisionPolicy = CollisionPolicy(cfg.CollisionPolicy) | ||
| } else { | ||
| rc.CollisionPolicy = CollisionPolicy(DefaultCollisionPolicy) | ||
| } |
There was a problem hiding this comment.
validation before setting on CollisionPolicy?
| } | ||
|
|
||
| // If we made changes, set mutation via ChangeSet | ||
| if changesMade { |
There was a problem hiding this comment.
modfiedBids is only being used if enrichment actually happens, there could be a case where we don't have any changes and we build the modifiedBids slice then effectively throw it out.
Can we only build modifiedBids if enrichment actually happens?
| ) | ||
|
|
||
| // Vast represents the root VAST XML element. | ||
| type Vast struct { |
There was a problem hiding this comment.
thought: instead of rolling our own VAST struct, we could utilize an opensource library ie https://github.com/rs/vast
cc: @bsardo
There was a problem hiding this comment.
Ideally, if we are considering using an open source library, we would use one that:
- supports the latest VAST version
- has strong adoption
- is actively maintained
The library you referenced does not appear to support VAST 4. I'm not sure if that's a deal breaker.
I do also see https://github.com/haxqer/vast which could be an option. We should take a closer look at these libraries. Let's discuss at the PBS-Go Engineering meeting next week.
There was a problem hiding this comment.
I checked Go libraries specifically. I couldn’t find a Go VAST library that supports the latest VAST 4.3 version, has strong adoption, and is actively maintained.
The closest option seems to be joeig/go-vast, because it supports parsing, modifying, and building VAST files and has a relatively recent release. However, it only supports VAST 4.2 and has low adoption.
haxqer/vast also supports VAST 4.2 and has more GitHub stars/forks, but it does not have published releases and still has TODOs around tracking events.
So for PBS-Go, I think we probably should not depend on an external Go library unless VAST 4.2 is acceptable. Otherwise, we may need to keep our own structs or use one of these libraries only as a base and extend it to cover the missing VAST 4.3 parts.
- Replace custom boolPtr/float64Ptr helpers with ptrutil.ToPtr from util/ptrutil
- Remove TestBoolPtr/TestFloat64Ptr tests (covered by ptrutil package)
- Fix enabled check: treat nil as disabled (explicit opt-in required), use IsEnabled()
- Add isValidSelectionStrategy and isValidCollisionPolicy validation with fallback to default for unknown values
- Add CollisionVastWins constant ('VAST_WINS') to CollisionPolicy
- Change SelectedBid.Bid from value type to pointer (*openrtb2.Bid)
- Add glog error logging in handler.go BuildVastFromBidResponse error path
- Lazy-allocate modifiedBids slice only when enrichment actually occurs (avoid unnecessary allocation when no bids are enriched)
- Update README.md and README_EN.md to reflect all above changes
This module provides comprehensive VAST processing for CTV ads:
module.go: PBS module entry point (Builder + HandleRawBidderResponseHook)
pipeline.go: Main orchestration and BuildVastFromBidResponse entrypoint
handler.go: HTTP handler for VAST requests
types.go: Type definitions, interfaces (BidSelector, Enricher, Formatter)
config.go: PBS-style layered configuration (host/account/profile merge)
model/: VAST XML structures, parser, and helper functions
select/: Bid selection logic with SINGLE/TOP_N strategies
enrich/: VAST enrichment with VAST_WINS collision policy
format/: VAST XML formatting for GAM_SSU receiver
Features:
Bid selection by price with deal prioritization
VAST XML parsing and skeleton generation
Metadata enrichment (pricing, advertiser, categories, debug)
Pod support with sequence numbering
VAST_WINS policy — preserves existing VAST values, does not overwrite
ChangeSet/UpdateBids mutation pattern (PBS-compliant, same as ortb2blocking)
clearInnerXML() fix preventing duplicate XML elements during serialization
Golden file tests for XML output
Links:
https://docs.google.com/document/d/11M6zyLfgbmPdZeg41xk0T_5n12WbGeSXqqEN6cKyXms/edit?tab=t.0#heading=h.b23io1ujc2es
https://docs.google.com/document/d/1KrEQk7gOZj0pUJHwVb7850awbQdiE8EfzciwlfvtSQQ/edit?tab=t.0
https://docs.google.com/document/d/1z9LpL6pQhNXq3kpG-f0hs9h-2JWaPfhExb_VuNVSsOg/edit?tab=t.xquqb6kq1v9e#heading=h.e5mvrqiytc2u
https://docs.google.com/document/d/1Fkwym4EREfOdaTD8SHUODKwlj-vWpZWQa0eGUToyzPU/edit?tab=t.0#heading=h.iaqb0q3s18nj