Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 273 additions & 0 deletions adapters/clydo/clydo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
package clydo

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) {
template, err := template.New("endpointTemplate").Parse(config.Endpoint)
if err != nil {
return nil, fmt.Errorf("Failed to parse endpoint url: %v", err)
}
bidder := &adapter{
endpoint: template,
}
return bidder, nil
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var requests []*adapters.RequestData
var errors []error

impIds, err := prepareImpIds(request)
if err != nil {
return nil, append(errors, err)
}

headers, err := prepareHeaders(request)
if err != nil {
return nil, append(errors, err)
}

for _, imp := range request.Imp {
reqData, err := a.prepareRequest(request, imp, impIds, headers)
if err != nil {
errors = append(errors, err)
continue
}
requests = append(requests, reqData)
}
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 errResp := adapters.CheckResponseStatusCodeForErrors(responseData); errResp != nil {
return nil, []error{errResp}
}
response, err := prepareBidResponse(responseData.Body)
if err != nil {
return nil, []error{err}
}

bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
if response.Cur != "" {
bidResponse.Currency = response.Cur
}

bidTypeMap, err := buildBidTypeMap(request.Imp)
if err != nil {
return nil, []error{err}
}
bids := prepareSeatBids(response.SeatBid, bidTypeMap)
bidResponse.Bids = bids

return bidResponse, nil
}

func (a *adapter) prepareRequest(request *openrtb2.BidRequest, imp openrtb2.Imp, impIds []string, headers http.Header) (*adapters.RequestData, error) {
params, err := prepareExtParams(imp)
if err != nil {
return nil, err
}
endpoint, err := a.prepareEndpoint(params)
if err != nil {
return nil, err
}
body, err := prepareBody(request, imp)
if err != nil {
return nil, err
}

return &adapters.RequestData{
Method: "POST",
Uri: endpoint,
Body: body,
Headers: headers,
ImpIDs: impIds,
}, nil
}

func prepareExtParams(imp openrtb2.Imp) (*openrtb_ext.ImpExtClydo, error) {
var clydoImpExt openrtb_ext.ImpExtClydo
var bidderExt adapters.ExtImpBidder
if len(imp.Ext) == 0 {
return nil, &errortypes.BadInput{
Message: "missing ext",
}
}
if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil {
return nil, &errortypes.BadInput{
Message: "invalid ext",
}
}
if len(bidderExt.Bidder) == 0 {
return nil, &errortypes.BadInput{
Message: "missing ext.bidder",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unmarshalling error doesn’t always indicate a missing field — it could also result from formatting issues or incorrect data types. We should check for an empty imp.ext and handle such errors gracefully.

}
}
if err := jsonutil.Unmarshal(bidderExt.Bidder, &clydoImpExt); err != nil {
return nil, &errortypes.BadInput{
Message: "invalid ext.bidder",
}
}
return &clydoImpExt, nil
}

func (a *adapter) prepareEndpoint(params *openrtb_ext.ImpExtClydo) (string, error) {
partnerId := params.PartnerId
if partnerId == "" {
return "", &errortypes.BadInput{
Message: "invalid partnerId",
}
}

region := params.Region
if region == "" {
region = "us"
}

endpointParams := macros.EndpointTemplateParams{
PartnerId: partnerId,
Region: region,
}
return macros.ResolveMacros(a.endpoint, endpointParams)
}

func prepareBody(request *openrtb2.BidRequest, imp openrtb2.Imp) ([]byte, error) {
reqCopy := *request
reqCopy.Imp = []openrtb2.Imp{imp}

body, err := jsonutil.Marshal(&reqCopy)
if err != nil {
return nil, err
}

return body, nil
}

func prepareHeaders(request *openrtb2.BidRequest) (http.Header, error) {
allHeaders := map[string]string{
"X-OpenRTB-Version": "2.5",
"Accept": "application/json",
"Content-Type": "application/json; charset=utf-8",
}

allHeaders, err := appendDeviceHeaders(allHeaders, request)
if err != nil {
return nil, err
}

headers := make(http.Header)
for k, v := range allHeaders {
headers.Add(k, v)
}

return headers, nil
}

func appendDeviceHeaders(headers map[string]string, request *openrtb2.BidRequest) (map[string]string, error) {
if request.Device == nil {
return nil, &errortypes.BadInput{Message: "Failed to get device headers"}
}

if ipv6 := request.Device.IPv6; ipv6 != "" {
headers["X-Forwarded-For"] = ipv6
}
if ip := request.Device.IP; ip != "" {
headers["X-Forwarded-For"] = ip
}
if ua := request.Device.UA; ua != "" {
headers["User-Agent"] = ua
}

return headers, nil
}

func prepareImpIds(request *openrtb2.BidRequest) ([]string, error) {
impIds := openrtb_ext.GetImpIDs(request.Imp)
if impIds == nil {
return nil, &errortypes.BadInput{Message: "Failed to get imp ids"}
}
return impIds, nil
}

func prepareBidResponse(body []byte) (openrtb2.BidResponse, error) {
var response openrtb2.BidResponse
if err := jsonutil.Unmarshal(body, &response); err != nil {
return response, err
}
return response, nil
}

func prepareSeatBids(seatBids []openrtb2.SeatBid, bidTypeMap map[string]openrtb_ext.BidType) []*adapters.TypedBid {
var typedBids []*adapters.TypedBid

for _, seatBid := range seatBids {
for i := range seatBid.Bid {
bid := &seatBid.Bid[i]
bidType := getMediaTypeForBid(bid, bidTypeMap)
typedBids = append(typedBids, &adapters.TypedBid{
Bid: bid,
BidType: bidType,
})
}
}

return typedBids
}

func buildBidTypeMap(imps []openrtb2.Imp) (map[string]openrtb_ext.BidType, error) {
bidTypeMap := make(map[string]openrtb_ext.BidType, len(imps))
for _, imp := range imps {
if _, exists := bidTypeMap[imp.ID]; exists {
return nil, &errortypes.BadInput{
Message: "Duplicate impression ID found",
}
}

switch {
case imp.Audio != nil:
bidTypeMap[imp.ID] = openrtb_ext.BidTypeAudio
case imp.Video != nil:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use metadata from the bid to determine the media type instead of relying on the impression? Relying solely on the impression could cause issues in multi-format requests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clydo does not support multi-format requests, so let's stick with impression-based mapping

bidTypeMap[imp.ID] = openrtb_ext.BidTypeVideo
case imp.Native != nil:
bidTypeMap[imp.ID] = openrtb_ext.BidTypeNative
case imp.Banner != nil:
bidTypeMap[imp.ID] = openrtb_ext.BidTypeBanner
default:
return nil, &errortypes.BadInput{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your static/bidder-info/clydo.yaml file lists audio as supported media type but, if a request with an imp.Audio was to come, your buildBidTypeMap(imps []openrtb2.Imp) function would throw an errortypes.BadInput error in line 266. Should audio be removed from the list? OR should it be allowed in buildBidTypeMap(imps []openrtb2.Imp)?

14 capabilities:
15   app:
16     mediaTypes:
17       - banner
18       - video
19       - audio
20       - native
21   site:
22     mediaTypes:
23       - banner
24       - video
25       - audio
26       - native
static/bidder-info/clydo.yaml

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added audio to buildBidTypeMap, clydo can support it

Message: "Failed to get media type",
}
}
}
return bidTypeMap, nil
}

func getMediaTypeForBid(bid *openrtb2.Bid, bidTypeMap map[string]openrtb_ext.BidType) openrtb_ext.BidType {
if mediaType, ok := bidTypeMap[bid.ImpID]; ok {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are imp.ID collisions likely? In other words, is an edge case like the one shown below, where two imp.IDs are the same for two imp elements with different formats, feasible?

req.Imp = []openrtb2.Imp{
    {ID: "sameID", Video: &openrtb2.Video{ /* not-nil */ },
    {ID: "sameID", Banner: &openrtb2.Banner{ /* not-nil */ },
    {ID: "anotherID", Native: &openrtb2.Native{ /* not-nil */ },
}

If this edge case ever presents itself, it seems that the the current logic would overwrite the "sameID" entry in bidTypeMap. bidTypeMap would look like:

bidTypeMap := map[string]openrtb_ext.BidType{
    "sameID":    openrtb_ext.BidTypeBanner,
    "anotherID": openrtb_ext.BidTypeNative,
}

Which would mislabel one bid as a banner, when in reality it is a video. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it's better to validate duplicates

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scenario can occur in the case of a multi-format request. Does Clydo support multi-format requests? If so, would it be better to determine the media type from the bid instead of relying on the impression?

return mediaType
}
return openrtb_ext.BidTypeBanner
}
30 changes: 30 additions & 0 deletions adapters/clydo/clydo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package clydo

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.BidderClydo, config.Adapter{
Endpoint: "http://{{.Region}}.clydo.io/{{.PartnerId}}"},
config.Server{ExternalUrl: "http://hosturl.com"})

if buildErr != nil {
t.Fatalf("Builder returned unexpected error %v", buildErr)
}

adapterstest.RunJSONBidderTest(t, "clydotest", bidder)
}

func TestEndpointTemplateMalformed(t *testing.T) {
_, buildErr := Builder(openrtb_ext.BidderClydo, config.Adapter{
Endpoint: "{{Malformed}}"},
config.Server{ExternalUrl: "http://hosturl.com"})

assert.Error(t, buildErr)
}
Loading
Loading