-
Notifications
You must be signed in to change notification settings - Fork 760
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
New Adapter: Adnuntius #2014
Changes from all commits
90adf68
8b5bb30
2ef05d3
57f824e
ef6d3b7
c21586a
b5d7833
2825270
9444bff
82ddf79
524d0b3
41bf3ff
cbddd69
25fefef
a651cce
6c5fe81
176ef4b
e4a5700
6f8ee16
2f3b0b7
f1682e1
0426a7e
1aedd36
5838ca3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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()), | ||
}} | ||
} | ||
|
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we only deal in 400 or 200 for this endpoint. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are the destination URLs always going to start with There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
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), | ||
} | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 justerror
and return it immediately.Edit: Please check same thing in other functions