Skip to content

New Adapter: Start.io #4324

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
179 changes: 179 additions & 0 deletions adapters/startio/startio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package startio

import (
"fmt"
"net/http"
"net/url"
"slices"

"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/openrtb_ext"
"github.com/prebid/prebid-server/v3/util/jsonutil"
)

type adapter struct {
endpoint string
}

func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
uri, err := url.ParseRequestURI(config.Endpoint)
if err != nil {
return nil, err
}

bidder := &adapter{
endpoint: uri.String(),
}

return bidder, nil
}

func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var requests []*adapters.RequestData
var errors []error
requestCopy := *request

if err := validateRequest(requestCopy); err != nil {
return nil, []error{err}
}

validImpressions, err := getValidImpressions(requestCopy.Imp)
if err != nil {
return nil, []error{err}
}
for i := range validImpressions {
requestCopy.Imp = []openrtb2.Imp{validImpressions[i]}

requestBody, err := jsonutil.Marshal(requestCopy)
if err != nil {
errors = append(errors, fmt.Errorf("imp[%d]: failed to marshal request: %w", i, err))
continue
}

requestData := &adapters.RequestData{
Method: http.MethodPost,
Uri: adapter.endpoint,
Body: requestBody,
Headers: buildRequestHeaders(),
ImpIDs: []string{validImpressions[i].ID},
}

requests = append(requests, requestData)
}

return requests, errors
}

func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if response.StatusCode == http.StatusNoContent {
return nil, nil
Comment on lines +71 to +72
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know this is not in the documentation yet but please use the adapter utility function adapters#IsResponseStatusCodeNoContent to check for status 204:

if adapters.IsResponseStatusCodeNoContent(responseData) {
	return nil, nil
}

Comment on lines +71 to +72
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test called status-204.json to cover this case.

} else if response.StatusCode != http.StatusOK {
return nil, []error{wrapReqError(fmt.Sprintf("Unexpected status code: %d.", response.StatusCode))}
}
Comment on lines +73 to +75
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please delete the else if statement. This check is performed by adapters.CheckResponseStatusCodeForErrors on line 77.


if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil {
return nil, []error{err}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test called status-400.json to cover this case.

}

var bidResponse openrtb2.BidResponse
if err := jsonutil.Unmarshal(response.Body, &bidResponse); err != nil {
return nil, []error{wrapReqError(fmt.Sprintf("failed to unmarshal response body: %v", err))}
}

var errs []error
bidderResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResponse.SeatBid))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it better to start with a capacity derived from SeatBid[i].Bid instead?


for i := range bidResponse.SeatBid {
for j := range bidResponse.SeatBid[i].Bid {
bid := &bidResponse.SeatBid[i].Bid[j]
bidType, err := getMediaTypeForBid(*bid)

if err != nil {
errs = append(errs, err)
} else {
bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
Bid: bid,
BidType: bidType,
})
}
}
}

return bidderResponse, errs
}

func validateRequest(request openrtb2.BidRequest) error {
if !isSupportedCurrency(request.Cur) {
return wrapReqError("unsupported currency: only USD is accepted")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test called req-invalid-unsupported-cur.json to cover this case.

}

if !hasSiteOrAppID(request) {
return wrapReqError("request must contain either site.id or app.id")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add supplemental JSON tests called req-invalid-missing-site-id.json and req-invalid-missing-app-id.json to cover this case.

}
Comment on lines +113 to +115
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please delete the hasSiteOrAppID function. Since you've declared support for both site and app in your YAML file, your bidder will only ever be called if site or app is present.


return nil
}

func getValidImpressions(imps []openrtb2.Imp) ([]openrtb2.Imp, error) {
var validImpressions []openrtb2.Imp

for _, imp := range imps {
imp.Audio = nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can delete this line. There's no need to explicitly delete audio. PBS core will strip out any media type objects that your bidder has not declared support for in your YAML file.

hasValidMedia := imp.Banner != nil || imp.Video != nil || imp.Native != nil
if hasValidMedia {
validImpressions = append(validImpressions, imp)
}
}

if len(validImpressions) == 0 {
return nil, wrapReqError("invalid bid request: at least one banner, video, or native impression is required.")
}

return validImpressions, nil
}
Comment on lines +120 to +136
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please delete this function as it is unnecessary. I see that you're performing validation checks to ensure the request contains a media type that you support and also that there is at least one impression. PBS core will only ever call your adapter if there is a valid media type and at least one valid impression.


func hasSiteOrAppID(req openrtb2.BidRequest) bool {
return (req.Site != nil && req.Site.ID != "") || (req.App != nil && req.App.ID != "")
}
Comment on lines +138 to +140
Copy link
Collaborator

Choose a reason for hiding this comment

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

I just want to point that there is already some site and app validation being performed by PBS core. Please see endpoints/openrtb2/auction.go#validateSite and endpoints/openrtb2/auction.go#validateApp to determine if the validation being performed there suits your needs. If it does, you can remove this code as your adapter would never be called if that PBS core validation fails.


func buildRequestHeaders() http.Header {
headers := http.Header{}
headers.Add("Content-Type", "application/json;charset=utf-8")
headers.Add("Accept", "application/json")
headers.Add("X-Openrtb-Version", "2.5")

return headers
}

func isSupportedCurrency(currencies []string) bool {
return len(currencies) == 0 || slices.Contains(currencies, "USD")
}

func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) {

Copy link
Collaborator

Choose a reason for hiding this comment

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

Super nitpick: delete blank line

if bid.Ext != nil {
var bidExt openrtb_ext.ExtBid
err := jsonutil.Unmarshal(bid.Ext, &bidExt)
if err == nil && bidExt.Prebid != nil {
switch bidExt.Prebid.Type {

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.

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. 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.

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. 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.

case "banner":
return openrtb_ext.BidTypeBanner, nil
case "video":
return openrtb_ext.BidTypeVideo, nil
case "native":
return openrtb_ext.BidTypeNative, nil
}
}
}

return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("Failed to parse bid media type for impression %s.", bid.ImpID),
}
Comment on lines +172 to +174
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test called invalid-resp-media-type.json to cover this case. Since you don't support audio I suggest returning that in your response.

}

func wrapReqError(errorStr string) *errortypes.BadInput {
return &errortypes.BadInput{Message: errorStr}
}
Loading
Loading