Skip to content
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: AdTonos #3853

Merged
merged 2 commits into from
Sep 12, 2024
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
142 changes: 142 additions & 0 deletions adapters/adtonos/adtonos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package adtonos

import (
"encoding/json"
"fmt"
"net/http"
"text/template"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v2/adapters"
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/errortypes"
"github.com/prebid/prebid-server/v2/macros"
"github.com/prebid/prebid-server/v2/openrtb_ext"
)

type adapter struct {
endpointTemplate *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("unable to parse endpoint url template: %v", err)
}

bidder := &adapter{
endpointTemplate: template,
}
return bidder, nil
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var bidderExt adapters.ExtImpBidder
if err := json.Unmarshal(request.Imp[0].Ext, &bidderExt); err != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Invalid imp.ext for impression index %d. Error Infomation: %s", 0, err.Error()),
}}
}
var impExt openrtb_ext.ImpExtAdTonos
if err := json.Unmarshal(bidderExt.Bidder, &impExt); err != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Invalid imp.ext.bidder for impression index %d. Error Infomation: %s", 0, err.Error()),
}}
}

endpoint, err := a.buildEndpointURL(&impExt)
if err != nil {
return nil, []error{err}
}

requestJson, err := json.Marshal(request)
if err != nil {
return nil, []error{err}
}

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

requestData := &adapters.RequestData{
Method: "POST",
Uri: endpoint,
Body: requestJson,
Headers: headers,
ImpIDs: openrtb_ext.GetImpIDs(request.Imp),
}

return []*adapters.RequestData{requestData}, nil
}

func (a *adapter) buildEndpointURL(params *openrtb_ext.ImpExtAdTonos) (string, error) {
endpointParams := macros.EndpointTemplateParams{PublisherID: params.SupplierID}
return macros.ResolveMacros(a.endpointTemplate, endpointParams)
}

func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if adapters.IsResponseStatusCodeNoContent(responseData) {
return nil, nil
}

if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
return nil, []error{err}
}

var response openrtb2.BidResponse
if err := json.Unmarshal(responseData.Body, &response); err != nil {
return nil, []error{err}
}

bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
bidResponse.Currency = response.Cur
var errors []error
for _, seatBid := range response.SeatBid {
for i := range seatBid.Bid {
bidType, err := getMediaTypeForBid(seatBid.Bid[i], request.Imp)
if err != nil {
errors = append(errors, err)
continue
}
b := &adapters.TypedBid{
Bid: &seatBid.Bid[i],
BidType: bidType,
}
bidResponse.Bids = append(bidResponse.Bids, b)
}
}
return bidResponse, errors
}

func getMediaTypeForBid(bid openrtb2.Bid, requestImps []openrtb2.Imp) (openrtb_ext.BidType, error) {
if bid.MType != 0 {
// If present, use explicit markup type annotation from the bidder:
switch bid.MType {
case openrtb2.MarkupAudio:
return openrtb_ext.BidTypeAudio, nil
case openrtb2.MarkupVideo:
return openrtb_ext.BidTypeVideo, nil
case openrtb2.MarkupBanner:
return openrtb_ext.BidTypeBanner, nil
case openrtb2.MarkupNative:
return openrtb_ext.BidTypeNative, nil
}
}
// As a fallback, guess markup type based on requested type - AdTonos is an audio company so we prioritize that.
for _, requestImp := range requestImps {
if requestImp.ID == bid.ImpID {
if requestImp.Audio != nil {

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Leaving this here because AdTonos is an audio ad exchange - we incidentally do audio-as-video for publishers which absolutely cannot play an audio ad due to technical constraints, but really we just want to return VASTs with sound only.)

Copy link
Contributor

Choose a reason for hiding this comment

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

@rkaw92 what bot is suggesting is to use Mtype field in the response to decide returned mediaType for an impression instead of relying on impID. For example,

func getBidType(bid openrtb2.Bid) (openrtb_ext.BidType, error) {
	// determinate media type by bid response field mtype
	switch bid.MType {
	case openrtb2.MarkupBanner:
		return openrtb_ext.BidTypeBanner, nil
	case openrtb2.MarkupVideo:
		return openrtb_ext.BidTypeVideo, nil
	case openrtb2.MarkupAudio:
		return openrtb_ext.BidTypeAudio, nil
	case openrtb2.MarkupNative:
		return openrtb_ext.BidTypeNative, nil
	}

	return "", &errortypes.BadInput{
		Message: fmt.Sprintf("Could not define media type for impression: %s", bid.ImpID),
	}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, makes sense. We don't have a way of always returning mtype currently, but I can add logic that will optionally use it when present, and fall back to the current heuristic if not. In the future, when mtype coverage increases, the code will have anticipated it.

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've added handling for when mtype is explicitly indicated and left the guesstimation in place as a fallback, does that look better?

return openrtb_ext.BidTypeAudio, nil
} else if requestImp.Video != nil {

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the media type defaults to openrtb_ext.BidTypeVideo, nil.

I think the review bot may be wrong here - we default to audio, not video.

Copy link
Contributor

Choose a reason for hiding this comment

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

return openrtb_ext.BidTypeVideo, nil
} else {
return "", &errortypes.BadInput{
Message: fmt.Sprintf("Unsupported bidtype for bid: \"%s\"", bid.ImpID),
}
}
}
}
return "", &errortypes.BadInput{
Message: fmt.Sprintf("Failed to find impression: \"%s\"", bid.ImpID),
}
}
30 changes: 30 additions & 0 deletions adapters/adtonos/adtonos_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package adtonos

import (
"testing"

"github.com/prebid/prebid-server/v2/adapters/adapterstest"
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/openrtb_ext"
"github.com/stretchr/testify/assert"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderAdTonos, config.Adapter{
Endpoint: "http://exchange.example.com/bid/{{.PublisherID}}"},
config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})

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

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

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

assert.Error(t, buildErr)
}
115 changes: 115 additions & 0 deletions adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"mockBidRequest": {
"id": "some-request-id",
"tmax": 1000,
"imp": [
{
"id": "some-impression-id",
"bidfloor": 4.2,
"ext": {
"bidder": {
"supplierId": "777XYZ123"
}
},
"audio": {
"mimes": [
"audio/mpeg"
]
}
}
],
"test": 1,
"site": {
"publisher": {
"id": "1"
},
"page": "http://www.example.com",
"domain": "www.example.com"
},
"device": {}
},
"httpCalls": [
{
"expectedRequest": {
"uri": "http://exchange.example.com/bid/777XYZ123",
"headers": {
"Content-Type": [
"application/json;charset=utf-8"
],
"Accept": [
"application/json"
]
},
"body": {
"id": "some-request-id",
"tmax": 1000,
"imp": [
{
"id": "some-impression-id",
"bidfloor": 4.2,
"ext": {
"bidder": {
"supplierId": "777XYZ123"
}
},
"audio": {
"mimes": [
"audio/mpeg"
]
}
}
],
"site": {
"publisher": {
"id": "1"
},
"page": "http://www.example.com",
"domain": "www.example.com"
},
"device": {},
"test": 1
},
"impIDs":["some-impression-id"]
},
"mockResponse": {
"status": 200,
"body": {
"id": "some-request-id",
"cur": "USD",
"seatbid": [
{
"bid": [
{
"id": "1",
"impid": "some-impression-id",
"crid": "some-creative-id",
"adm": "<VAST>TAG</VAST>",
"price": 6.5,
"mtype": 3
}
]
}
]
}
}
}
],
"expectedBidResponses": [
{
"currency": "USD",
"bids": [
{
"bid": {
"id": "1",
"impid": "some-impression-id",
"crid": "some-creative-id",
"adm": "<VAST>TAG</VAST>",
"price": 6.5,
"mtype": 3
},
"type": "audio"
}
]
}
]
}
Loading
Loading