This repository was archived by the owner on Dec 22, 2022. It is now read-only.
forked from prebid/prebid-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Consumable adapter implementation (prebid#801)
* Create placeholder type for Consumable adapter. * Create placeholder type for Consumable parameters. * Create placeholder JSON Schema for Consumable parameters. * Create skeleton implementation of Bidder for ConsumableAdapter. * Add placeholder NewConsumableBidder function. * Add Consumable to adapter map. * Add comment to ExtImpConsumable. * Generate skeleton bid request. * Set User-Agent header. * Set Forwarded and X-Forwarded-For headers. * Set Referer header. * Set Origin header. * Add type representing ad placement. * Add type representing user. * Add type representing Bid Request. * Add placeholder JSON body to request. * Populate Time field of bid request. * Populate more fields of bid request. * Don't panic if request.Site is nil. * #8 Initial partial implmentation of MakeBids * #11 Add initial exemplary test to get things going * #8 Some initial correct-looking impl and test for MakeBids * Partially implement creating placements from impressions. * #4 Add ExtImpConsumable contract class * #6 Add handling of Ext.SiteId and NetworkId * #11 Provide configurable instant time source to allow testing * #6 Add conversion of w,h formats to adtypes in MakeRequests * #8 Implement retrieveAd for ad markup * #4 Update JSON Schema to match prebid.js consumable docs See https://github.com/prebid/prebid.github.io/blob/b7e1f6fbdbdb87b2b8cd283d311e8ddb2264e599/dev-docs/bidders/consumable.md * #16 Add bidder-info/consumable.yaml * #8 Clean up comments in MakeBids * #12 Add params_test.go * #8 MakeBids: Use bid.id=decision.adId and bid.crId=decision.creativeId Also take out potential nil pointer dereferences so that we don't kill the server on omitted data. * #11 Add a JSON test for absent impressionUrl * #9 Implement usersync based on prebid.js adapter code Only question here is what to do with redirect * Make unitName optional * Duplicate networkId,siteId,unitId/Name from first placement to top of request This is to match requests observed on live sites making calls to serverbid. * Add header info to simple-banner.json test scenario * Handle adId as int64 and use for bid.crid. Use internalReq.id for bid.id Note: Need to validate that bid.id can be same on multiple bids (other adapters use rand(1000) and request.id) * #9 Turn buyeruid into azk Cookie header when calling serverbid.com * #17 Extract Consumable endpoint and usersync_url to config with defaults * #9 Update usersync to work based on templates * Add callback URL to usersync for consumable * Remove additional markup from ad markup - just pass Content.Body through * Remove println's that were accidentally committed * Organise import so consumable import is in order * Delete fmt.Printf calls that shouldn't be there * Clarify why VENDOR_ID is a TODO * Move getImp and error check within test for a valid decision
- Loading branch information
Showing
20 changed files
with
828 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package consumable | ||
|
||
import ( | ||
"github.com/mxmCherry/openrtb" | ||
"strconv" | ||
) | ||
|
||
/* Turn array of openrtb formats into consumable's code*/ | ||
func getSizeCodes(Formats []openrtb.Format) []int { | ||
|
||
codes := make([]int, 0) | ||
for _, format := range Formats { | ||
str := strconv.FormatUint(format.W, 10) + "x" + strconv.FormatUint(format.H, 10) | ||
if code, ok := sizeMap[str]; ok { | ||
codes = append(codes, code) | ||
} | ||
} | ||
return codes | ||
} | ||
|
||
var sizeMap = map[string]int{ | ||
"120x90": 1, | ||
// 120x90 is in twice in prebid.js implementation - probably as spacer | ||
"468x60": 3, | ||
"728x90": 4, | ||
"300x250": 5, | ||
"160x600": 6, | ||
"120x600": 7, | ||
"300x100": 8, | ||
"180x150": 9, | ||
"336x280": 10, | ||
"240x400": 11, | ||
"234x60": 12, | ||
"88x31": 13, | ||
"120x60": 14, | ||
"120x240": 15, | ||
"125x125": 16, | ||
"220x250": 17, | ||
"250x250": 18, | ||
"250x90": 19, | ||
"0x0": 20, // TODO: can this be removed - I suspect it's padding in prebid.js impl | ||
"200x90": 21, | ||
"300x50": 22, | ||
"320x50": 23, | ||
"320x480": 24, | ||
"185x185": 25, | ||
"620x45": 26, | ||
"300x125": 27, | ||
"800x250": 28, | ||
// below order is preserved from prebid.js implementation for easy comparison | ||
"970x90": 77, | ||
"970x250": 123, | ||
"300x600": 43, | ||
"970x66": 286, | ||
"970x280": 3230, | ||
"486x60": 429, | ||
"700x500": 374, | ||
"300x1050": 934, | ||
"320x100": 1578, | ||
"320x250": 331, | ||
"320x267": 3301, | ||
"728x250": 2730, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,286 @@ | ||
package consumable | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"github.com/mxmCherry/openrtb" | ||
"github.com/prebid/prebid-server/adapters" | ||
"github.com/prebid/prebid-server/errortypes" | ||
"github.com/prebid/prebid-server/openrtb_ext" | ||
"net/http" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
type ConsumableAdapter struct { | ||
clock instant | ||
endpoint string | ||
} | ||
|
||
type bidRequest struct { | ||
Placements []placement `json:"placements"` | ||
Time int64 `json:"time"` | ||
NetworkId int `json:"networkId,omitempty"` | ||
SiteId int `json:"siteId"` | ||
UnitId int `json:"unitId"` | ||
UnitName string `json:"unitName,omitempty"` | ||
IncludePricingData bool `json:"includePricingData"` | ||
User user `json:"user,omitempty"` | ||
Referrer string `json:"referrer,omitempty"` | ||
Ip string `json:"ip,omitempty"` | ||
Url string `json:"url,omitempty"` | ||
EnableBotFiltering bool `json:"enableBotFiltering,omitempty"` | ||
Parallel bool `json:"parallel"` | ||
} | ||
|
||
type placement struct { | ||
DivName string `json:"divName"` | ||
NetworkId int `json:"networkId,omitempty"` | ||
SiteId int `json:"siteId"` | ||
UnitId int `json:"unitId"` | ||
UnitName string `json:"unitName,omitempty"` | ||
AdTypes []int `json:"adTypes"` | ||
} | ||
|
||
type user struct { | ||
Key string `json:"key,omitempty"` | ||
} | ||
|
||
type bidResponse struct { | ||
Decisions map[string]decision `json:"decisions"` // map by bidId | ||
} | ||
|
||
/** | ||
* See https://dev.adzerk.com/v1.0/reference/response | ||
*/ | ||
type decision struct { | ||
Pricing *pricing `json:"pricing"` | ||
AdID int64 `json:"adId"` | ||
BidderName string `json:"bidderName,omitempty"` | ||
CreativeID string `json:"creativeId,omitempty"` | ||
Contents []contents `json:"contents"` | ||
ImpressionUrl *string `json:"impressionUrl,omitempty"` | ||
} | ||
|
||
type contents struct { | ||
Body string `json:"body"` | ||
} | ||
|
||
type pricing struct { | ||
ClearPrice *float64 `json:"clearPrice"` | ||
} | ||
|
||
func (a *ConsumableAdapter) MakeRequests(request *openrtb.BidRequest) ([]*adapters.RequestData, []error) { | ||
headers := http.Header{ | ||
"Content-Type": {"application/json"}, | ||
"Accept": {"application/json"}, | ||
} | ||
|
||
if request.Device != nil { | ||
if request.Device.UA != "" { | ||
headers.Set("User-Agent", request.Device.UA) | ||
} | ||
|
||
if request.Device.IP != "" { | ||
headers.Set("Forwarded", "for="+request.Device.IP) | ||
headers.Set("X-Forwarded-For", request.Device.IP) | ||
} | ||
} | ||
|
||
// Set azk cookie to one we got via sync | ||
if request.User != nil { | ||
userID := strings.TrimSpace(request.User.BuyerUID) | ||
if len(userID) > 0 { | ||
headers.Add("Cookie", fmt.Sprintf("%s=%s", "azk", userID)) | ||
} | ||
} | ||
|
||
if request.Site != nil && request.Site.Page != "" { | ||
headers.Set("Referer", request.Site.Page) | ||
|
||
pageUrl, err := url.Parse(request.Site.Page) | ||
if err == nil { | ||
origin := url.URL{ | ||
Scheme: pageUrl.Scheme, | ||
Opaque: pageUrl.Opaque, | ||
Host: pageUrl.Host, | ||
} | ||
|
||
headers.Set("Origin", origin.String()) | ||
} | ||
} | ||
|
||
body := bidRequest{ | ||
Placements: make([]placement, len(request.Imp)), | ||
Time: a.clock.Now().Unix(), | ||
IncludePricingData: true, | ||
EnableBotFiltering: true, | ||
Parallel: true, | ||
} | ||
|
||
if request.Site != nil { | ||
body.Referrer = request.Site.Ref // Effectively the previous page to the page where the ad will be shown | ||
body.Url = request.Site.Page // where the impression will be made | ||
} | ||
|
||
for i, impression := range request.Imp { | ||
|
||
_, consumableExt, err := extractExtensions(impression) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// These get set on the first one in observed working requests | ||
if i == 0 { | ||
body.NetworkId = consumableExt.NetworkId | ||
body.SiteId = consumableExt.SiteId | ||
body.UnitId = consumableExt.UnitId | ||
body.UnitName = consumableExt.UnitName | ||
} | ||
|
||
body.Placements[i] = placement{ | ||
DivName: impression.ID, | ||
NetworkId: consumableExt.NetworkId, | ||
SiteId: consumableExt.SiteId, | ||
UnitId: consumableExt.UnitId, | ||
UnitName: consumableExt.UnitName, | ||
AdTypes: getSizeCodes(impression.Banner.Format), // was adTypes: bid.adTypes || getSize(bid.sizes) in prebid.js | ||
} | ||
} | ||
|
||
bodyBytes, err := json.Marshal(body) | ||
if err != nil { | ||
return nil, []error{err} | ||
} | ||
|
||
requests := []*adapters.RequestData{ | ||
{ | ||
Method: "POST", | ||
Uri: "https://e.serverbid.com/api/v2", | ||
Body: bodyBytes, | ||
Headers: headers, | ||
}, | ||
} | ||
|
||
return requests, nil | ||
} | ||
|
||
/* | ||
internal original request in OpenRTB, external = result of us having converted it (what comes out of MakeRequests) | ||
*/ | ||
func (a *ConsumableAdapter) MakeBids( | ||
internalRequest *openrtb.BidRequest, | ||
externalRequest *adapters.RequestData, | ||
response *adapters.ResponseData, | ||
) (*adapters.BidderResponse, []error) { | ||
|
||
if response.StatusCode == http.StatusNoContent { | ||
return nil, nil | ||
} | ||
|
||
if response.StatusCode == http.StatusBadRequest { | ||
return nil, []error{&errortypes.BadInput{ | ||
Message: fmt.Sprintf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), | ||
}} | ||
} | ||
|
||
if response.StatusCode != http.StatusOK { | ||
return nil, []error{&errortypes.BadServerResponse{ | ||
Message: fmt.Sprintf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), | ||
}} | ||
} | ||
|
||
var serverResponse bidResponse // response from Consumable | ||
if err := json.Unmarshal(response.Body, &serverResponse); err != nil { | ||
return nil, []error{&errortypes.BadServerResponse{ | ||
Message: fmt.Sprintf("error while decoding response, err: %s", err), | ||
}} | ||
} | ||
|
||
bidderResponse := adapters.NewBidderResponse() | ||
var errors []error | ||
|
||
for impID, decision := range serverResponse.Decisions { | ||
|
||
if decision.Pricing != nil && decision.Pricing.ClearPrice != nil { | ||
|
||
imp := getImp(impID, internalRequest.Imp) | ||
if imp == nil { | ||
errors = append(errors, &errortypes.BadServerResponse{ | ||
Message: fmt.Sprintf( | ||
"ignoring bid id=%s, request doesn't contain any impression with id=%s", internalRequest.ID, impID), | ||
}) | ||
continue | ||
} | ||
|
||
bid := openrtb.Bid{} | ||
bid.ID = internalRequest.ID | ||
bid.ImpID = impID | ||
bid.Price = *decision.Pricing.ClearPrice | ||
bid.AdM = retrieveAd(decision) | ||
bid.W = imp.Banner.Format[0].W // TODO: Review to check if this is correct behaviour | ||
bid.H = imp.Banner.Format[0].H | ||
bid.CrID = strconv.FormatInt(decision.AdID, 10) | ||
bid.Exp = 30 // TODO: Check this is intention of TTL | ||
|
||
// not yet ported from prebid.js adapter | ||
//bid.requestId = bidId; | ||
//bid.currency = 'USD'; | ||
//bid.netRevenue = true; | ||
//bid.referrer = utils.getTopWindowUrl(); | ||
|
||
bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ | ||
Bid: &bid, | ||
BidType: getMediaTypeForImp(getImp(bid.ImpID, internalRequest.Imp)), | ||
}) | ||
} | ||
} | ||
return bidderResponse, errors | ||
} | ||
|
||
func getImp(impId string, imps []openrtb.Imp) *openrtb.Imp { | ||
for _, imp := range imps { | ||
if imp.ID == impId { | ||
return &imp | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func extractExtensions(impression openrtb.Imp) (*adapters.ExtImpBidder, *openrtb_ext.ExtImpConsumable, []error) { | ||
var bidderExt adapters.ExtImpBidder | ||
if err := json.Unmarshal(impression.Ext, &bidderExt); err != nil { | ||
return nil, nil, []error{&errortypes.BadInput{ | ||
Message: err.Error(), | ||
}} | ||
} | ||
|
||
var consumableExt openrtb_ext.ExtImpConsumable | ||
if err := json.Unmarshal(bidderExt.Bidder, &consumableExt); err != nil { | ||
return nil, nil, []error{&errortypes.BadInput{ | ||
Message: err.Error(), | ||
}} | ||
} | ||
|
||
return &bidderExt, &consumableExt, nil | ||
} | ||
|
||
func getMediaTypeForImp(imp *openrtb.Imp) openrtb_ext.BidType { | ||
// TODO: Whatever logic we need here possibly as follows - may always be Video when we bid | ||
if imp.Banner != nil { | ||
return openrtb_ext.BidTypeBanner | ||
} else if imp.Video != nil { | ||
return openrtb_ext.BidTypeVideo | ||
} | ||
return openrtb_ext.BidTypeVideo | ||
} | ||
|
||
func testConsumableBidder(testClock instant, endpoint string) *ConsumableAdapter { | ||
return &ConsumableAdapter{testClock, endpoint} | ||
} | ||
|
||
func NewConsumableBidder(endpoint string) *ConsumableAdapter { | ||
return &ConsumableAdapter{realInstant{}, endpoint} | ||
} |
Oops, something went wrong.