-
Notifications
You must be signed in to change notification settings - Fork 904
VDOAI Bid Adapter: adding new PBS module #4799
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package vdoai | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/prebid/prebid-server/v4/openrtb_ext" | ||
| ) | ||
|
|
||
| // This file tests static/bidder-params/vdoai.json | ||
| // These also validate the format of the external API: request.imp[i].ext.prebid.bidder.vdoai | ||
|
|
||
| func TestValidParams(t *testing.T) { | ||
| validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") | ||
| if err != nil { | ||
| t.Fatalf("Failed to fetch the json-schemas. %v", err) | ||
| } | ||
|
|
||
| for _, validParam := range validParams { | ||
| if err := validator.Validate(openrtb_ext.BidderVdoai, json.RawMessage(validParam)); err != nil { | ||
| t.Errorf("Schema rejected vdoai params: %s", validParam) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestInvalidParams(t *testing.T) { | ||
| validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") | ||
| if err != nil { | ||
| t.Fatalf("Failed to fetch the json-schemas. %v", err) | ||
| } | ||
|
|
||
| for _, invalidParam := range invalidParams { | ||
| if err := validator.Validate(openrtb_ext.BidderVdoai, json.RawMessage(invalidParam)); err == nil { | ||
| t.Errorf("Schema allowed unexpected params: %s", invalidParam) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| var validParams = []string{ | ||
| `{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc"}`, | ||
| `{"adUnitId":123456,"adUnitType":"video","host":"ads.vdo.ai","publisherId":"pub-xyz"}`, | ||
| `{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc","bidfloor":1.5}`, | ||
| `{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc","custom1":"val1","custom2":"val2","custom3":"val3","custom4":"val4","custom5":"val5"}`, | ||
| `{"adUnitId":123456,"adUnitType":"video","host":"ads.vdo.ai","publisherId":"pub-xyz","bidfloor":2.0,"custom1":"c1"}`, | ||
| `{"adUnitId":"123456","adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc"}`, | ||
| `{"adUnitId":"vdo-123456","adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc"}`, | ||
| } | ||
|
|
||
| var invalidParams = []string{ | ||
| ``, | ||
| `null`, | ||
| `true`, | ||
| `5`, | ||
| `4.2`, | ||
| `[]`, | ||
| `{}`, | ||
| `{"adUnitId":"123456"}`, | ||
| `{"adUnitType":"banner"}`, | ||
| `{"adUnitId":123456,"adUnitType":"banner"}`, | ||
| `{"adUnitId":123456,"adUnitType":"video"}`, | ||
| `{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net"}`, | ||
| `{"adUnitId":123456,"adUnitType":"banner","publisherId":"pub-abc"}`, | ||
| `{"adUnitId":"123456","adUnitType":"banner"}`, | ||
| `{"adUnitId":"123456","adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc","bidfloor":"notanumber"}`, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| package vdoai | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "text/template" | ||
|
|
||
| "github.com/prebid/openrtb/v20/openrtb2" | ||
| "github.com/prebid/prebid-server/v4/adapters" | ||
| "github.com/prebid/prebid-server/v4/config" | ||
| "github.com/prebid/prebid-server/v4/errortypes" | ||
| "github.com/prebid/prebid-server/v4/macros" | ||
| "github.com/prebid/prebid-server/v4/openrtb_ext" | ||
| "github.com/prebid/prebid-server/v4/util/jsonutil" | ||
| ) | ||
|
|
||
| type adapter struct { | ||
| endpointTemplate *template.Template | ||
| } | ||
|
|
||
| // Builder builds a new instance of the vdoai adapter for the given bidder with the given config. | ||
| func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { | ||
| template, err := template.New("endpointTemplate").Parse(config.Endpoint) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) | ||
| } | ||
|
|
||
| bidder := &adapter{ | ||
| endpointTemplate: template, | ||
| } | ||
| return bidder, nil | ||
| } | ||
|
|
||
| func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { | ||
| var errors []error | ||
|
|
||
| // Group imps by endpoint URL | ||
| impsByEndpoint := make(map[string][]openrtb2.Imp) | ||
|
|
||
| for i := range request.Imp { | ||
| imp := request.Imp[i] | ||
|
|
||
| var bidderExt adapters.ExtImpBidder | ||
| if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil { | ||
| errors = append(errors, &errortypes.BadInput{ | ||
| Message: fmt.Sprintf("ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err), | ||
| }) | ||
| continue | ||
| } | ||
|
|
||
| var impExt openrtb_ext.ImpExtVdoai | ||
| if err := jsonutil.Unmarshal(bidderExt.Bidder, &impExt); err != nil { | ||
| errors = append(errors, &errortypes.BadInput{ | ||
| Message: fmt.Sprintf("ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err), | ||
| }) | ||
| continue | ||
| } | ||
|
|
||
| // Inject bidfloor from bidder ext into the imp if not already set | ||
| if impExt.BidFloor > 0 && imp.BidFloor == 0 { | ||
| imp.BidFloor = impExt.BidFloor | ||
| } | ||
|
|
||
| endpointParams := macros.EndpointTemplateParams{ | ||
| Host: impExt.Host, | ||
| PublisherID: impExt.PublisherId, | ||
| } | ||
|
|
||
| endpointURL, err := macros.ResolveMacros(a.endpointTemplate, endpointParams) | ||
| if err != nil { | ||
| errors = append(errors, err) | ||
| continue | ||
| } | ||
|
|
||
| impsByEndpoint[endpointURL] = append(impsByEndpoint[endpointURL], imp) | ||
| } | ||
|
|
||
| if len(impsByEndpoint) == 0 { | ||
| return nil, errors | ||
| } | ||
|
|
||
| var requests []*adapters.RequestData | ||
| for endpoint, imps := range impsByEndpoint { | ||
| requestCopy := *request | ||
| requestCopy.Imp = imps | ||
|
|
||
| requestJSON, err := jsonutil.Marshal(requestCopy) | ||
| if err != nil { | ||
| errors = append(errors, err) | ||
| continue | ||
| } | ||
|
|
||
| headers := http.Header{} | ||
| headers.Add("Content-Type", "application/json") | ||
|
|
||
| requestData := &adapters.RequestData{ | ||
| Method: "POST", | ||
| Uri: endpoint, | ||
| Body: requestJSON, | ||
| Headers: headers, | ||
| ImpIDs: openrtb_ext.GetImpIDs(imps), | ||
| } | ||
|
|
||
| requests = append(requests, requestData) | ||
| } | ||
|
|
||
| return requests, errors | ||
| } | ||
|
|
||
| func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { | ||
| if adapters.IsResponseStatusCodeNoContent(responseData) { | ||
| return nil, nil | ||
| } | ||
|
|
||
| if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { | ||
| return nil, []error{err} | ||
| } | ||
|
|
||
| var response openrtb2.BidResponse | ||
| if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil { | ||
| return nil, []error{&errortypes.BadServerResponse{ | ||
| Message: fmt.Sprintf("error while decoding response, err: %s", err), | ||
| }} | ||
| } | ||
|
|
||
| bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) | ||
| if response.Cur != "" { | ||
| bidResponse.Currency = response.Cur | ||
| } | ||
|
|
||
| var errors []error | ||
| for _, seatBid := range response.SeatBid { | ||
| for i, bid := range seatBid.Bid { | ||
| bidType, err := getMediaTypeForBid(bid, request.Imp) | ||
| if err != nil { | ||
| errors = append(errors, err) | ||
| continue | ||
| } | ||
|
|
||
| bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ | ||
| Bid: &seatBid.Bid[i], | ||
| BidType: bidType, | ||
| }) | ||
| } | ||
| } | ||
| return bidResponse, errors | ||
| } | ||
|
|
||
| // getMediaTypeForBid determines the media type for a bid. | ||
| // It first tries bid.ext.prebid.type (set by the server), then falls back to | ||
| // detecting from the matching impression's media type objects. | ||
| func getMediaTypeForBid(bid openrtb2.Bid, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { | ||
| // Try bid.ext.prebid.type first | ||
| if bid.Ext != nil { | ||
| var bidExt openrtb_ext.ExtBid | ||
| if err := jsonutil.Unmarshal(bid.Ext, &bidExt); err == nil && bidExt.Prebid != nil { | ||
| bidType, err := openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) | ||
| if err == nil { | ||
| return bidType, nil | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Try bid.MType (OpenRTB 2.6) | ||
| switch bid.MType { | ||
| case openrtb2.MarkupBanner: | ||
| return openrtb_ext.BidTypeBanner, nil | ||
| case openrtb2.MarkupVideo: | ||
| return openrtb_ext.BidTypeVideo, nil | ||
| } | ||
|
|
||
| // Fallback: detect from the matching impression | ||
| for _, imp := range imps { | ||
| if imp.ID == bid.ImpID { | ||
| if imp.Video != nil { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeVideo, nil. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression. |
||
| return openrtb_ext.BidTypeVideo, nil | ||
| } | ||
| if imp.Banner != nil { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeBanner, nil. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression. |
||
| return openrtb_ext.BidTypeBanner, nil | ||
| } | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return "", &errortypes.BadServerResponse{ | ||
| Message: fmt.Sprintf("failed to determine media type for bid with imp id \"%s\"", bid.ImpID), | ||
| } | ||
| } | ||
|
|
||
| // vdoaiImpExt is used to re-marshal the imp ext with the vdoai-specific fields | ||
| // preserved under the "bidder" key. | ||
| type vdoaiImpExt struct { | ||
| Bidder json.RawMessage `json:"bidder"` | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package vdoai | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/prebid/prebid-server/v4/adapters/adapterstest" | ||
| "github.com/prebid/prebid-server/v4/config" | ||
| "github.com/prebid/prebid-server/v4/openrtb_ext" | ||
| ) | ||
|
|
||
| func TestJsonSamples(t *testing.T) { | ||
| bidder, buildErr := Builder( | ||
| openrtb_ext.BidderVdoai, | ||
| config.Adapter{Endpoint: "https://{{.Host}}/{{.PublisherID}}"}, | ||
| config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}, | ||
| ) | ||
|
|
||
| if buildErr != nil { | ||
| t.Fatalf("Builder returned unexpected error %v", buildErr) | ||
| } | ||
|
|
||
| adapterstest.RunJSONBidderTest(t, "vdoaitest", bidder) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider this as a suggestion. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, recommends implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.