-
Notifications
You must be signed in to change notification settings - Fork 938
New Adapter: Matterfull RTB #4343
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
7948275
6acf51a
2dcbd6e
bac4c5b
125fe40
03437de
17b0e23
0f232db
165a485
0932e3a
f09a297
8a579e4
9eee013
340a7d8
2cf277f
929c0cb
f77e5f9
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,294 @@ | ||
| package matterfullrtb | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "net/http" | ||
| "net/url" | ||
| "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/iterutil" | ||
| "github.com/prebid/prebid-server/v4/util/jsonutil" | ||
| ) | ||
|
|
||
| type adapter struct { | ||
| EndpointTemplate *template.Template | ||
| } | ||
|
|
||
| // MakeRequests prepares request information for prebid-server core | ||
| func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { | ||
| var errs []error | ||
|
|
||
| pub2impressions, impErrs := getImpressionsInfo(request.Imp) | ||
| errs = append(errs, impErrs...) | ||
| if len(pub2impressions) == 0 { | ||
| return nil, errs | ||
| } | ||
|
|
||
| result := make([]*adapters.RequestData, 0, len(pub2impressions)) | ||
| for k, imps := range pub2impressions { | ||
| bidRequest, err := a.buildAdapterRequest(request, &k, imps) | ||
| if err != nil { | ||
| errs = append(errs, err) | ||
| continue | ||
| } | ||
| result = append(result, bidRequest) | ||
| } | ||
| return result, errs | ||
| } | ||
|
|
||
| // getImpressionsInfo checks each impression for validity and returns valid impressions grouped by ext params | ||
| func getImpressionsInfo(imps []openrtb2.Imp) (map[openrtb_ext.ExtImpMatterfullRTB][]openrtb2.Imp, []error) { | ||
| var errors []error | ||
| res := make(map[openrtb_ext.ExtImpMatterfullRTB][]openrtb2.Imp) | ||
|
|
||
| for i := range imps { | ||
| imp := imps[i] // value copy so compatImpression does not mutate the original request | ||
| impExt, err := getImpressionExt(&imp) | ||
| if err != nil { | ||
| errors = append(errors, err) | ||
| continue | ||
| } | ||
| if err := compatImpression(&imp); err != nil { | ||
| errors = append(errors, err) | ||
| continue | ||
| } | ||
| res[impExt] = append(res[impExt], imp) | ||
| } | ||
| return res, errors | ||
| } | ||
|
|
||
| // Alter impression info to comply with Matterfull RTB requirements | ||
| func compatImpression(imp *openrtb2.Imp) error { | ||
| imp.Ext = nil // do not forward ext to Matterfull RTB | ||
| if imp.Banner != nil { | ||
| return compatBannerImpression(imp) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func compatBannerImpression(imp *openrtb2.Imp) error { | ||
| if imp.Banner.W == nil || imp.Banner.H == nil { | ||
|
Contributor
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. question (non-blocking): when exactly one of This mirrors lunamedia verbatim, so I suspect it's deliberate — but no test covers the "one of w/h present + format present" case, so the intent isn't pinned anywhere. A one-line comment or a |
||
| bannerCopy := *imp.Banner | ||
| banner := &bannerCopy | ||
|
|
||
| if len(banner.Format) == 0 { | ||
| return &errortypes.BadInput{ | ||
| Message: "Expected at least one banner.format entry or explicit w/h", | ||
| } | ||
| } | ||
|
|
||
| format := banner.Format[0] | ||
|
|
||
| // Deep copy the remaining formats | ||
| if len(banner.Format) > 1 { | ||
| newFormats := make([]openrtb2.Format, len(banner.Format)-1) | ||
|
Contributor
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. suggestion (non-blocking): this "keep the remaining formats" branch is only covered by the Go unit test — no JSON fixture exercises it through the wire. Every fixture with a multi-entry Consider repurposing one of them to |
||
| copy(newFormats, banner.Format[1:]) | ||
| banner.Format = newFormats | ||
| } else { | ||
| banner.Format = nil | ||
| } | ||
|
|
||
| banner.W = &format.W | ||
| banner.H = &format.H | ||
|
|
||
| imp.Banner = banner | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func getImpressionExt(imp *openrtb2.Imp) (openrtb_ext.ExtImpMatterfullRTB, error) { | ||
| var bidderExt adapters.ExtImpBidder | ||
| if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil { | ||
| return openrtb_ext.ExtImpMatterfullRTB{}, &errortypes.BadInput{ | ||
| Message: err.Error(), | ||
| } | ||
| } | ||
| var matterfullExt openrtb_ext.ExtImpMatterfullRTB | ||
| if err := jsonutil.Unmarshal(bidderExt.Bidder, &matterfullExt); err != nil { | ||
| return openrtb_ext.ExtImpMatterfullRTB{}, &errortypes.BadInput{ | ||
| Message: err.Error(), | ||
| } | ||
| } | ||
|
|
||
| return matterfullExt, nil | ||
| } | ||
|
|
||
| func (a *adapter) buildAdapterRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext.ExtImpMatterfullRTB, imps []openrtb2.Imp) (*adapters.RequestData, error) { | ||
| newBidRequest := createBidRequest(prebidBidRequest, imps) | ||
| reqJSON, err := jsonutil.Marshal(newBidRequest) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| headers := http.Header{} | ||
| headers.Add("Content-Type", "application/json;charset=utf-8") | ||
| headers.Add("Accept", "application/json") | ||
| headers.Add("x-openrtb-version", "2.6") | ||
|
|
||
| url, err := a.buildEndpointURL(params) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return &adapters.RequestData{ | ||
| Method: "POST", | ||
| Uri: url, | ||
| Body: reqJSON, | ||
| Headers: headers, | ||
| ImpIDs: openrtb_ext.GetImpIDs(imps)}, nil | ||
| } | ||
|
|
||
| func createBidRequest(prebidBidRequest *openrtb2.BidRequest, imps []openrtb2.Imp) *openrtb2.BidRequest { | ||
|
|
||
| reqCopy := *prebidBidRequest | ||
| newBidRequest := &reqCopy | ||
| newBidRequest.Imp = imps | ||
|
|
||
| if newBidRequest.Site != nil { | ||
| siteCopy := *newBidRequest.Site | ||
| newBidRequest.Site = &siteCopy | ||
| newBidRequest.Site.Publisher = nil | ||
| newBidRequest.Site.Domain = "" | ||
| } | ||
|
|
||
| if newBidRequest.App != nil { | ||
| appCopy := *newBidRequest.App | ||
| newBidRequest.App = &appCopy | ||
| newBidRequest.App.Publisher = nil | ||
| } | ||
|
|
||
| return newBidRequest | ||
| } | ||
|
|
||
| // Builds endpoint url based on adapter-specific pub settings from imp.ext | ||
| func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtImpMatterfullRTB) (string, error) { | ||
| endpointParams := macros.EndpointTemplateParams{PublisherID: url.QueryEscape(params.PublisherID)} | ||
| return macros.ResolveMacros(a.EndpointTemplate, endpointParams) | ||
| } | ||
|
|
||
| // MakeBids translates Matterfull RTB bid response to prebid-server specific format | ||
| func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { | ||
| if adapters.IsResponseStatusCodeNoContent(response) { | ||
| return nil, nil | ||
| } | ||
|
|
||
| if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { | ||
| return nil, []error{err} | ||
| } | ||
|
|
||
| var bidResp openrtb2.BidResponse | ||
| if err := jsonutil.Unmarshal(response.Body, &bidResp); err != nil { | ||
| msg := fmt.Sprintf("Bad server response: %v", err) | ||
| return nil, []error{&errortypes.BadServerResponse{Message: msg}} | ||
| } | ||
|
|
||
| if len(bidResp.SeatBid) == 0 { | ||
| return nil, nil | ||
| } | ||
|
|
||
| bidResponse := adapters.NewBidderResponse() | ||
| if bidResp.Cur != "" { | ||
| bidResponse.Currency = bidResp.Cur | ||
| } | ||
|
|
||
| var errs []error | ||
| for _, seatBid := range bidResp.SeatBid { | ||
| for bid := range iterutil.SlicePointerValues(seatBid.Bid) { | ||
| bidType, err := getMediaTypeForBid(bid, internalRequest.Imp) | ||
| if err != nil { | ||
| errs = append(errs, err) | ||
| continue | ||
| } | ||
| bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ | ||
| Bid: bid, | ||
| BidType: bidType, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| return bidResponse, errs | ||
|
postindustria-code marked this conversation as resolved.
|
||
| } | ||
|
|
||
| func getMediaTypeForBid(bid *openrtb2.Bid, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { | ||
| switch bid.MType { | ||
| case openrtb2.MarkupBanner: | ||
| return openrtb_ext.BidTypeBanner, nil | ||
| case openrtb2.MarkupVideo: | ||
| return openrtb_ext.BidTypeVideo, nil | ||
| case openrtb2.MarkupNative: | ||
| return openrtb_ext.BidTypeNative, nil | ||
| case openrtb2.MarkupAudio: | ||
| return openrtb_ext.BidTypeAudio, nil | ||
| case 0: | ||
| return getMediaTypeForImpID(bid.ImpID, imps) | ||
| default: | ||
| return "", &errortypes.BadServerResponse{ | ||
| Message: fmt.Sprintf("Unsupported mtype %d for impression with ID: %s", bid.MType, bid.ImpID), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func getMediaTypeForImpID(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { | ||
| for imp := range iterutil.SlicePointerValues(imps) { | ||
| if imp.ID == impID { | ||
| bidType, found, multiFormat := getMediaTypeForImp(imp) | ||
| if multiFormat { | ||
| return "", &errortypes.BadServerResponse{ | ||
| Message: fmt.Sprintf("Bid must have non-zero mtype for multi-format impression with ID: %s", impID), | ||
| } | ||
| } | ||
| if found { | ||
| return bidType, nil | ||
| } | ||
| return "", &errortypes.BadServerResponse{ | ||
| Message: fmt.Sprintf("Could not determine media type for impression with ID: %s", impID), | ||
| } | ||
| } | ||
| } | ||
| return "", &errortypes.BadServerResponse{ | ||
| Message: fmt.Sprintf("Failed to find impression for ID: %s", impID), | ||
| } | ||
| } | ||
|
|
||
| func getMediaTypeForImp(imp *openrtb2.Imp) (openrtb_ext.BidType, bool, bool) { | ||
| var bidType openrtb_ext.BidType | ||
| formatCount := 0 | ||
|
|
||
| if imp.Banner != nil { | ||
| bidType = openrtb_ext.BidTypeBanner | ||
| formatCount++ | ||
| } | ||
| if imp.Video != nil { | ||
| bidType = openrtb_ext.BidTypeVideo | ||
| formatCount++ | ||
| } | ||
| if imp.Audio != nil { | ||
| bidType = openrtb_ext.BidTypeAudio | ||
| formatCount++ | ||
| } | ||
| if imp.Native != nil { | ||
| bidType = openrtb_ext.BidTypeNative | ||
| formatCount++ | ||
| } | ||
|
|
||
| return bidType, formatCount == 1, formatCount > 1 | ||
| } | ||
|
|
||
| // Builder builds a new instance of the Matterfull RTB adapter for the given bidder with the given config. | ||
| func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { | ||
| urlTemplate, err := template.New("endpointTemplate").Parse(config.Endpoint) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unable to parse endpoint url template: %w", err) | ||
| } | ||
|
|
||
| bidder := &adapter{ | ||
| EndpointTemplate: urlTemplate, | ||
| } | ||
| return bidder, nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.