From c3cca60bfd7e49275c261536d2d302349c798bdc Mon Sep 17 00:00:00 2001 From: Robert Kawecki Date: Thu, 8 Aug 2024 16:53:24 +0200 Subject: [PATCH 1/2] Add AdTonos Exchange bidder adapter --- adapters/adtonos/adtonos.go | 128 ++++++++++++++++++ adapters/adtonos/adtonos_test.go | 30 ++++ .../adtonostest/exemplary/simple-audio.json | 113 ++++++++++++++++ .../adtonostest/exemplary/simple-video.json | 113 ++++++++++++++++ .../wrong-impression-mapping.json | 103 ++++++++++++++ adapters/adtonos/params_test.go | 43 ++++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adtonos.go | 5 + static/bidder-info/adtonos.yaml | 24 ++++ static/bidder-params/adtonos.json | 14 ++ 11 files changed, 577 insertions(+) create mode 100644 adapters/adtonos/adtonos.go create mode 100644 adapters/adtonos/adtonos_test.go create mode 100644 adapters/adtonos/adtonostest/exemplary/simple-audio.json create mode 100644 adapters/adtonos/adtonostest/exemplary/simple-video.json create mode 100644 adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json create mode 100644 adapters/adtonos/params_test.go create mode 100644 openrtb_ext/imp_adtonos.go create mode 100644 static/bidder-info/adtonos.yaml create mode 100644 static/bidder-params/adtonos.json diff --git a/adapters/adtonos/adtonos.go b/adapters/adtonos/adtonos.go new file mode 100644 index 00000000000..b51c50f69fe --- /dev/null +++ b/adapters/adtonos/adtonos.go @@ -0,0 +1,128 @@ +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 := getMediaTypeForImp(seatBid.Bid[i].ImpID, 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 getMediaTypeForImp(responseImpId string, requestImps []openrtb2.Imp) (openrtb_ext.BidType, error) { + for _, requestImp := range requestImps { + if requestImp.ID == responseImpId { + if requestImp.Audio != nil { + return openrtb_ext.BidTypeAudio, nil + } else if requestImp.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } else { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Unsupported bidtype for bid: \"%s\"", responseImpId), + } + } + } + } + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression: \"%s\"", responseImpId), + } +} diff --git a/adapters/adtonos/adtonos_test.go b/adapters/adtonos/adtonos_test.go new file mode 100644 index 00000000000..612e71783be --- /dev/null +++ b/adapters/adtonos/adtonos_test.go @@ -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) +} diff --git a/adapters/adtonos/adtonostest/exemplary/simple-audio.json b/adapters/adtonos/adtonostest/exemplary/simple-audio.json new file mode 100644 index 00000000000..61cac002660 --- /dev/null +++ b/adapters/adtonos/adtonostest/exemplary/simple-audio.json @@ -0,0 +1,113 @@ +{ + "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": "TAG", + "price": 6.5 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/adtonos/adtonostest/exemplary/simple-video.json b/adapters/adtonos/adtonostest/exemplary/simple-video.json new file mode 100644 index 00000000000..f00089b4008 --- /dev/null +++ b/adapters/adtonos/adtonostest/exemplary/simple-video.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "id": "video-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ], + "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": "video-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ], + "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": "video-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json b/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json new file mode 100644 index 00000000000..6fa20e3d3c3 --- /dev/null +++ b/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "correct-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": "correct-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":["correct-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "unexpected-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "Failed to find impression: \"unexpected-impression-id\"", + "comparison": "literal" + } + ] +} diff --git a/adapters/adtonos/params_test.go b/adapters/adtonos/params_test.go new file mode 100644 index 00000000000..98fa85f7b0d --- /dev/null +++ b/adapters/adtonos/params_test.go @@ -0,0 +1,43 @@ +package adtonos + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderAdTonos, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderAdTonos, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"supplierId": ""}`, + `{"supplierId": "7YZxxxdJMSXWv7SwY"}`, +} + +var invalidParams = []string{ + `{"supplierId": 42}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 74fb044ec18..2f282b08cd3 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -26,6 +26,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/adsinteractive" "github.com/prebid/prebid-server/v2/adapters/adtarget" "github.com/prebid/prebid-server/v2/adapters/adtelligent" + "github.com/prebid/prebid-server/v2/adapters/adtonos" "github.com/prebid/prebid-server/v2/adapters/adtrgtme" "github.com/prebid/prebid-server/v2/adapters/advangelists" "github.com/prebid/prebid-server/v2/adapters/adview" @@ -246,6 +247,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAdtarget: adtarget.Builder, openrtb_ext.BidderAdtrgtme: adtrgtme.Builder, openrtb_ext.BidderAdtelligent: adtelligent.Builder, + openrtb_ext.BidderAdTonos: adtonos.Builder, openrtb_ext.BidderAdvangelists: advangelists.Builder, openrtb_ext.BidderAdView: adview.Builder, openrtb_ext.BidderAdxcg: adxcg.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 15113334222..2b6cbcb2cb8 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -43,6 +43,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderAdtarget, BidderAdtrgtme, BidderAdtelligent, + BidderAdTonos, BidderAdvangelists, BidderAdView, BidderAdxcg, @@ -361,6 +362,7 @@ const ( BidderAdsinteractive BidderName = "adsinteractive" BidderAdtarget BidderName = "adtarget" BidderAdtrgtme BidderName = "adtrgtme" + BidderAdTonos BidderName = "adtonos" BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" BidderAdView BidderName = "adview" diff --git a/openrtb_ext/imp_adtonos.go b/openrtb_ext/imp_adtonos.go new file mode 100644 index 00000000000..f59ee35b329 --- /dev/null +++ b/openrtb_ext/imp_adtonos.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ImpExtAdTonos struct { + SupplierID string `json:"supplierId"` +} diff --git a/static/bidder-info/adtonos.yaml b/static/bidder-info/adtonos.yaml new file mode 100644 index 00000000000..37a81372710 --- /dev/null +++ b/static/bidder-info/adtonos.yaml @@ -0,0 +1,24 @@ +endpoint: https://exchange.adtonos.com/bid/{{.PublisherID}} +maintainer: + email: support@adtonos.com +gvlVendorID: 682 +geoscope: + - global +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - audio + # NOTE: This is purely an audio ad exchange - video ads are synthesized from audio ads for compatibility + # with mobile games that only understand video mimetypes. The visual layer is just a placeholder. + - video + site: + mediaTypes: + - audio + dooh: + mediaTypes: + - audio +userSync: + redirect: + url: https://play.adtonos.com/redir?to={{.RedirectURL}} + userMacro: '@UUID@' diff --git a/static/bidder-params/adtonos.json b/static/bidder-params/adtonos.json new file mode 100644 index 00000000000..e43d23e2b4b --- /dev/null +++ b/static/bidder-params/adtonos.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdTonos Adapter Params", + "description": "A schema which validates params accepted by the AdTonos adapter", + + "type": "object", + "properties": { + "supplierId": { + "type": "string", + "description": "ID of the supplier account in AdTonos platform" + } + }, + "required": ["supplierId"] +} \ No newline at end of file From 11475993dc78bb55958c44d247c6aab36d88596f Mon Sep 17 00:00:00 2001 From: Robert Kawecki Date: Mon, 19 Aug 2024 14:27:44 +0200 Subject: [PATCH 2/2] Bidder AdTonos: Add explicit bid.mtype handling --- adapters/adtonos/adtonos.go | 24 +++- .../exemplary/simple-audio-with-mtype.json | 115 ++++++++++++++++++ 2 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json diff --git a/adapters/adtonos/adtonos.go b/adapters/adtonos/adtonos.go index b51c50f69fe..dff60733955 100644 --- a/adapters/adtonos/adtonos.go +++ b/adapters/adtonos/adtonos.go @@ -93,7 +93,7 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R var errors []error for _, seatBid := range response.SeatBid { for i := range seatBid.Bid { - bidType, err := getMediaTypeForImp(seatBid.Bid[i].ImpID, request.Imp) + bidType, err := getMediaTypeForBid(seatBid.Bid[i], request.Imp) if err != nil { errors = append(errors, err) continue @@ -108,21 +108,35 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R return bidResponse, errors } -func getMediaTypeForImp(responseImpId string, requestImps []openrtb2.Imp) (openrtb_ext.BidType, error) { +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 == responseImpId { + if requestImp.ID == bid.ImpID { if requestImp.Audio != nil { return openrtb_ext.BidTypeAudio, nil } else if requestImp.Video != nil { return openrtb_ext.BidTypeVideo, nil } else { return "", &errortypes.BadInput{ - Message: fmt.Sprintf("Unsupported bidtype for bid: \"%s\"", responseImpId), + Message: fmt.Sprintf("Unsupported bidtype for bid: \"%s\"", bid.ImpID), } } } } return "", &errortypes.BadInput{ - Message: fmt.Sprintf("Failed to find impression: \"%s\"", responseImpId), + Message: fmt.Sprintf("Failed to find impression: \"%s\"", bid.ImpID), } } diff --git a/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json b/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json new file mode 100644 index 00000000000..c9103e37ee4 --- /dev/null +++ b/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json @@ -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": "TAG", + "price": 6.5, + "mtype": 3 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5, + "mtype": 3 + }, + "type": "audio" + } + ] + } + ] +}