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

Consumable adapter implementation #801

Merged
merged 49 commits into from
Feb 19, 2019
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2efc020
Create placeholder type for Consumable adapter.
djcsdy Dec 17, 2018
e0fc6ac
Create placeholder type for Consumable parameters.
djcsdy Dec 17, 2018
32049b3
Create placeholder JSON Schema for Consumable parameters.
djcsdy Dec 17, 2018
a49ea69
Create skeleton implementation of Bidder for ConsumableAdapter.
djcsdy Dec 17, 2018
67638ce
Add placeholder NewConsumableBidder function.
djcsdy Dec 17, 2018
cbe2012
Add Consumable to adapter map.
djcsdy Dec 17, 2018
85ab3b3
Add comment to ExtImpConsumable.
djcsdy Dec 20, 2018
12ca33c
Generate skeleton bid request.
djcsdy Dec 20, 2018
0e06448
Set User-Agent header.
djcsdy Dec 20, 2018
3581845
Set Forwarded and X-Forwarded-For headers.
djcsdy Dec 20, 2018
9daef05
Set Referer header.
djcsdy Dec 20, 2018
8a05b46
Set Origin header.
djcsdy Dec 20, 2018
848ec09
Add type representing ad placement.
djcsdy Dec 20, 2018
9fe53d7
Add type representing user.
djcsdy Dec 20, 2018
926245f
Add type representing Bid Request.
djcsdy Dec 20, 2018
f1e75d6
Add placeholder JSON body to request.
djcsdy Dec 20, 2018
08de3ad
Populate Time field of bid request.
djcsdy Dec 20, 2018
0db0d8a
Populate more fields of bid request.
djcsdy Dec 20, 2018
108cd80
Don't panic if request.Site is nil.
djcsdy Dec 20, 2018
131a7a7
#8 Initial partial implmentation of MakeBids
Jan 3, 2019
a842fbe
#11 Add initial exemplary test to get things going
nealeu Jan 3, 2019
a2f0609
#11 Provide configurable instant time source to allow testing
nealeu Jan 4, 2019
0831344
Partially implement creating placements from impressions.
djcsdy Jan 4, 2019
f98e84f
#8 Some initial correct-looking impl and test for MakeBids
Jan 4, 2019
a5da397
#4 Add ExtImpConsumable contract class
Jan 4, 2019
b470c8c
#6 Add handling of Ext.SiteId and NetworkId
Jan 4, 2019
646f6d3
#6 Add conversion of w,h formats to adtypes in MakeRequests
nealeu Jan 7, 2019
1b694d7
#8 Implement retrieveAd for ad markup
nealeu Jan 7, 2019
ba70cfd
#4 Update JSON Schema to match prebid.js consumable docs
Jan 8, 2019
0f3e297
#16 Add bidder-info/consumable.yaml
Jan 8, 2019
c39245c
#8 Clean up comments in MakeBids
Jan 8, 2019
3f092c6
#12 Add params_test.go
nealeu Jan 8, 2019
de2cae5
#8 MakeBids: Use bid.id=decision.adId and bid.crId=decision.creativeId
nealeu Jan 8, 2019
97211f9
#11 Add a JSON test for absent impressionUrl
nealeu Jan 8, 2019
fa2b367
#9 Implement usersync based on prebid.js adapter code
Jan 9, 2019
7ef1333
Make unitName optional
Jan 17, 2019
303f549
Duplicate networkId,siteId,unitId/Name from first placement to top of…
nealeu Jan 17, 2019
a405c16
Add header info to simple-banner.json test scenario
nealeu Jan 17, 2019
996ac73
#9 Turn buyeruid into azk Cookie header when calling serverbid.com
Jan 18, 2019
9671898
Handle adId as int64 and use for bid.crid. Use internalReq.id for bid.id
nealeu Jan 17, 2019
ac02390
#17 Extract Consumable endpoint and usersync_url to config with defaults
Jan 21, 2019
52d7b0b
#9 Update usersync to work based on templates
nealeu Jan 21, 2019
86cee7c
Add callback URL to usersync for consumable
Jan 24, 2019
f5592fb
Remove additional markup from ad markup - just pass Content.Body through
Jan 28, 2019
599c0d4
Remove println's that were accidentally committed
nealeu Feb 12, 2019
8afa1b0
Organise import so consumable import is in order
nealeu Feb 12, 2019
7429712
Delete fmt.Printf calls that shouldn't be there
Feb 13, 2019
990f703
Clarify why VENDOR_ID is a TODO
Feb 13, 2019
c2f7306
Move getImp and error check within test for a valid decision
Feb 15, 2019
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
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,
}
288 changes: 288 additions & 0 deletions adapters/consumable/consumable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
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}
}

fmt.Printf("%s", bodyBytes)
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 {
imp := getImp(impID, internalRequest.Imp)
nealeu marked this conversation as resolved.
Show resolved Hide resolved
if imp == nil {
errors = append(errors, &errortypes.BadServerResponse{
Message: fmt.Sprintf("ignoring bid id=%s, request doesn't contain any impression with id=%s", "TODO: bid.ID", impID),
})
fmt.Printf("%s", errors[0])
nealeu marked this conversation as resolved.
Show resolved Hide resolved
continue
}

if decision.Pricing != nil && decision.Pricing.ClearPrice != nil {

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 {
fmt.Printf("err: %s", err) // TODO: DELETE
nealeu marked this conversation as resolved.
Show resolved Hide resolved
return nil, nil, []error{&errortypes.BadInput{
Message: err.Error(),
}}
}

var consumableExt openrtb_ext.ExtImpConsumable
if err := json.Unmarshal(bidderExt.Bidder, &consumableExt); err != nil {
fmt.Printf("err: %s", err) // TODO: DELETE
nealeu marked this conversation as resolved.
Show resolved Hide resolved
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