Skip to content
This repository was archived by the owner on Dec 22, 2022. It is now read-only.

Commit

Permalink
Consumable adapter implementation (prebid#801)
Browse files Browse the repository at this point in the history
* 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
nealeu authored and hhhjort committed Feb 19, 2019
1 parent 573b220 commit 21f27a0
Show file tree
Hide file tree
Showing 20 changed files with 828 additions and 5 deletions.
63 changes: 63 additions & 0 deletions adapters/consumable/adtypes.go
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,
}
286 changes: 286 additions & 0 deletions adapters/consumable/consumable.go
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}
}
Loading

0 comments on commit 21f27a0

Please sign in to comment.