-
Notifications
You must be signed in to change notification settings - Fork 867
New Adapter: Flatads #4243
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
New Adapter: Flatads #4243
Changes from all commits
68298aa
ec2ab30
899cec7
1d54edd
07c69e1
16888dc
2df21b2
074a59e
d3d0bfa
ca0d664
46e614c
7238724
0d6a35c
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,157 @@ | ||
| package flatads | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "net/http" | ||
| "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 | ||
| } | ||
|
|
||
| func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { | ||
| endpointTemplate, err := template.New("endpointTemplate").Parse(config.Endpoint) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unable to parse endpoint template: %v", err) | ||
| } | ||
|
|
||
| bidder := &adapter{ | ||
| endpoint: endpointTemplate, | ||
| } | ||
| return bidder, nil | ||
| } | ||
|
|
||
| func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { | ||
| var requests []*adapters.RequestData | ||
| var errs []error | ||
|
|
||
| headers := http.Header{} | ||
| headers.Add("Content-Type", "application/json;charset=utf-8") | ||
| headers.Add("Accept", "application/json") | ||
| if request.Device != nil { | ||
| if len(request.Device.UA) > 0 { | ||
| headers.Add("User-Agent", request.Device.UA) | ||
| } | ||
|
|
||
| if len(request.Device.IPv6) > 0 { | ||
| headers.Add("X-Forwarded-For", request.Device.IPv6) | ||
| } | ||
|
|
||
| if len(request.Device.IP) > 0 { | ||
| headers.Add("X-Forwarded-For", request.Device.IP) | ||
| } | ||
| } | ||
| for _, imp := range request.Imp { | ||
| requestCopy := *request | ||
| requestCopy.Imp = []openrtb2.Imp{imp} | ||
|
|
||
| endpoint, err := a.buildEndpointFromRequest(&imp) | ||
| if err != nil { | ||
| errs = append(errs, err) | ||
| continue | ||
| } | ||
|
|
||
| requestJSON, err := jsonutil.Marshal(requestCopy) | ||
| if err != nil { | ||
| errs = append(errs, err) | ||
| continue | ||
| } | ||
|
|
||
| r := &adapters.RequestData{ | ||
| Method: http.MethodPost, | ||
| Body: requestJSON, | ||
| Uri: endpoint, | ||
| Headers: headers, | ||
| ImpIDs: openrtb_ext.GetImpIDs(requestCopy.Imp), | ||
| } | ||
|
|
||
| requests = append(requests, r) | ||
| } | ||
|
|
||
| return requests, errs | ||
| } | ||
|
|
||
| func (a *adapter) buildEndpointFromRequest(imp *openrtb2.Imp) (string, error) { | ||
| var impExt adapters.ExtImpBidder | ||
| if err := jsonutil.Unmarshal(imp.Ext, &impExt); err != nil { | ||
| return "", &errortypes.BadInput{ | ||
flatads marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Message: fmt.Sprintf("Failed to deserialize bidder impression extension: %v", err), | ||
| } | ||
| } | ||
|
|
||
| var flatadsExt openrtb_ext.ImpExtFlatads | ||
| if err := jsonutil.Unmarshal(impExt.Bidder, &flatadsExt); err != nil { | ||
| return "", &errortypes.BadInput{ | ||
| Message: fmt.Sprintf("Failed to deserialize Flatads extension: %v", err), | ||
| } | ||
| } | ||
|
|
||
| endpointParams := macros.EndpointTemplateParams{ | ||
| TokenID: flatadsExt.Token, | ||
| PublisherID: flatadsExt.PublisherId, | ||
| } | ||
|
|
||
| return macros.ResolveMacros(a.endpoint, endpointParams) | ||
| } | ||
|
|
||
| func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { | ||
| if adapters.IsResponseStatusCodeNoContent(responseData) { | ||
| return nil, nil | ||
| } | ||
|
|
||
| err := adapters.CheckResponseStatusCodeForErrors(responseData) | ||
| if err != nil { | ||
| return nil, []error{err} | ||
| } | ||
| var response openrtb2.BidResponse | ||
| if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil { | ||
| return nil, []error{err} | ||
| } | ||
|
|
||
| bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) | ||
| if len(response.Cur) != 0 { | ||
| bidResponse.Currency = response.Cur | ||
| } | ||
|
|
||
| var errors []error | ||
| for _, seatBid := range response.SeatBid { | ||
| for i := range seatBid.Bid { | ||
| bidMediaType, err := getMediaTypeForBid(seatBid.Bid[i].ImpID, request.Imp) | ||
| if err != nil { | ||
flatads marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| errors = append(errors, err) | ||
| continue | ||
| } | ||
| bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ | ||
| Bid: &seatBid.Bid[i], | ||
| BidType: bidMediaType, | ||
| }) | ||
| } | ||
| } | ||
| return bidResponse, errors | ||
| } | ||
|
|
||
| func getMediaTypeForBid(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { | ||
| for _, imp := range imps { | ||
| if imp.ID == impID { | ||
| 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 | ||
| } 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, 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 | ||
| } 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, 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.BidTypeNative, nil | ||
| } | ||
| } | ||
| } | ||
| return "", &errortypes.BadServerResponse{ | ||
flatads marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Message: fmt.Sprintf("The impression with ID %s is not present into the request", impID), | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package flatads | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/prebid/prebid-server/v3/adapters/adapterstest" | ||
| "github.com/prebid/prebid-server/v3/config" | ||
| "github.com/prebid/prebid-server/v3/openrtb_ext" | ||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestJsonSamples(t *testing.T) { | ||
| bidder, buildErr := Builder(openrtb_ext.BidderFlatads, config.Adapter{ | ||
| Endpoint: "https://test.endpoint.com/api/rtbs/adx/rtb?x-net-id={{.PublisherID}}&x-net-token={{.TokenID}}"}, | ||
| config.Server{}) | ||
|
|
||
| if buildErr != nil { | ||
| t.Fatalf("Builder returned unexpected error %v", buildErr) | ||
| } | ||
|
|
||
| adapterstest.RunJSONBidderTest(t, "flatadstest", bidder) | ||
| } | ||
|
|
||
| func TestEndpointTemplateMalformed(t *testing.T) { | ||
| _, buildErr := Builder(openrtb_ext.BidderFlatads, config.Adapter{ | ||
| Endpoint: "x-net-id={{PublisherID}}&x-net-token={{TokenID}}"}, config.Server{}) | ||
|
|
||
| assert.Error(t, buildErr) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| { | ||
| "mockBidRequest": { | ||
| "id": "test-request-id-banner", | ||
| "device": { | ||
| "ua": "test-user-agent", | ||
| "ip": "123.123.123.123", | ||
| "language": "en", | ||
| "dnt": 0 | ||
| }, | ||
| "imp": [ | ||
| { | ||
| "id": "test-imp-id-banner", | ||
| "banner": { | ||
| "format": [ | ||
| { | ||
| "w": 728, | ||
| "h": 90 | ||
| } | ||
| ] | ||
| }, | ||
| "ext": { | ||
| "bidder": { | ||
| "token": "66668888", | ||
| "publisherId": "1111" | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| "httpCalls": [ | ||
| { | ||
| "expectedRequest": { | ||
| "headers": { | ||
| "Content-Type": [ | ||
| "application/json;charset=utf-8" | ||
| ], | ||
| "Accept": [ | ||
| "application/json" | ||
| ], | ||
| "User-Agent": [ | ||
| "test-user-agent" | ||
| ], | ||
| "X-Forwarded-For": [ | ||
| "123.123.123.123" | ||
| ] | ||
| }, | ||
| "uri": "https://test.endpoint.com/api/rtbs/adx/rtb?x-net-id=1111&x-net-token=66668888", | ||
| "body": { | ||
| "id": "test-request-id-banner", | ||
| "device": { | ||
| "ua": "test-user-agent", | ||
| "ip": "123.123.123.123", | ||
| "language": "en", | ||
| "dnt": 0 | ||
| }, | ||
| "imp": [ | ||
| { | ||
| "id": "test-imp-id-banner", | ||
| "banner": { | ||
| "format": [ | ||
| { | ||
| "w": 728, | ||
| "h": 90 | ||
| } | ||
| ] | ||
| }, | ||
| "ext": { | ||
| "bidder": { | ||
| "token": "66668888", | ||
| "publisherId": "1111" | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| "impIDs": ["test-imp-id-banner"] | ||
| }, | ||
| "mockResponse": { | ||
| "status": 200, | ||
| "body": { | ||
| "id": "test-request-id-banner", | ||
| "seatbid": [ | ||
| { | ||
| "seat": "flatads", | ||
| "bid": [ | ||
| { | ||
| "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", | ||
| "impid": "test-imp-id-banner", | ||
| "price": 0.5, | ||
| "adm": "some-test-ad-banner", | ||
| "crid": "crid_10", | ||
| "w": 728, | ||
| "h": 90 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "cur": "USD" | ||
| } | ||
| } | ||
| } | ||
| ], | ||
| "expectedBidResponses": [ | ||
| { | ||
| "currency": "USD", | ||
| "bids": [ | ||
| { | ||
| "bid": { | ||
| "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", | ||
| "impid": "test-imp-id-banner", | ||
| "price": 0.5, | ||
| "adm": "some-test-ad-banner", | ||
| "crid": "crid_10", | ||
| "w": 728, | ||
| "h": 90 | ||
| }, | ||
| "type": "banner" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
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.
Please add test coverage for invalid requestCopy
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.
https://docs.prebid.org/prebid-server/developers/add-new-bidder-go.html#adapter-code-tests
It says that I do not need to test the error conditions for jsonutil.Marshal calls, for template parse errors within MakeRequests or MakeBids, or for url.Parse calls.
The request object is generated after deserialization of the request, and it is usually not Invalid.