-
Notifications
You must be signed in to change notification settings - Fork 867
RTBStack adapter #4685
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?
RTBStack adapter #4685
Changes from all commits
484d41e
502aaa5
60b451c
5a9b3fe
f6d7c97
f510625
11dbfcd
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,216 @@ | ||
| package rtbstack | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "strings" | ||
| "text/template" | ||
|
|
||
| "github.com/prebid/openrtb/v20/openrtb2" | ||
| "github.com/prebid/prebid-server/v3/adapters" | ||
| "github.com/prebid/prebid-server/v3/config" | ||
| "github.com/prebid/prebid-server/v3/errortypes" | ||
| "github.com/prebid/prebid-server/v3/macros" | ||
| "github.com/prebid/prebid-server/v3/openrtb_ext" | ||
| "github.com/prebid/prebid-server/v3/util/jsonutil" | ||
| ) | ||
|
|
||
| type adapter struct { | ||
| endpoint *template.Template | ||
| } | ||
|
|
||
| // impCtx represents the context containing an OpenRTB impression and its corresponding RTBStack extension configuration. | ||
| type impCtx struct { | ||
| imp openrtb2.Imp | ||
| rtbStackExt *openrtb_ext.ExtImpRTBStack | ||
| } | ||
|
|
||
| // extImpRTBStack is used for imp->ext when sending to rtb-stack backend | ||
| type extImpRTBStack struct { | ||
| TagId string `json:"tagid"` | ||
| CustomParams map[string]interface{} `json:"customParams,omitempty"` | ||
| } | ||
|
|
||
| func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { | ||
| tpl, err := template.New("endpointTemplate").Parse(config.Endpoint) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) | ||
| } | ||
|
|
||
| bidder := &adapter{ | ||
| endpoint: tpl, | ||
| } | ||
| return bidder, nil | ||
| } | ||
|
|
||
| func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { | ||
| if len(request.Imp) == 0 { | ||
| return nil, []error{&errortypes.BadInput{ | ||
| Message: "No impressions in request", | ||
| }} | ||
| } | ||
|
|
||
| var errs []error | ||
| var validImps []*impCtx | ||
|
|
||
| for _, imp := range request.Imp { | ||
| ext, err := preprocessImp(&imp) | ||
| if err != nil { | ||
| errs = append(errs, err) | ||
| continue | ||
| } | ||
|
|
||
| validImps = append(validImps, &impCtx{ | ||
| imp: imp, | ||
| rtbStackExt: ext, | ||
| }) | ||
| } | ||
|
|
||
| if len(validImps) == 0 { | ||
| return nil, errs | ||
| } | ||
|
|
||
| request.Imp = nil | ||
| for _, v := range validImps { | ||
| request.Imp = append(request.Imp, v.imp) | ||
| } | ||
|
|
||
| endpoint, err := a.buildEndpointURL(validImps[0].rtbStackExt) | ||
| if err != nil { | ||
| return nil, []error{err} | ||
| } | ||
|
|
||
| var newRequest openrtb2.BidRequest | ||
| newRequest = *request | ||
|
|
||
| if request.Site != nil && request.Site.Domain == "" { | ||
| newSite := *request.Site | ||
| newSite.Domain = request.Site.Page | ||
| newRequest.Site = &newSite | ||
| } | ||
|
|
||
| reqJSON, err := json.Marshal(newRequest) | ||
| if err != nil { | ||
| return nil, []error{&errortypes.BadInput{ | ||
| Message: "Error parsing reqJSON object", | ||
| }} | ||
| } | ||
|
|
||
| headers := http.Header{} | ||
| headers.Add("Content-Type", "application/json;charset=utf-8") | ||
| headers.Add("Accept", "application/json") | ||
| return []*adapters.RequestData{{ | ||
| Method: "POST", | ||
| Uri: endpoint, | ||
| Body: reqJSON, | ||
| Headers: headers, | ||
| ImpIDs: openrtb_ext.GetImpIDs(request.Imp), | ||
| }}, []error{} | ||
| } | ||
|
|
||
| func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { | ||
| if response.StatusCode == http.StatusNoContent { | ||
| return nil, nil | ||
| } | ||
|
|
||
| if response.StatusCode != http.StatusOK { | ||
| return nil, []error{fmt.Errorf("Unexpected status code: %d", response.StatusCode)} | ||
| } | ||
|
|
||
| var bidResp openrtb2.BidResponse | ||
| if err := jsonutil.Unmarshal(response.Body, &bidResp); err != nil { | ||
| return nil, []error{err} | ||
| } | ||
|
|
||
| if len(bidResp.SeatBid) == 0 || len(bidResp.SeatBid[0].Bid) == 0 { | ||
| return nil, nil | ||
| } | ||
|
|
||
| bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) | ||
|
|
||
| for _, sb := range bidResp.SeatBid { | ||
| for i := 0; i < len(sb.Bid); i++ { | ||
| bid := sb.Bid[i] | ||
| bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ | ||
| Bid: &bid, | ||
| BidType: getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp), | ||
| }) | ||
|
|
||
| } | ||
| } | ||
| return bidResponse, []error{} | ||
| } | ||
|
|
||
| func (a *adapter) buildEndpointURL(ext *openrtb_ext.ExtImpRTBStack) (string, error) { | ||
| // Normalize host to avoid accidental protocol prefixes and domains accidentally included | ||
| host := ext.Host | ||
| host = strings.TrimPrefix(host, "http://") | ||
| host = strings.TrimPrefix(host, "https://") | ||
|
|
||
| endpointParams := macros.EndpointTemplateParams{Host: host} | ||
| baseURL, err := macros.ResolveMacros(a.endpoint, endpointParams) | ||
| if err != nil { | ||
| return "", fmt.Errorf("unable to resolve endpoint: %v", err) | ||
| } | ||
|
|
||
| if ext.Query == "" { | ||
| return baseURL, nil | ||
| } | ||
|
|
||
| if strings.HasPrefix(ext.Query, "/") { | ||
| return baseURL + ext.Query, nil | ||
| } | ||
|
|
||
| return baseURL + "/" + ext.Query, nil | ||
| } | ||
|
|
||
| func preprocessImp( | ||
| imp *openrtb2.Imp, | ||
| ) (*openrtb_ext.ExtImpRTBStack, error) { | ||
| var bidderExt adapters.ExtImpBidder | ||
| if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil { | ||
| return nil, &errortypes.BadInput{Message: err.Error()} | ||
| } | ||
|
|
||
| var impExt openrtb_ext.ExtImpRTBStack | ||
| if err := jsonutil.Unmarshal(bidderExt.Bidder, &impExt); err != nil { | ||
| return nil, &errortypes.BadInput{ | ||
| Message: "Wrong RTBStack bidder ext", | ||
| } | ||
| } | ||
|
|
||
| imp.TagID = impExt.TagId | ||
|
|
||
| // create new imp->ext without odd params | ||
| newExt := extImpRTBStack{ | ||
| TagId: impExt.TagId, | ||
| CustomParams: impExt.CustomParams, | ||
| } | ||
|
|
||
| // simplify content from imp->ext->bidder to imp->ext | ||
| newImpExtForRTBStack, err := json.Marshal(newExt) | ||
| if err != nil { | ||
| return nil, &errortypes.BadInput{Message: err.Error()} | ||
| } | ||
| imp.Ext = newImpExtForRTBStack | ||
|
|
||
| return &impExt, nil | ||
| } | ||
|
|
||
| func getMediaTypeForImp(impID string, imps []openrtb2.Imp) openrtb_ext.BidType { | ||
| for _, imp := range imps { | ||
| if imp.ID == impID { | ||
| if imp.Banner != nil { | ||
| return openrtb_ext.BidTypeBanner | ||
| } else 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. 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 | ||
| } else if imp.Native != 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.BidTypeNative. 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.BidTypeNative | ||
| } else if imp.Audio != 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.BidTypeAudio. 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.BidTypeAudio | ||
| } | ||
| } | ||
| } | ||
| return openrtb_ext.BidTypeBanner | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package rtbstack | ||
|
|
||
| import ( | ||
| "github.com/prebid/prebid-server/v3/adapters/adapterstest" | ||
| "github.com/prebid/prebid-server/v3/config" | ||
| "github.com/prebid/prebid-server/v3/openrtb_ext" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestJsonSamples(t *testing.T) { | ||
| bidder, buildErr := Builder(openrtb_ext.BidderRTBStack, config.Adapter{ | ||
| Endpoint: "https://{{.Host}}.rtb-stack.com"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) | ||
|
|
||
| if buildErr != nil { | ||
| t.Fatalf("Builder returned unexpected error %v", buildErr) | ||
| } | ||
|
|
||
| adapterstest.RunJSONBidderTest(t, "rtbstacktest", bidder) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| { | ||
| "mockBidRequest": { | ||
| "id": "test-request-id", | ||
| "app": { | ||
| "bundle": "com.prebid.app" | ||
| }, | ||
| "device": { | ||
| "ifa": "ec943cb9-61ec-460f-a925-6489c3fcc4e3" | ||
| }, | ||
| "imp": [ | ||
| { | ||
| "id": "imp-app-audio-short", | ||
| "audio": { | ||
| "mimes": ["audio/mpeg", "audio/mp4"], | ||
| "minduration": 5, | ||
| "maxduration": 30 | ||
| }, | ||
| "ext": { | ||
| "bidder": { | ||
| "host": "test", | ||
| "query": "prebid?client=c4527281-5aa5-4c8e-bc53-a80bb3f99470&endpoint=309&ssp=145", | ||
| "tagId": "app-audio-short", | ||
| "customParams": { | ||
| "foo": "bar" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "id": "imp-app-audio-long", | ||
| "audio": { | ||
| "mimes": ["audio/mpeg", "audio/mp4"], | ||
| "minduration": 15, | ||
| "maxduration": 60 | ||
| }, | ||
| "ext": { | ||
| "bidder": { | ||
| "host": "test", | ||
| "query": "prebid?client=c4527281-5aa5-4c8e-bc53-a80bb3f99470&endpoint=309&ssp=145", | ||
| "tagId": "app-audio-long", | ||
| "customParams": {} | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
|
|
||
| "httpCalls": [ | ||
| { | ||
| "expectedRequest": { | ||
| "uri": "https://test.rtb-stack.com/prebid?client=c4527281-5aa5-4c8e-bc53-a80bb3f99470&endpoint=309&ssp=145", | ||
| "body": { | ||
| "id": "test-request-id", | ||
| "app": { | ||
| "bundle": "com.prebid.app" | ||
| }, | ||
| "device": { | ||
| "ifa": "ec943cb9-61ec-460f-a925-6489c3fcc4e3" | ||
| }, | ||
| "imp": [ | ||
| { | ||
| "id": "imp-app-audio-short", | ||
| "audio": { | ||
| "mimes": ["audio/mpeg", "audio/mp4"], | ||
| "minduration": 5, | ||
| "maxduration": 30 | ||
| }, | ||
| "tagid": "app-audio-short", | ||
| "ext": { | ||
| "tagid": "app-audio-short", | ||
| "customParams": { | ||
| "foo": "bar" | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "id": "imp-app-audio-long", | ||
| "audio": { | ||
| "mimes": ["audio/mpeg", "audio/mp4"], | ||
| "minduration": 15, | ||
| "maxduration": 60 | ||
| }, | ||
| "tagid": "app-audio-long", | ||
| "ext": { | ||
| "tagid": "app-audio-long" | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| "impIDs": ["imp-app-audio-short", "imp-app-audio-long"] | ||
| }, | ||
| "mockResponse": { | ||
| "status": 200, | ||
| "body": { | ||
| "id": "test-request-id", | ||
| "seatbid": [ | ||
| { | ||
| "seat": "rtbstack", | ||
| "bid": [ | ||
| { | ||
| "id": "bid-app-audio-short-1", | ||
| "impid": "imp-app-audio-short", | ||
| "price": 0.75, | ||
| "adm": "<audio src='short.mp3'></audio>", | ||
| "crid": "crid-app-audio-short-1" | ||
| }, | ||
| { | ||
| "id": "bid-app-audio-long-1", | ||
| "impid": "imp-app-audio-long", | ||
| "price": 1.25, | ||
| "adm": "<audio src='long.mp3'></audio>", | ||
| "crid": "crid-app-audio-long-1" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "cur": "USD" | ||
| } | ||
| } | ||
| } | ||
| ], | ||
|
|
||
| "expectedBidResponses": [ | ||
| { | ||
| "currency": "USD", | ||
| "bids": [ | ||
| { | ||
| "bid": { | ||
| "id": "bid-app-audio-short-1", | ||
| "impid": "imp-app-audio-short", | ||
| "price": 0.75, | ||
| "adm": "<audio src='short.mp3'></audio>", | ||
| "crid": "crid-app-audio-short-1" | ||
| }, | ||
| "type": "audio" | ||
| }, | ||
| { | ||
| "bid": { | ||
| "id": "bid-app-audio-long-1", | ||
| "impid": "imp-app-audio-long", | ||
| "price": 1.25, | ||
| "adm": "<audio src='long.mp3'></audio>", | ||
| "crid": "crid-app-audio-long-1" | ||
| }, | ||
| "type": "audio" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
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. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeBanner. 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.