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: Adnuntius #2014

Merged
merged 24 commits into from
Oct 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
90adf68
Making adnuntius bidder working.
mikael-lundin Sep 13, 2021
8b5bb30
Testing that container works.
mikael-lundin Sep 14, 2021
2ef05d3
Tests work but fail.
mikael-lundin Sep 22, 2021
57f824e
Tzo added to the request.
mikael-lundin Sep 23, 2021
ef6d3b7
Tests for adnuntius adapter.
mikael-lundin Sep 24, 2021
c21586a
Gdpr Added to adapter.
mikael-lundin Sep 24, 2021
b5d7833
Merge pull request #1 from Adnuntius/consent
mikael-lundin Sep 24, 2021
2825270
Fixing commented issues in pull request.
mikael-lundin Oct 5, 2021
9444bff
Making adnuntius bidder working.
mikael-lundin Sep 13, 2021
82ddf79
Testing that container works.
mikael-lundin Sep 14, 2021
524d0b3
Tests work but fail.
mikael-lundin Sep 22, 2021
41bf3ff
Tzo added to the request.
mikael-lundin Sep 23, 2021
cbddd69
Tests for adnuntius adapter.
mikael-lundin Sep 24, 2021
25fefef
Gdpr Added to adapter.
mikael-lundin Sep 24, 2021
a651cce
Fixing commented issues in pull request.
mikael-lundin Oct 5, 2021
6c5fe81
Changes made in response to comments in pull request.
mikael-lundin Oct 14, 2021
176ef4b
Merge branch 'master' of github.com:Adnuntius/prebid-server
mikael-lundin Oct 14, 2021
e4a5700
Adding constants.
mikael-lundin Oct 14, 2021
6f8ee16
Validation issue fixed.
mikael-lundin Oct 14, 2021
2f3b0b7
Adding handling of 400 error, removing timezone file.
mikael-lundin Oct 14, 2021
f1682e1
Changed from errortypes.BadInput to errortypes.BadServerResponse.
mikael-lundin Oct 19, 2021
0426a7e
Merge remote-tracking branch 'upstream/master'
mikael-lundin Oct 28, 2021
1aedd36
Made changes based on comments
mikael-lundin Oct 28, 2021
5838ca3
Changed error type to Bad input for url-parsing.
mikael-lundin Oct 29, 2021
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
303 changes: 303 additions & 0 deletions adapters/adnuntius/adnuntius.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
package adnuntius

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/mxmCherry/openrtb/v15/openrtb2"
"github.com/prebid/prebid-server/adapters"
"github.com/prebid/prebid-server/config"
"github.com/prebid/prebid-server/errortypes"
"github.com/prebid/prebid-server/openrtb_ext"
"github.com/prebid/prebid-server/util/timeutil"
)

type QueryString map[string]string
type adapter struct {
time timeutil.Time
endpoint string
}
type adnAdunit struct {
AuId string `json:"auId"`
TargetId string `json:"targetId"`
}

type AdnResponse struct {
AdUnits []struct {
AuId string
TargetId string
Html string
ResponseId string
Ads []struct {
Bid struct {
Amount float64
Currency string
}
AdId string
CreativeWidth string
CreativeHeight string
CreativeId string
LineItemId string
Html string
DestinationUrls map[string]string
}
}
}
type adnMetaData struct {
Usi string `json:"usi,omitempty"`
}
type adnRequest struct {
AdUnits []adnAdunit `json:"adUnits"`
MetaData adnMetaData `json:"metaData,omitempty"`
Context string `json:"context,omitempty"`
}

const defaultNetwork = "default"
const defaultSite = "unknown"
const minutesInHour = 60

// Builder builds a new instance of the Adnuntius adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) {
bidder := &adapter{
time: &timeutil.RealTime{},
endpoint: config.Endpoint,
}

return bidder, nil
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
return a.generateRequests(*request)
}

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

func makeEndpointUrl(ortbRequest openrtb2.BidRequest, a *adapter) (string, []error) {
uri, err := url.Parse(a.endpoint)
if err != nil {
return "", []error{fmt.Errorf("failed to parse Adnuntius endpoint: %v", err)}
}

gdpr, consent, err := getGDPR(&ortbRequest)
if err != nil {
return "", []error{fmt.Errorf("failed to parse Adnuntius endpoint: %v", err)}
}

_, offset := a.time.Now().Zone()
tzo := -offset / minutesInHour

q := uri.Query()
if gdpr != "" && consent != "" {
q.Set("gdpr", gdpr)
q.Set("consentString", consent)
}
q.Set("tzo", fmt.Sprint(tzo))
q.Set("format", "json")

url := a.endpoint + "?" + q.Encode()
return url, nil
}

/*
Generate the requests to Adnuntius to reduce the amount of requests going out.
*/
func (a *adapter) generateRequests(ortbRequest openrtb2.BidRequest) ([]*adapters.RequestData, []error) {
var requestData []*adapters.RequestData
networkAdunitMap := make(map[string][]adnAdunit)
headers := setHeaders()

endpoint, err := makeEndpointUrl(ortbRequest, a)
if err != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("failed to parse URL: %s", err),
}}
}

for _, imp := range ortbRequest.Imp {
if imp.Banner == nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("ignoring imp id=%s, Adnuntius supports only Banner", imp.ID),
}}
}
var bidderExt adapters.ExtImpBidder
if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Error unmarshalling ExtImpBidder: %s", err.Error()),
}}
}

var adnuntiusExt openrtb_ext.ImpExtAdnunitus
if err := json.Unmarshal(bidderExt.Bidder, &adnuntiusExt); err != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Error unmarshalling ExtImpBmtm: %s", err.Error()),
}}
}
Copy link
Contributor

@VeronikaSolovei9 VeronikaSolovei9 Oct 12, 2021

Choose a reason for hiding this comment

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

In case of error occurs, lines 128, 134 and 141 - should you just return an error or skip to next imp?
Edit: Similar to what you do in MakeBids

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hopefully I've done it as you meant it in my coming commit.

Copy link
Contributor

@VeronikaSolovei9 VeronikaSolovei9 Oct 17, 2021

Choose a reason for hiding this comment

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

Thank you for the update.
Please check it one more time. You probably don't want to collect all errors. Instead return after first error occurrence. If so please modify return []error to just error and return it immediately.
Edit: Please check same thing in other functions


network := defaultNetwork
if adnuntiusExt.Network != "" {
network = adnuntiusExt.Network
bsardo marked this conversation as resolved.
Show resolved Hide resolved
}

networkAdunitMap[network] = append(
networkAdunitMap[network],
adnAdunit{
AuId: adnuntiusExt.Auid,
TargetId: fmt.Sprintf("%s-%s", adnuntiusExt.Auid, imp.ID),
})
}

site := defaultSite
if ortbRequest.Site != nil && ortbRequest.Site.Page != "" {
site = ortbRequest.Site.Page
bsardo marked this conversation as resolved.
Show resolved Hide resolved
}

for _, networkAdunits := range networkAdunitMap {

adnuntiusRequest := adnRequest{
AdUnits: networkAdunits,
Context: site,
}

ortbUser := ortbRequest.User
if ortbUser != nil {
ortbUserId := ortbRequest.User.ID
if ortbUserId != "" {
adnuntiusRequest.MetaData.Usi = ortbRequest.User.ID
}
}

adnJson, err := json.Marshal(adnuntiusRequest)
if err != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Error unmarshalling adnuntius request: %s", err.Error()),
}}
}

requestData = append(requestData, &adapters.RequestData{
Method: http.MethodPost,
Uri: endpoint,
Body: adnJson,
Headers: headers,
})

}

return requestData, nil
}

func (a *adapter) MakeBids(request *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {

if response.StatusCode == http.StatusBadRequest {
bsardo marked this conversation as resolved.
Show resolved Hide resolved
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Status code: %d, Request malformed", response.StatusCode),
}}
}

if response.StatusCode != http.StatusOK {
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 test for when the status is some other error not specifically handled in the conditionals above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we only deal in 400 or 200 for this endpoint. :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

As you mentioned, you want to handle 400 errors so you should return the BadInput error in that case, but you should also have a catch all in case another status code is returned. In that case if response.StatusCode != http.StatusOK makes sense. I would think that this block would be primarily to handle bad/unexpected behavior on your remote server (5xx) by returning the BadServerResponse error.

return nil, []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("Status code: %d, Something went wrong with your request", response.StatusCode),
}}
}

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

bidResponse, bidErr := generateBidResponse(&adnResponse, request)
if bidErr != nil {
return nil, bidErr
}

return bidResponse, nil
}

func getGDPR(request *openrtb2.BidRequest) (string, string, error) {
gdpr := ""
var extRegs openrtb_ext.ExtRegs
if request.Regs != nil {
if err := json.Unmarshal(request.Regs.Ext, &extRegs); err != nil {
return "", "", fmt.Errorf("failed to parse ExtRegs in Adnuntius GDPR check: %v", err)
}
if extRegs.GDPR != nil && (*extRegs.GDPR == 0 || *extRegs.GDPR == 1) {
gdpr = strconv.Itoa(int(*extRegs.GDPR))
}
}

consent := ""
if request.User != nil && request.User.Ext != nil {
var extUser openrtb_ext.ExtUser
if err := json.Unmarshal(request.User.Ext, &extUser); err != nil {
return "", "", fmt.Errorf("failed to parse ExtUser in Adnuntius GDPR check: %v", err)
}
consent = extUser.Consent
}

return gdpr, consent, nil
}

func generateBidResponse(adnResponse *AdnResponse, request *openrtb2.BidRequest) (*adapters.BidderResponse, []error) {
bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(adnResponse.AdUnits))
var currency string

for i, adunit := range adnResponse.AdUnits {

if len(adunit.Ads) > 0 {

ad := adunit.Ads[0]

currency = ad.Bid.Currency
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm curious about what you're doing with currency. Currency is set on the bidResponse. It looks like you iterate over all ad units and only process the first ad for an adunit. You're then setting the currency to that ad's bid currency. That means that the currency you select will be the currency of the last ad you process since you're assigning currency to bidResponse.currency on line 298 after you're done iterating over all adunits. This should be fine if it is guaranteed all ad bids will have currency set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, this is just me getting around how our ad server works. there will always be a currency set in the ad unit response.


creativeWidth, widthErr := strconv.ParseInt(ad.CreativeWidth, 10, 64)
if widthErr != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Value of width: %s is not a string", ad.CreativeWidth),
}}
}

creativeHeight, heightErr := strconv.ParseInt(ad.CreativeHeight, 10, 64)
if heightErr != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Value of height: %s is not a string", ad.CreativeHeight),
}}
}

adDomain := []string{}
for _, url := range ad.DestinationUrls {
domainArray := strings.Split(url, "/")
domain := strings.Replace(domainArray[2], "www.", "", -1)
adDomain = append(adDomain, domain)
Comment on lines +276 to +278
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are the destination URLs always going to start with http(s)://?
Nitpick: maybe split on // and grab domainArray[1] instead? IMO that seems a little more straightforward.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that seems like a good idea but I got the impression that i was only allowed to send the top level domain in that field? :D If not I can just stop what that stuff is doing and just pass the entire url. And yes, we force the url to have http(s)://

}

bid := openrtb2.Bid{
ID: ad.AdId,
ImpID: request.Imp[i].ID,
W: creativeWidth,
H: creativeHeight,
AdID: ad.AdId,
CID: ad.LineItemId,
CrID: ad.CreativeId,
Price: ad.Bid.Amount * 1000,
AdM: adunit.Html,
ADomain: adDomain,
}

bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: &bid,
BidType: "banner",
})
}

}
bidResponse.Currency = currency
return bidResponse, nil
}
46 changes: 46 additions & 0 deletions adapters/adnuntius/adnuntius_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package adnuntius

import (
"testing"
"time"

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

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderAdnuntius, config.Adapter{
Endpoint: "http://whatever.url"})

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

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

func assertTzo(t *testing.T, bidder adapters.Bidder) {
bidderAdnuntius, _ := bidder.(*adapter)
assert.NotNil(t, bidderAdnuntius.time)
}

// FakeTime implements the Time interface
type FakeTime struct {
time time.Time
}

func (ft *FakeTime) Now() time.Time {
return ft.time
}

func replaceRealTimeWithKnownTime(bidder adapters.Bidder) {
bidderAdnuntius, _ := bidder.(*adapter)
bidderAdnuntius.time = &FakeTime{
time: time.Date(2016, 1, 1, 12, 30, 15, 0, time.UTC),
}
}
Loading