Skip to content
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
216 changes: 216 additions & 0 deletions adapters/rtbstack/rtbstack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package rtbstack

import (
"encoding/json"
"fmt"
"net/http"
"strings"
"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
}

// impCtx represents the context containing an OpenRTB impression and its corresponding RTBStack extension configuration.
type impCtx struct {
imp openrtb2.Imp
rtbStackExt *openrtb_ext.ExtImpRTBStack
}

// extImpRTBStack is used for imp->ext when sending to rtb-stack backend
type extImpRTBStack struct {
TagId string `json:"tagid"`
CustomParams map[string]interface{} `json:"customParams,omitempty"`
}

func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
tpl, err := template.New("endpointTemplate").Parse(config.Endpoint)
if err != nil {
return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
}

bidder := &adapter{
endpoint: tpl,
}
return bidder, nil
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
if len(request.Imp) == 0 {
return nil, []error{&errortypes.BadInput{
Message: "No impressions in request",
}}
}

var errs []error
var validImps []*impCtx

for _, imp := range request.Imp {
ext, err := preprocessImp(&imp)
if err != nil {
errs = append(errs, err)
continue
}

validImps = append(validImps, &impCtx{
imp: imp,
rtbStackExt: ext,
})
}

if len(validImps) == 0 {
return nil, errs
}

request.Imp = nil
for _, v := range validImps {
request.Imp = append(request.Imp, v.imp)
}

endpoint, err := a.buildEndpointURL(validImps[0].rtbStackExt)
if err != nil {
return nil, []error{err}
}

var newRequest openrtb2.BidRequest
newRequest = *request

if request.Site != nil && request.Site.Domain == "" {
newSite := *request.Site
newSite.Domain = request.Site.Page
newRequest.Site = &newSite
}

reqJSON, err := json.Marshal(newRequest)
if err != nil {
return nil, []error{&errortypes.BadInput{
Message: "Error parsing reqJSON object",
}}
}

headers := http.Header{}
headers.Add("Content-Type", "application/json;charset=utf-8")
headers.Add("Accept", "application/json")
return []*adapters.RequestData{{
Method: "POST",
Uri: endpoint,
Body: reqJSON,
Headers: headers,
ImpIDs: openrtb_ext.GetImpIDs(request.Imp),
}}, []error{}
}

func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if response.StatusCode == http.StatusNoContent {
return nil, nil
}

if response.StatusCode != http.StatusOK {
return nil, []error{fmt.Errorf("Unexpected status code: %d", response.StatusCode)}
}

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

if len(bidResp.SeatBid) == 0 || len(bidResp.SeatBid[0].Bid) == 0 {
return nil, nil
}

bidResponse := adapters.NewBidderResponseWithBidsCapacity(5)

for _, sb := range bidResp.SeatBid {
for i := 0; i < len(sb.Bid); i++ {
bid := sb.Bid[i]
bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: &bid,
BidType: getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp),
})

}
}
return bidResponse, []error{}
}

func (a *adapter) buildEndpointURL(ext *openrtb_ext.ExtImpRTBStack) (string, error) {
// Normalize host to avoid accidental protocol prefixes and domains accidentally included
host := ext.Host
host = strings.TrimPrefix(host, "http://")
host = strings.TrimPrefix(host, "https://")

endpointParams := macros.EndpointTemplateParams{Host: host}
baseURL, err := macros.ResolveMacros(a.endpoint, endpointParams)
if err != nil {
return "", fmt.Errorf("unable to resolve endpoint: %v", err)
}

if ext.Query == "" {
return baseURL, nil
}

if strings.HasPrefix(ext.Query, "/") {
return baseURL + ext.Query, nil
}

return baseURL + "/" + ext.Query, nil
}

func preprocessImp(
imp *openrtb2.Imp,
) (*openrtb_ext.ExtImpRTBStack, error) {
var bidderExt adapters.ExtImpBidder
if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil {
return nil, &errortypes.BadInput{Message: err.Error()}
}

var impExt openrtb_ext.ExtImpRTBStack
if err := jsonutil.Unmarshal(bidderExt.Bidder, &impExt); err != nil {
return nil, &errortypes.BadInput{
Message: "Wrong RTBStack bidder ext",
}
}

imp.TagID = impExt.TagId

// create new imp->ext without odd params
newExt := extImpRTBStack{
TagId: impExt.TagId,
CustomParams: impExt.CustomParams,
}

// simplify content from imp->ext->bidder to imp->ext
newImpExtForRTBStack, err := json.Marshal(newExt)
if err != nil {
return nil, &errortypes.BadInput{Message: err.Error()}
}
imp.Ext = newImpExtForRTBStack

return &impExt, nil
}

func getMediaTypeForImp(impID string, imps []openrtb2.Imp) openrtb_ext.BidType {
for _, imp := range imps {
if imp.ID == impID {
if imp.Banner != 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.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.

return openrtb_ext.BidTypeBanner
} else if imp.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. 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
} else if imp.Native != 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.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.

return openrtb_ext.BidTypeNative
} else if imp.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. 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.BidTypeAudio
}
}
}
return openrtb_ext.BidTypeBanner
}
19 changes: 19 additions & 0 deletions adapters/rtbstack/rtbstack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package rtbstack

import (
"github.com/prebid/prebid-server/v3/adapters/adapterstest"
"github.com/prebid/prebid-server/v3/config"
"github.com/prebid/prebid-server/v3/openrtb_ext"
"testing"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderRTBStack, config.Adapter{
Endpoint: "https://{{.Host}}.rtb-stack.com"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})

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

adapterstest.RunJSONBidderTest(t, "rtbstacktest", bidder)
}
150 changes: 150 additions & 0 deletions adapters/rtbstack/rtbstacktest/exemplary/app-audio.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
{
"mockBidRequest": {
"id": "test-request-id",
"app": {
"bundle": "com.prebid.app"
},
"device": {
"ifa": "ec943cb9-61ec-460f-a925-6489c3fcc4e3"
},
"imp": [
{
"id": "imp-app-audio-short",
"audio": {
"mimes": ["audio/mpeg", "audio/mp4"],
"minduration": 5,
"maxduration": 30
},
"ext": {
"bidder": {
"host": "test",
"query": "prebid?client=c4527281-5aa5-4c8e-bc53-a80bb3f99470&endpoint=309&ssp=145",
"tagId": "app-audio-short",
"customParams": {
"foo": "bar"
}
}
}
},
{
"id": "imp-app-audio-long",
"audio": {
"mimes": ["audio/mpeg", "audio/mp4"],
"minduration": 15,
"maxduration": 60
},
"ext": {
"bidder": {
"host": "test",
"query": "prebid?client=c4527281-5aa5-4c8e-bc53-a80bb3f99470&endpoint=309&ssp=145",
"tagId": "app-audio-long",
"customParams": {}
}
}
}
]
},

"httpCalls": [
{
"expectedRequest": {
"uri": "https://test.rtb-stack.com/prebid?client=c4527281-5aa5-4c8e-bc53-a80bb3f99470&endpoint=309&ssp=145",
"body": {
"id": "test-request-id",
"app": {
"bundle": "com.prebid.app"
},
"device": {
"ifa": "ec943cb9-61ec-460f-a925-6489c3fcc4e3"
},
"imp": [
{
"id": "imp-app-audio-short",
"audio": {
"mimes": ["audio/mpeg", "audio/mp4"],
"minduration": 5,
"maxduration": 30
},
"tagid": "app-audio-short",
"ext": {
"tagid": "app-audio-short",
"customParams": {
"foo": "bar"
}
}
},
{
"id": "imp-app-audio-long",
"audio": {
"mimes": ["audio/mpeg", "audio/mp4"],
"minduration": 15,
"maxduration": 60
},
"tagid": "app-audio-long",
"ext": {
"tagid": "app-audio-long"
}
}
]
},
"impIDs": ["imp-app-audio-short", "imp-app-audio-long"]
},
"mockResponse": {
"status": 200,
"body": {
"id": "test-request-id",
"seatbid": [
{
"seat": "rtbstack",
"bid": [
{
"id": "bid-app-audio-short-1",
"impid": "imp-app-audio-short",
"price": 0.75,
"adm": "<audio src='short.mp3'></audio>",
"crid": "crid-app-audio-short-1"
},
{
"id": "bid-app-audio-long-1",
"impid": "imp-app-audio-long",
"price": 1.25,
"adm": "<audio src='long.mp3'></audio>",
"crid": "crid-app-audio-long-1"
}
]
}
],
"cur": "USD"
}
}
}
],

"expectedBidResponses": [
{
"currency": "USD",
"bids": [
{
"bid": {
"id": "bid-app-audio-short-1",
"impid": "imp-app-audio-short",
"price": 0.75,
"adm": "<audio src='short.mp3'></audio>",
"crid": "crid-app-audio-short-1"
},
"type": "audio"
},
{
"bid": {
"id": "bid-app-audio-long-1",
"impid": "imp-app-audio-long",
"price": 1.25,
"adm": "<audio src='long.mp3'></audio>",
"crid": "crid-app-audio-long-1"
},
"type": "audio"
}
]
}
]
}
Loading
Loading