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

Introduce Adhese adapter #1292

Merged
merged 34 commits into from
May 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
87e1e8c
Initial commit
sander-adhese Mar 9, 2020
2a179f3
it really really really wants an uri :rolling eyes:
sander-adhese Mar 9, 2020
b4e8def
Some minor changes. Update test data with fields necessary to succeed…
sander-adhese Mar 9, 2020
9d8ab63
anonymize
sander-adhese Mar 9, 2020
8e186ce
Base tests pass
sander-adhese Mar 24, 2020
678e903
Supply IDs
sander-adhese Mar 25, 2020
11096e3
Merge pull request #1 from adhese/add_adhese_bidder
sander-adhese Apr 15, 2020
ec7cd73
revert custom error
sander-adhese Apr 15, 2020
86d9913
Merge branch 'add_adhese_bidder'
sander-adhese Apr 15, 2020
7ffd1ce
Merge branch 'master' into master
sander-adhese Apr 15, 2020
99a0cb6
Place adapter entry is correct place
sander-adhese Apr 16, 2020
27f73ac
added adhese as non-sync bidder
sander-adhese Apr 16, 2020
d4503b6
Use actual template logic for url instead, adjust test to prove templ…
sander-adhese Apr 21, 2020
c57e4e3
Host = url parts + account
sander-adhese Apr 23, 2020
0c4a138
Empty body will do
sander-adhese Apr 23, 2020
1eef713
Simplify Adhese objs
sander-adhese Apr 23, 2020
d0fad99
Get rid of body alltogether
sander-adhese Apr 23, 2020
9a475b0
Process feedback
sander-adhese Apr 24, 2020
686ebc5
Simplify some more
sander-adhese Apr 24, 2020
b419a80
Cleanup, implement feedback
sander-adhese Apr 30, 2020
630193d
Urlencode referrer, deduplicote some code
mefjush May 6, 2020
a1541e6
Indentation fix
mefjush May 6, 2020
af19d47
Merge pull request #2 from adhese/master
sander-adhese May 7, 2020
b75e6fd
Merge pull request #3 from adhese/prebid-master
sander-adhese May 7, 2020
4d17eb9
Fix tests
mefjush May 7, 2020
80b7178
Merge remote-tracking branch 'prebid/master'
sander-adhese May 8, 2020
a9a6125
Fix indentation
sander-adhese May 8, 2020
c4ef1ba
Process feedback
sander-adhese May 12, 2020
fa92cdb
Add some more test coverage
sander-adhese May 12, 2020
16e287b
Increase code coverage
sander-adhese May 12, 2020
b6fccfe
Apply optimization suggestion
sander-adhese May 13, 2020
b5765fc
Process feedback
sander-adhese May 27, 2020
210f3f4
Fix config, add check on impression counter
sander-adhese May 28, 2020
8451e93
Add test + make test pass in go 1.12
sander-adhese May 28, 2020
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
269 changes: 269 additions & 0 deletions adapters/adhese/adhese.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
package adhese

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

"github.com/golang/glog"
"github.com/mxmCherry/openrtb"
"github.com/prebid/prebid-server/adapters"
"github.com/prebid/prebid-server/errortypes"
"github.com/prebid/prebid-server/macros"
"github.com/prebid/prebid-server/openrtb_ext"
)

type AdheseAdapter struct {
endpointTemplate template.Template
}

func extractSlotParameter(parameters openrtb_ext.ExtImpAdhese) string {
return fmt.Sprintf("/sl%s-%s", url.PathEscape(parameters.Location), url.PathEscape(parameters.Format))
}

func extractTargetParameters(parameters openrtb_ext.ExtImpAdhese) string {
if len(parameters.Keywords) == 0 {
return ""
}
var parametersAsString = ""
var targetParsed map[string]interface{}
err := json.Unmarshal(parameters.Keywords, &targetParsed)
if err != nil {
return ""
}

targetKeys := make([]string, 0, len(targetParsed))
for key := range targetParsed {
targetKeys = append(targetKeys, key)
}
sort.Strings(targetKeys)

for _, targetKey := range targetKeys {
var targetingValues = targetParsed[targetKey].([]interface{})
parametersAsString += "/" + url.PathEscape(targetKey)
for _, targetRawValKey := range targetingValues {
var targetValueParsed = targetRawValKey.(string)
parametersAsString += targetValueParsed + ";"
}
parametersAsString = strings.TrimRight(parametersAsString, ";")
}

return parametersAsString
}

func extractGdprParameter(request *openrtb.BidRequest) string {
if request.User != nil {
var extUser openrtb_ext.ExtUser
if err := json.Unmarshal(request.User.Ext, &extUser); err == nil {
return "/xt" + extUser.Consent
}
}
return ""
}

func extractRefererParameter(request *openrtb.BidRequest) string {
if request.Site != nil && request.Site.Page != "" {
return "/xf" + url.QueryEscape(request.Site.Page)
}
return ""
}

func (a *AdheseAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
errs := make([]error, 0, len(request.Imp))

var err error

// If all the requests are invalid, Call to adaptor is skipped
if len(request.Imp) == 0 {
errs = append(errs, WrapReqError("Imp is empty"))
return nil, errs
}

var imp = &request.Imp[0]
var bidderExt adapters.ExtImpBidder

if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
errs = append(errs, WrapReqError("Request could not be parsed as ExtImpBidder due to: "+err.Error()))
return nil, errs
}

var params openrtb_ext.ExtImpAdhese
if err := json.Unmarshal(bidderExt.Bidder, &params); err != nil {
errs = append(errs, WrapReqError("Request could not be parsed as ExtImpAdhese due to: "+err.Error()))
return nil, errs
}

// Compose url
endpointParams := macros.EndpointTemplateParams{AccountID: params.Account}

host, err := macros.ResolveMacros(*&a.endpointTemplate, endpointParams)
if err != nil {
errs = append(errs, WrapReqError("Could not compose url from template and request account val: "+err.Error()))
return nil, errs
}
complete_url := fmt.Sprintf("%s%s%s%s%s",
host,
extractSlotParameter(params),
extractTargetParameters(params),
extractGdprParameter(request),
extractRefererParameter(request))

return []*adapters.RequestData{{
Method: "GET",
Uri: complete_url,
}}, errs
}

func (a *AdheseAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if response.StatusCode == http.StatusNoContent {
return nil, nil
} else if response.StatusCode != http.StatusOK {
return nil, []error{WrapServerError(fmt.Sprintf("Unexpected status code: %d.", response.StatusCode))}
}

var bidResponse openrtb.BidResponse

var adheseBidResponseArray []AdheseBid
if err := json.Unmarshal(response.Body, &adheseBidResponseArray); err != nil {
return nil, []error{err, WrapServerError(fmt.Sprintf("Response %v could not be parsed as generic Adhese bid.", string(response.Body)))}
}

var adheseBid = adheseBidResponseArray[0]

if adheseBid.Origin == "JERLICIA" {
SyntaxNode marked this conversation as resolved.
Show resolved Hide resolved
var extArray []AdheseExt
var originDataArray []AdheseOriginData
if err := json.Unmarshal(response.Body, &extArray); err != nil {
return nil, []error{err, WrapServerError(fmt.Sprintf("Response %v could not be parsed to JERLICIA ext.", string(response.Body)))}
}

if err := json.Unmarshal(response.Body, &originDataArray); err != nil {
return nil, []error{err, WrapServerError(fmt.Sprintf("Response %v could not be parsed to JERLICIA origin data.", string(response.Body)))}
}
bidResponse = convertAdheseBid(adheseBid, extArray[0], originDataArray[0])
} else {
bidResponse = convertAdheseOpenRtbBid(adheseBid)
}

price, err := strconv.ParseFloat(adheseBid.Extension.Prebid.Cpm.Amount, 64)
if err != nil {
return nil, []error{err, WrapServerError(fmt.Sprintf("Could not parse Price %v as float ", string(adheseBid.Extension.Prebid.Cpm.Amount)))}
}
width, err := strconv.ParseUint(adheseBid.Width, 10, 64)
if err != nil {
return nil, []error{err, WrapServerError(fmt.Sprintf("Could not parse Width %v as int ", string(adheseBid.Width)))}
}
height, err := strconv.ParseUint(adheseBid.Height, 10, 64)
if err != nil {
return nil, []error{err, WrapServerError(fmt.Sprintf("Could not parse Height %v as int ", string(adheseBid.Height)))}
}
bidResponse.Cur = adheseBid.Extension.Prebid.Cpm.Currency
if len(bidResponse.SeatBid) > 0 && len(bidResponse.SeatBid[0].Bid) > 0 {
bidResponse.SeatBid[0].Bid[0].Price = price
bidResponse.SeatBid[0].Bid[0].W = width
bidResponse.SeatBid[0].Bid[0].H = height
}

bidderResponse := adapters.NewBidderResponseWithBidsCapacity(5)

if len(bidResponse.SeatBid) == 0 {
return nil, []error{WrapServerError("Response resulted in an empty seatBid array.")}
}

var errs []error
for _, sb := range bidResponse.SeatBid {
for i := 0; i < len(sb.Bid); i++ {
bid := sb.Bid[i]
bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
Bid: &bid,
BidType: getBidType(bid.AdM),
})

}
}
return bidderResponse, errs
}

func convertAdheseBid(adheseBid AdheseBid, adheseExt AdheseExt, adheseOriginData AdheseOriginData) openrtb.BidResponse {
adheseExtJson, err := json.Marshal(adheseOriginData)
if err != nil {
glog.Error(fmt.Sprintf("Unable to parse adhese Origin Data as JSON due to %v", err))
adheseExtJson = make([]byte, 0)
}
return openrtb.BidResponse{
ID: adheseExt.Id,
SeatBid: []openrtb.SeatBid{{
Bid: []openrtb.Bid{{
DealID: adheseExt.OrderId,
CrID: adheseExt.Id,
AdM: getAdMarkup(adheseBid, adheseExt),
Ext: adheseExtJson,
}},
Seat: "",
}},
}
}

func convertAdheseOpenRtbBid(adheseBid AdheseBid) openrtb.BidResponse {
var response openrtb.BidResponse = adheseBid.OriginData
if len(response.SeatBid) > 0 && len(response.SeatBid[0].Bid) > 0 {
response.SeatBid[0].Bid[0].AdM = adheseBid.Body
}
return response
}

func getAdMarkup(adheseBid AdheseBid, adheseExt AdheseExt) string {
if adheseExt.Ext == "js" {
if ContainsAny(adheseBid.Body, []string{"<script", "<div", "<html"}) {
counter := ""
if len(adheseExt.ImpressionCounter) > 0 {
counter = "<img src='" + adheseExt.ImpressionCounter + "' style='height:1px; width:1px; margin: -1px -1px; display:none;'/>"
}
return adheseBid.Body + counter
}
if ContainsAny(adheseBid.Body, []string{"<?xml", "<vast"}) {
return adheseBid.Body
}
}
guscarreon marked this conversation as resolved.
Show resolved Hide resolved
return adheseExt.Tag
}

func getBidType(bidAdm string) openrtb_ext.BidType {
if bidAdm != "" && ContainsAny(bidAdm, []string{"<?xml", "<vast"}) {
return openrtb_ext.BidTypeVideo
}
return openrtb_ext.BidTypeBanner
}

func WrapReqError(errorStr string) *errortypes.BadInput {
return &errortypes.BadInput{Message: errorStr}
Copy link
Contributor

Choose a reason for hiding this comment

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

See earlier comment. I don't think it's appropriate to wrap everything as BadInput, which signals that the publisher is making the call wrong. Some instances look better served by BadServerResponse in which you get a 500 back or malformed json.

}

func WrapServerError(errorStr string) *errortypes.BadServerResponse {
return &errortypes.BadServerResponse{Message: errorStr}
}

func ContainsAny(raw string, keys []string) bool {
lowerCased := strings.ToLower(raw)
for i := 0; i < len(keys); i++ {
if strings.Contains(lowerCased, keys[i]) {
return true
}
}
return false

}

func NewAdheseBidder(uri string) *AdheseAdapter {
template, err := template.New("endpointTemplate").Parse(uri)
if err != nil {
glog.Fatal("Unable to parse endpoint url template")
return nil
}
return &AdheseAdapter{endpointTemplate: *template}
}
11 changes: 11 additions & 0 deletions adapters/adhese/adhese_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package adhese

import (
"testing"

"github.com/prebid/prebid-server/adapters/adapterstest"
)

func TestJsonSamples(t *testing.T) {
adapterstest.RunJSONBidderTest(t, "adhesetest", NewAdheseBidder("https://ads-{{.AccountID}}.adhese.com/json"))
}
103 changes: 103 additions & 0 deletions adapters/adhese/adhesetest/exemplary/banner-internal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"mockBidRequest": {
"id": "test-req",
"user": {
"ext": {
"consent" : "dummy"
}
},
"imp": [
{
"id": "test-req",
"banner": {
"format": [
{
"w": 300,
"h": 250
}
]
},
"ext": {
"bidder": {
"account": "demo",
"location": "_adhese_prebid_demo_",
"format": "leaderboard",
"targets":
{
"ci": ["gent", "brussels"],
"ag": ["55"],
"tl": ["all"]
}
}
}
}
],
"site": {
"id": "test",
"publisher": {
"id": "123"
}
}
},
"httpCalls": [
{
"expectedRequest": {
"uri": "https://ads-demo.adhese.com/json/sl_adhese_prebid_demo_-leaderboard/ag55/cigent;brussels/tlall/xtdummy"
},
"mockResponse": {
"status": 200,
"body": [{
"origin": "JERLICIA",
"originInstance": "",
"ext": "js",
"slotName": "_main_page_-leaderboard",
"adType": "leaderboard",
"orderId": "888",
"id": "60613369",
"width": "728",
"height": "90",
"body": "<div style='background-color:red; height:250px; width:300px'></div>",
"tracker": "https://hosts-demo.adhese.com/rtb_gateway/handlers/client/track/?id=a2f39296-6dd0-4b3c-be85-7baa22e7ff4a",
"impressionCounter": "https://hosts-demo.adhese.com/rtb_gateway/handlers/client/track/?id=a2f39296-6dd0-4b3c-be85-7baa22e7ff4a",
"extension": {
"prebid": {
"cpm": {
"amount": "1",
"currency": "USD"
}
}
}
}]
}
}
],
"expectedBidResponses": [
{
"currency": "USD",
"bids": [
{
"bid": {
"adm": "<div style='background-color:red; height:250px; width:300px'></div><img src='https://hosts-demo.adhese.com/rtb_gateway/handlers/client/track/?id=a2f39296-6dd0-4b3c-be85-7baa22e7ff4a' style='height:1px; width:1px; margin: -1px -1px; display:none;'/>",
"w": 728,
"h": 90,
"id": "",
"impid": "",
"price": 1.000000,
"dealid": "888",
"crid": "60613369",
"ext": {
"adFormat": "",
"adType": "leaderboard",
"adspaceId": "",
"libId": "",
"orderProperty": "",
"priority": "",
"viewableImpressionCounter": ""
}
},
"type": "banner"
}
]
}
]
}
Loading