diff --git a/adapters/consumable/adtypes.go b/adapters/consumable/adtypes.go new file mode 100644 index 00000000000..5eb3d3b369e --- /dev/null +++ b/adapters/consumable/adtypes.go @@ -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, +} diff --git a/adapters/consumable/consumable.go b/adapters/consumable/consumable.go new file mode 100644 index 00000000000..476b7ddc180 --- /dev/null +++ b/adapters/consumable/consumable.go @@ -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} +} diff --git a/adapters/consumable/consumable/exemplary/simple-banner.json b/adapters/consumable/consumable/exemplary/simple-banner.json new file mode 100644 index 00000000000..ae57a9f13f6 --- /dev/null +++ b/adapters/consumable/consumable/exemplary/simple-banner.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 250}] + }, + "ext": { + "bidder": { + "networkId": 11, + "siteId": 32, + "unitId": 42 + } + } + } + ], + "device": { + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36", + "ip": "123.123.123.123" + }, + "site": { + "domain": "www.some.com", + "page": "http://www.some.com/page-where-ad-will-be-shown" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://e.serverbid.com/api/v2", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json" + ], + "User-Agent": [ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Forwarded": [ + "for=123.123.123.123" + ], + "Origin": [ + "http://www.some.com" + ], + "Referer": [ + "http://www.some.com/page-where-ad-will-be-shown" + ] + }, + "body": { + "placements": [ + { + "adTypes": [2730], + "divName": "test-imp-id", + "networkId": 11, + "siteId": 32, + "unitId": 42 + } + ], + "networkId": 11, + "siteId": 32, + "unitId": 42, + "time": 1451651415, + "url": "http://www.some.com/page-where-ad-will-be-shown", + "includePricingData": true, + "user":{}, + "enableBotFiltering": true, + "parallel": true + } + }, + "mockResponse": { + "status": 200, + "body": { + "decisions": { + "test-imp-id": { + "adId": 1234567890, + "pricing": { + "clearPrice": 0.5 + }, + "width": 728, + "height": 250, + "impressionUrl": "http://localhost:8080/shown", + "contents" : [ + { + "body": "" + } + ] + } + } + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "", + "crid": "1234567890", + "exp": 30, + "w": 728, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/consumable/consumable/supplemental/simple-banner-no-impressionUrl.json b/adapters/consumable/consumable/supplemental/simple-banner-no-impressionUrl.json new file mode 100644 index 00000000000..66f18d548b5 --- /dev/null +++ b/adapters/consumable/consumable/supplemental/simple-banner-no-impressionUrl.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-no-impUrl-id", + "banner": { + "format": [{"w": 300, "h": 250}] + }, + "ext": { + "bidder": { + "networkId": 11, + "siteId": 32, + "unitId": 42, + "unitName": "the-answer" + } + } + } + ], + "device": { + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36", + "ip": "123.123.123.123" + }, + "site": { + "domain": "www.some.com", + "page": "http://www.some.com/page-where-ad-will-be-shown", + "ref": "http://www.some.com/page-before-the-ad-if-any" + }, + "user": { + "buyeruid": "azk-user-id" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json" + ], + "Cookie": [ + "azk=azk-user-id" + ], + "User-Agent": [ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Forwarded": [ + "for=123.123.123.123" + ], + "Origin": [ + "http://www.some.com" + ], + "Referer": [ + "http://www.some.com/page-where-ad-will-be-shown" + ] + }, + "uri": "https://e.serverbid.com/api/v2", + "body": { + "placements": [ + { + "adTypes": [5], + "divName": "test-no-impUrl-id", + "networkId": 11, + "siteId": 32, + "unitId": 42, + "unitName": "the-answer" + } + ], + "networkId": 11, + "siteId": 32, + "unitId": 42, + "unitName": "the-answer", + "time": 1451651415, + "includePricingData": true, + "user":{}, + "enableBotFiltering": true, + "parallel": true, + "url": "http://www.some.com/page-where-ad-will-be-shown", + "referrer": "http://www.some.com/page-before-the-ad-if-any" + } + }, + "mockResponse": { + "status": 200, + "body": { + "decisions": { + "test-no-impUrl-id": { + "adId": 1234567890, + "bidderName": "aol", + "pricing": { + "clearPrice": 0.5 + }, + "width": 300, + "height": 250, + "contents" : [ + { + "body": "" + } + ] + } + } + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-request-id", + "crid": "1234567890", + "impid": "test-no-impUrl-id", + "price": 0.5, + "adm": "", + "exp": 30, + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/consumable/consumable_test.go b/adapters/consumable/consumable_test.go new file mode 100644 index 00000000000..734ee400523 --- /dev/null +++ b/adapters/consumable/consumable_test.go @@ -0,0 +1,13 @@ +package consumable + +import ( + "testing" + "time" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + clock := knownInstant(time.Date(2016, 1, 1, 12, 30, 15, 0, time.UTC)) + adapterstest.RunJSONBidderTest(t, "consumable", testConsumableBidder(clock, "http://serverbid/api/v2")) +} diff --git a/adapters/consumable/instant.go b/adapters/consumable/instant.go new file mode 100644 index 00000000000..5a32fef8837 --- /dev/null +++ b/adapters/consumable/instant.go @@ -0,0 +1,21 @@ +package consumable + +import "time" + +type instant interface { + Now() time.Time +} + +// Send a real instance when you construct it in adapter_map.go +type realInstant struct{} + +func (_ realInstant) Now() time.Time { + return time.Now() +} + +// Use this for tests e.g. knownInstant(time.Date(y, m, ..., time.UTC)) +type knownInstant time.Time + +func (i knownInstant) Now() time.Time { + return time.Time(i) +} diff --git a/adapters/consumable/params_test.go b/adapters/consumable/params_test.go new file mode 100644 index 00000000000..42de5cb9ca8 --- /dev/null +++ b/adapters/consumable/params_test.go @@ -0,0 +1,59 @@ +package consumable + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/consumable.json +// +// These also validate the format of the external API: request.imp[i].ext.consumable + +// TestValidParams makes sure that the 33across schema accepts all imp.ext fields which we intend to support. +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderConsumable, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected Consumable params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the Consumable schema rejects all the imp.ext fields we don't support. +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderConsumable, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected Consumable params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"networkId": 22, "siteId": 1, "unitId": 101, "unitName": "unit-1"}`, + `{"networkId": 22, "siteId": 1, "unitId": 101, "unitName": "-unit-1"}`, // unitName can start with a dash + `{"networkId": 22, "siteId": 1, "unitId": 101}`, // unitName can be omitted (although prebid.js doesn't allow that) +} + +var invalidParams = []string{ + `{"networkId": 22, "siteId": 1, "unitId": 101, "unitName": "--unit-1"}`, // unitName cannot start -- + `{"networkId": 22, "siteId": 1, "unitId": 101, "unitName": "unit 1"}`, // unitName cannot contain spaces + `{"networkId": 22, "siteId": 1, "unitId": 101, "unitName": "1unit-1"}`, // unitName cannot start with a digit + `{"networkId": "22", "siteId": 1, "unitId": 101, "unitName": 11}`, // networkId must be a number + `{"networkId": 22, "siteId": "1", "unitId": 101, "unitName": 11}`, // siteId must be a number + `{"networkId": 22, "siteId": 1, "unitId": "101", "unitName": 11}`, // unitId must be a number + `{"networkId": 22, "siteId": 1, "unitId": 101, "unitName": 11}`, // unitName must be a string + `{"siteId": 1, "unitId": 101, "unitName": 11}`, // networkId must be present + `{"networkId": 22, "unitId": 101, "unitName": 11}`, // siteId must be present + `{"siteId": 1, "networkId": 22, "unitName": 11}`, // unitId must be present +} diff --git a/adapters/consumable/retrieveAd.go b/adapters/consumable/retrieveAd.go new file mode 100644 index 00000000000..7f69a1bbc23 --- /dev/null +++ b/adapters/consumable/retrieveAd.go @@ -0,0 +1,10 @@ +package consumable + +func retrieveAd(decision decision) string { + + if decision.Contents != nil && len(decision.Contents) > 0 { + return decision.Contents[0].Body + } + + return "" +} diff --git a/adapters/consumable/usersync.go b/adapters/consumable/usersync.go new file mode 100644 index 00000000000..68a93c80e02 --- /dev/null +++ b/adapters/consumable/usersync.go @@ -0,0 +1,19 @@ +package consumable + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +var VENDOR_ID uint16 = 65535 // TODO: Insert consumable value when one is assigned + +func NewConsumableSyncer(temp *template.Template) usersync.Usersyncer { + + return adapters.NewSyncer( + "consumable", + VENDOR_ID, + temp, + adapters.SyncTypeRedirect) +} diff --git a/adapters/consumable/usersync_test.go b/adapters/consumable/usersync_test.go new file mode 100644 index 00000000000..297a863a607 --- /dev/null +++ b/adapters/consumable/usersync_test.go @@ -0,0 +1,22 @@ +package consumable + +import ( + "testing" + "text/template" + + "github.com/stretchr/testify/assert" +) + +func TestConsumableSyncer(t *testing.T) { + + temp := template.Must(template.New("sync-template").Parse( + "//e.serverbid.com/udb/9969/match?redir=http%3A%2F%2Flocalhost%3A8000%2Fsetuid%3Fbidder%3Dconsumable%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")) + + syncer := NewConsumableSyncer(temp) + + u, _ := syncer.GetUsersyncInfo("0", "") + assert.Equal(t, "//e.serverbid.com/udb/9969/match?redir=http%3A%2F%2Flocalhost%3A8000%2Fsetuid%3Fbidder%3Dconsumable%26gdpr%3D0%26gdpr_consent%3D%26uid%3D", u.URL) + assert.Equal(t, "redirect", u.Type) + assert.Equal(t, uint16(65535), syncer.GDPRVendorID()) + assert.Equal(t, false, u.SupportCORS) +} diff --git a/adapters/consumable/utils.go b/adapters/consumable/utils.go new file mode 100644 index 00000000000..64e4872c619 --- /dev/null +++ b/adapters/consumable/utils.go @@ -0,0 +1,20 @@ +package consumable + +import ( + netUrl "net/url" +) + +/** + * Creates a snippet of HTML that retrieves the specified `url` + * Returns HTML snippet that contains the img src = set to `url` + */ +func createTrackPixelHtml(url *string) string { + if url == nil { + return "" + } + + escapedUrl := netUrl.QueryEscape(*url) + img := "
" + return img +} diff --git a/config/config.go b/config/config.go index 0e4b2f8615e..d2ec6c67e69 100644 --- a/config/config.go +++ b/config/config.go @@ -324,6 +324,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAppnexus, "https://ib.adnxs.com/getuid?"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadnxs%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBeachfront, "https://sync.bfmio.com/syncb?pid=155&gdpr={{.GDPR}}&gc={{.GDPRConsent}}&gce=1&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbeachfront%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bio_cid%5D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBrightroll, "https://pr-bh.ybp.yahoo.com/sync/appnexuspbs?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbrightroll%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConsumable, "https://e.serverbid.com/udb/9969/match?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconsumable%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/prebid/match?rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconversant%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderEPlanning, "https://sync.e-planning.net/um?uid"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Deplanning%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") // openrtb_ext.BidderFacebook doesn't have a good default. @@ -444,6 +445,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.beachfront.endpoint", "https://display.bfmio.com/prebid_display") v.SetDefault("adapters.beachfront.platform_id", "155") v.SetDefault("adapters.brightroll.endpoint", "http://east-bid.ybp.yahoo.com/bid/appnexuspbs") + v.SetDefault("adapters.consumable.endpoint", "https://e.serverbid.com/api/v2") v.SetDefault("adapters.conversant.endpoint", "http://api.hb.ad.cpe.dotomi.com/s2s/header/24") v.SetDefault("adapters.eplanning.endpoint", "http://ads.us.e-planning.net/hb/1") v.SetDefault("adapters.ix.endpoint", "http://appnexus-us-east.lb.indexww.com/transbidder?p=184932") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index ebc79c6142b..7a948910cd3 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -14,6 +14,7 @@ import ( "github.com/prebid/prebid-server/adapters/audienceNetwork" "github.com/prebid/prebid-server/adapters/beachfront" "github.com/prebid/prebid-server/adapters/brightroll" + "github.com/prebid/prebid-server/adapters/consumable" "github.com/prebid/prebid-server/adapters/conversant" "github.com/prebid/prebid-server/adapters/eplanning" "github.com/prebid/prebid-server/adapters/grid" @@ -45,6 +46,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter // TODO #615: Update the config setup so that the Beachfront URLs can be configured, and use those in TestRaceIntegration in exchange_test.go openrtb_ext.BidderBeachfront: beachfront.NewBeachfrontBidder(), openrtb_ext.BidderBrightroll: brightroll.NewBrightrollBidder(cfg.Adapters[string(openrtb_ext.BidderBrightroll)].Endpoint), + openrtb_ext.BidderConsumable: consumable.NewConsumableBidder(cfg.Adapters[string(openrtb_ext.BidderConsumable)].Endpoint), openrtb_ext.BidderEPlanning: eplanning.NewEPlanningBidder(client, cfg.Adapters[string(openrtb_ext.BidderEPlanning)].Endpoint), openrtb_ext.BidderGumGum: gumgum.NewGumGumBidder(cfg.Adapters[string(openrtb_ext.BidderGumGum)].Endpoint), openrtb_ext.BidderOpenx: openx.NewOpenxBidder(cfg.Adapters[string(openrtb_ext.BidderOpenx)].Endpoint), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index caef8ce3f9f..8303fa12afc 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -27,6 +27,7 @@ const ( BidderAppnexus BidderName = "appnexus" BidderBeachfront BidderName = "beachfront" BidderBrightroll BidderName = "brightroll" + BidderConsumable BidderName = "consumable" BidderConversant BidderName = "conversant" BidderEPlanning BidderName = "eplanning" BidderFacebook BidderName = "audienceNetwork" @@ -55,6 +56,7 @@ var BidderMap = map[string]BidderName{ "beachfront": BidderBeachfront, "audienceNetwork": BidderFacebook, "brightroll": BidderBrightroll, + "consumable": BidderConsumable, "conversant": BidderConversant, "eplanning": BidderEPlanning, "grid": BidderGrid, diff --git a/openrtb_ext/imp.go b/openrtb_ext/imp.go index 7e1f73ea9b6..947cab077c6 100644 --- a/openrtb_ext/imp.go +++ b/openrtb_ext/imp.go @@ -2,11 +2,12 @@ package openrtb_ext // ExtImp defines the contract for bidrequest.imp[i].ext type ExtImp struct { - Prebid *ExtImpPrebid `json:"prebid"` - Appnexus *ExtImpAppnexus `json:"appnexus"` - Rubicon *ExtImpRubicon `json:"rubicon"` - Adform *ExtImpAdform `json:"adform"` - Rhythmone *ExtImpRhythmone `json:"rhythmone"` + Prebid *ExtImpPrebid `json:"prebid"` + Appnexus *ExtImpAppnexus `json:"appnexus"` + Consumable *ExtImpConsumable `json:"consumable"` + Rubicon *ExtImpRubicon `json:"rubicon"` + Adform *ExtImpAdform `json:"adform"` + Rhythmone *ExtImpRhythmone `json:"rhythmone"` } // ExtImpPrebid defines the contract for bidrequest.imp[i].ext.prebid diff --git a/openrtb_ext/imp_consumable.go b/openrtb_ext/imp_consumable.go new file mode 100644 index 00000000000..09601630d3d --- /dev/null +++ b/openrtb_ext/imp_consumable.go @@ -0,0 +1,10 @@ +package openrtb_ext + +// ExtImpConsumable defines the contract for bidrequest.imp[i].ext.consumable +type ExtImpConsumable struct { + NetworkId int `json:"networkId,omitempty"` + SiteId int `json:"siteId,omitempty"` + UnitId int `json:"unitId,omitempty"` + /* UnitName gets used as a classname and in the URL when building the ad markup */ + UnitName string `json:"unitName,omitempty"` +} diff --git a/static/bidder-info/consumable.yaml b/static/bidder-info/consumable.yaml new file mode 100644 index 00000000000..f12ab2b4016 --- /dev/null +++ b/static/bidder-info/consumable.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "naffis@consumable.com" +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner diff --git a/static/bidder-params/consumable.json b/static/bidder-params/consumable.json new file mode 100644 index 00000000000..b1db53568b9 --- /dev/null +++ b/static/bidder-params/consumable.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Consumable Adapter Params", + "description": "A schema which validates params accepted by the Consumable adapter", + + "type": "object", + "properties": { + "siteId": { + "type": "number", + "description": "The site ID from Consumable", + "pattern": "^[0-9]+$" + }, + "networkId": { + "type": "number", + "description": "The network ID from Consumable", + "pattern": "^[0-9]+$" + }, + "unitId": { + "type": "number", + "description": "The unit ID from Consumable", + "pattern": "^[0-9]+$" + }, + "unitName": { + "type": "string", + "description": "The unit name from Consumable (expected to be a valid CSS class name)", + "pattern": "^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$" + } + }, + "required": ["siteId", "networkId","unitId"] +} diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 760f3ec457d..b2d3bbcc67b 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -13,6 +13,7 @@ import ( "github.com/prebid/prebid-server/adapters/audienceNetwork" "github.com/prebid/prebid-server/adapters/beachfront" "github.com/prebid/prebid-server/adapters/brightroll" + "github.com/prebid/prebid-server/adapters/consumable" "github.com/prebid/prebid-server/adapters/conversant" "github.com/prebid/prebid-server/adapters/eplanning" "github.com/prebid/prebid-server/adapters/grid" @@ -46,6 +47,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderAppnexus, appnexus.NewAppnexusSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderBeachfront, beachfront.NewBeachfrontSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderBrightroll, brightroll.NewBrightrollSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderConsumable, consumable.NewConsumableSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderConversant, conversant.NewConversantSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderEPlanning, eplanning.NewEPlanningSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderFacebook, audienceNetwork.NewFacebookSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 3dc18c4bbab..96a66737bdf 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -21,6 +21,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderBeachfront): syncConfig, string(openrtb_ext.BidderFacebook): syncConfig, string(openrtb_ext.BidderBrightroll): syncConfig, + string(openrtb_ext.BidderConsumable): syncConfig, string(openrtb_ext.BidderConversant): syncConfig, string(openrtb_ext.BidderEPlanning): syncConfig, string(openrtb_ext.BidderGrid): syncConfig,