Skip to content
157 changes: 157 additions & 0 deletions adapters/flatads/flatads.go
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)
Copy link
Contributor

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

Copy link
Contributor Author

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.

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{
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 {
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 {
Copy link

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, 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 {
Copy link

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.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 {
Copy link

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.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{
Message: fmt.Sprintf("The impression with ID %s is not present into the request", impID),
}
}
29 changes: 29 additions & 0 deletions adapters/flatads/flatads_test.go
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)
}
122 changes: 122 additions & 0 deletions adapters/flatads/flatadstest/exemplary/banner.json
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"
}
]
}
]
}
Loading
Loading